We introduce first-class support for Span<T>
and ReadOnlySpan<T>
in the language, including new implicit conversion types and consider them in more places,
allowing more natural programming with these integral types.
Since their introduction in C# 7.2, Span<T>
and ReadOnlySpan<T>
have worked their way into the language and base class library (BCL) in many key ways. This is great for
developers, as their introduction improves performance without costing developer safety. However, the language has held these types at arm's length in a few key ways,
which makes it hard to express the intent of APIs and leads to a significant amount of surface area duplication for new APIs. For example, the BCL has added a number of new
tensor primitive APIs in .NET 9, but these APIs are all offered on ReadOnlySpan<T>
. Because C# doesn't recognize the
relationship between ReadOnlySpan<T>
, Span<T>
, and T[]
, it means that any developers looking to use those APIs with anything other than a ReadOnlySpan<T>
have to explicitly
convert to a ReadOnlySpan<T>
. Further, it also means that they don't have IDE tooling guiding them to use these APIs, since nothing will indicate to the IDE that it is valid
to pass them after conversion. There are also issues with generic inference in these scenarios. In order to provide maximum usability for this style of API, the BCL will have to
define an entire set of Span<T>
and T[]
overloads, which is a lot of duplicate surface area to maintain for no real gain. This proposal seeks to address the problem by
having the language more directly recognize these types and conversions.
We add a new type of implicit conversion to the list in §10.2.1, an implicit span conversion. This conversion is defined as follows:
An implicit span conversion permits array_types
, System.Span<T>
, System.ReadOnlySpan<T>
, and string
to be converted between each other as follows:
- From any single-dimensional
array_type
with element typeEi
toSystem.Span<Ei>
- From any single-dimensional
array_type
with element typeEi
toSystem.ReadOnlySpan<Ui>
, provided thatEi
is covariance-convertible (§18.2.3.3) toUi
- From
System.Span<Ti>
toSystem.ReadOnlySpan<Ui>
, provided thatTi
is covariance-convertible (§18.2.3.3) toUi
- From
System.ReadOnlySpan<Ti>
toSystem.ReadOnlySpan<Ui>
, provided thatTi
is covariance-convertible (§18.2.3.3) toUi
- From
string
toSystem.ReadOnlySpan<char>
We also add implicit span conversion to the list of standard implicit conversions (§10.5.4). This allows overload resolution to consider them when performing argument resolution, as in the previously-linked API proposal.
We also add implicit span conversion to the list of acceptable implicit conversions on the first parameter of an extension method when determining applicability (12.8.9.3) (change in bold):
An extension method
Cᵢ.Mₑ
is eligible if:
Cᵢ
is a non-generic, non-nested class- The name of
Mₑ
is identifierMₑ
is accessible and applicable when applied to the arguments as a static method as shown above- An implicit identity, reference
or boxing, boxing, or span conversion exists from expr to the type of the first parameter ofMₑ
.
The goal of the variance section in implicit span conversion is to replicate some amount of covariance for System.ReadOnlySpan<T>
. Runtime changes would be required to fully
implement variance through generics here (see https://github.com/dotnet/csharplang/blob/main/proposals/ref-struct-interfaces.md for using ref struct
types in generics), but we can
allow a limited amount of covariance through use of a proposed .NET 9 API: dotnet/runtime#96952. This will allow the language to treat System.ReadOnlySpan<T>
as if the T
was declared as out T
in some scenarios. We do not, however, plumb this variant conversion through all variance scenarios, and do not add it to the definition of
variance-convertible in §18.2.3.3. If in the future, we change the runtime
to more deeply understand the variance here, we can take the minor breaking change to fully recognize it in the language.
Practically, this will also mean that in pattern matching for generic scenarios, we'd have behavior as follows:
using System;
M<object[]>(["0"]); // Does not print
M<ReadOnlySpan<string>>(["1"]); // Does not print
M<Span<object>>(["2"]); // Does not print
M<ReadOnlySpan<object>>(["3"]); // Prints
void M<T>(T t) where T : allows ref struct
{
if (t is ReadOnlySpan<object> r) Console.WriteLine(r[0]);
}
In array variance scenarios, this pattern would return true for all reference type arrays:
using System;
M<object[]>(["0"]); // Prints
M<string[]>(["1"]); // Prints
void M<T>(T t)
{
if (t is object[] r) Console.WriteLine(r[0]);
}
There is also an open question below about participation in delegate signature matching.
We update the type inferences section of the specification as follows (changes in bold).
An exact inference from a type
U
to a typeV
is made as follows:
- If
V
is one of the unfixedXᵢ
thenU
is added to the set of exact bounds forXᵢ
.- Otherwise, sets
V₁...Vₑ
andU₁...Uₑ
are determined by checking if any of the following cases apply:
V
is an array typeV₁[...]
andU
is an array typeU₁[...]
of the same rankV
is an array typeV₁[]
andU
is aSpan<U₁>
orReadOnlySpan<U₁>
V
is aSpan<V₁>
andU
is aSpan<U₁>
orReadOnlySpan<U₁>
V
is aReadOnlySpan<V₁>
andU
is aReadOnlySpan<U₁>
V
is the typeV₁?
andU
is the typeU₁
V
is a constructed typeC<V₁...Vₑ>
andU
is a constructed typeC<U₁...Uₑ>
If any of these cases apply then an exact inference is made from eachUᵢ
to the correspondingVᵢ
.- Otherwise, no inferences are made.
A lower-bound inference from a type
U
to a typeV
is made as follows:
- If
V
is one of the unfixedXᵢ
thenU
is added to the set of lower bounds forXᵢ
.- Otherwise, if
V
is the typeV₁?
andU
is the typeU₁?
then a lower bound inference is made fromU₁
toV₁
.- Otherwise, sets
U₁...Uₑ
andV₁...Vₑ
are determined by checking if any of the following cases apply:
V
is an array typeV₁[...]
andU
is an array typeU₁[...]
of the same rankV
is an array typeV₁[]
andU
is aSpan<U₁>
orReadOnlySpan<U₁>
V
is aSpan<V₁>
andU
is aSpan<U₁>
orReadOnlySpan<U₁>
V
is aReadOnlySpan<V₁>
andU
is aReadOnlySpan<U₁>
V
is one ofIEnumerable<V₁>
,ICollection<V₁>
,IReadOnlyList<V₁>>
,IReadOnlyCollection<V₁>
orIList<V₁>
andU
is a single-dimensional array typeU₁[]
V
is a constructedclass
,struct
,interface
ordelegate
typeC<V₁...Vₑ>
and there is a unique typeC<U₁...Uₑ>
such thatU
(or, ifU
is a typeparameter
, its effective base class or any member of its effective interface set) is identical to,inherits
from (directly or indirectly), or implements (directly or indirectly)C<U₁...Uₑ>
.- (The “uniqueness” restriction means that in the case interface
C<T>{} class U: C<X>, C<Y>{}
, then no inference is made when inferring fromU
toC<T>
becauseU₁
could beX
orY
.)
If any of these cases apply then an inference is made from eachUᵢ
to the correspondingVᵢ
as follows:- If
Uᵢ
is not known to be a reference type then an exact inference is made- Otherwise, if
U
is an array type thena lower-bound inference is madeinference depends on the type ofV
:
- If
V
is aSpan<Vᵢ>
, then an exact inference is made- If
V
is an array type or aReadOnlySpan<Vᵢ>
, then a lower-bound inference is made- Otherwise, if
U
is aSpan<Uᵢ>
then inference depends on the type ofV
:
- If
V
is aSpan<Vᵢ>
, then an exact inference is made- If
V
is aReadOnlySpan<Vᵢ>
, then a lower-bound inference is made- Otherwise, if
U
is aReadOnlySpan<Uᵢ>
andV
is aReadOnlySpan<Vᵢ>
a lower-bound inference is made:- Otherwise, if
V
isC<V₁...Vₑ>
then inference depends on thei-th
type parameter ofC
:
- If it is covariant then a lower-bound inference is made.
- If it is contravariant then an upper-bound inference is made.
- If it is invariant then an exact inference is made.
- Otherwise, no inferences are made.
An upper-bound inference from a type
U
to a typeV
is made as follows:
- If
V
is one of the unfixedXᵢ
thenU
is added to the set of upper bounds forXᵢ
.- Otherwise, sets
V₁...Vₑ
andU₁...Uₑ
are determined by checking if any of the following cases apply:
U
is an array typeU₁[...]
andV
is an array typeV₁[...]
of the same rankU
is an array typeU₁[]
andV
is aSpan<V₁>
orReadOnlySpan<V₁>
U
is aSpan<V₁>
andV
is aSpan<V₁>
orReadOnlySpan<V₁>
U
is aReadOnlySpan<V₁>
andV
is aReadOnlySpan<V₁>
U
is one ofIEnumerable<Uₑ>
,ICollection<Uₑ>
,IReadOnlyList<Uₑ>
,IReadOnlyCollection<Uₑ>
orIList<Uₑ>
andV
is a single-dimensional array typeVₑ[]
U
is the typeU1?
andV
is the typeV1?
U
is constructed class, struct, interface or delegate typeC<U₁...Uₑ>
andV
is aclass, struct, interface
ordelegate
type which isidentical
to,inherits
from (directly or indirectly), or implements (directly or indirectly) a unique typeC<V₁...Vₑ>
- (The “uniqueness” restriction means that given an interface
C<T>{} class V<Z>: C<X<Z>>, C<Y<Z>>{}
, then no inference is made when inferring fromC<U₁>
toV<Q>
. Inferences are not made fromU₁
to eitherX<Q>
orY<Q>
.)
If any of these cases apply then an inference is made from eachUᵢ
to the correspondingVᵢ
as follows:- If
Uᵢ
is not known to be a reference type then an exact inference is made- Otherwise, if
V
is an array type thenan upper-bound inference is madeinference depends on the type ofU
:
- If
U
is aSpan<Uᵢ>
, then an exact inference is made- If
U
is an array type or aReadOnlySpan<Uᵢ>
, then a upper-bound inference is made- Otherwise, if
V
is aSpan<Vᵢ>
then inference depends on the type ofU
:
- If
U
is aSpan<Uᵢ>
, then an exact inference is made- If
U
is aReadOnlySpan<Uᵢ>
, then an upper-bound inference is made- Otherwise, if
V
is aReadOnlySpan<Vᵢ>
andU
is aReadOnlySpan<Uᵢ>
an upper-bound inference is made:- Otherwise, if
U
isC<U₁...Uₑ>
then inference depends on thei-th
type parameter ofC
:
- If it is covariant then an upper-bound inference is made.
- If it is contravariant then a lower-bound inference is made.
- If it is invariant then an exact inference is made.
- Otherwise, no inferences are made.
As any proposal that changes conversions of existing scenarios, this proposal does introduce some new breaking changes. Here's a few examples:
By adding implicit span conversions to the list of standard implicit conversions, we can potentially change behavior when user-defined conversions are involved in a type hierarchy. This example shows that change, in comparison to an integer scenario that already behaves as the new C# 13 behavior will.
Span<string> span = [];
var d = new Derived();
d.M(span); // Base today, Derived tomorrow
int i = 1;
d.M(i); // Derived today, demonstrates new behavior
class Base
{
public void M(Span<string> s)
{
Console.WriteLine("Base");
}
public void M(int i)
{
Console.WriteLine("Base");
}
}
class Derived : Base
{
public static implicit operator Derived(ReadOnlySpan<string> r) => new Derived();
public static implicit operator Derived(long l) => new Derived();
public void M(Derived s)
{
Console.WriteLine("Derived");
}
}
By allowing implicit span conversions in extension method lookup, we can potentially change what extension method is resolved by overload resolution.
namespace N1
{
using N2;
public class C
{
public static void M()
{
Span<string> span = new string[0];
span.Test(); // Prints N2 today, N1 tomorrow
}
}
public static class N1Ext
{
public static void Test(this ReadOnlySpan<string> span)
{
Console.WriteLine("N1");
}
}
}
namespace N2
{
public static class N2Ext
{
public static void Test(this Span<string> span)
{
Console.WriteLine("N2");
}
}
}
Should we allow variance conversion in delegate signature matching? For example:
using System;
Span<string> M1() => throw null!;
void M2(ReadOnlySpan<object> r) {}
delegate ReadOnlySpan<string> D1();
delegate void D2(ReadOnlySpan<string> r);
// Should these work?
D1 d1 = M1; // Convert Span<string>() to ReadOnlySpan<string>()
D2 d2 = M2; // Convert void(ReadOnlySpan<object>) to void(ReadOnlySpan<string>)
// These work today
string[] M3() => throw null!;
void M4(object[] a) {}
delegate object[] D3();
delegate void D4(string[] a);
D3 d3 = M3; // Convert string[]() to object[]()
D4 d4 = M4; // Convert void(object[]) to void(string[])
These conversions may not be possible to do without creating a wrapper lambda without runtime changes; the existing variant delegate conversions are possible to emit without needing to create wrappers. We don't have precedent in the language for silent wrappers like this, and generally require users to create such wrapper lambdas themselves.
Keep things as they are.