- "Anything else for you?"
Champion issue: #8714
The first issue we looked at today was around how array covariance can impact conversions to Span<T>
. Because .NET has array
covariance, the conversion from an array to a Span<T>
has to check to make sure that the array is a T[]
, not some subtype
of T[]
; if this is the case, then the conversion will throw. This is a footgun for usages of this implicit conversion, and
one that was hit immediately on upgrade to C# 14 preview by Roslyn because of the triplet of IEnumerable<T>
, Span<T>
, and
ReadOnlySpan<T>
in test assert APIs. It is fairly common for some expected data to be an array of some specific subtype, while
the actual data being converted is a supertype. Previously this would have gone through IEnumerable<T>
, which was perfectly
fine with covariant arrays. Span<T>
is now the preferred overload, and that isn't fine with covariant arrays.
This issue, where an implicit conversion can throw, isn't entirely new to the language; after all, dynamic
can be implicitly
converted to any type, and that can throw. But it is likely more prominent than dynamic
, as with that feature, the user usually
intentionally started at dynamic
and went to a specific type, rather than starting at array and then invisibly going to a Span<T>
where IEnumerable<T>
used to be fine. While we don't think that we have reason to entirely remove the array->Span
conversion
from the feature, we do think that this merits some work to make sure that when there is a safer conversion available that likely
has the same intended meaning, we prefer that conversion instead.
The proposal here is to make ReadOnlySpan<T>
preferred over Span<T>
for first-class span. From an immediate type theory
perspective, this is an exception how most-specific type normally works. Since Span<T>
can be converted to ReadOnlySpan<T>
,
but not the other way around, our standard most-specific type logic would indicate that Span<T>
should be preferrable. However,
for these types of APIs that overloaded across Span<T>
and ReadOnlySpan<T>
, we think that the only reason they're overloaded
is because we currently don't have first-class spans as a language feature. Both overloads were added to make the API usable,
but the only thing the API needs is read access, and the Span<T>
API usually just forwards to the ReadOnlySpan<T>
version.
The only time this reasoning doesn't hold up is when we consider extension methods across multiple different types; such overloads
could indeed have different behavior, where one overload really does need mutable access, while the other only reads from the array.
Despite this, we think the overall feature is better served by preferring ReadOnlySpan<T>
over Span<T>
.
We also considered the question of whether we should take a bigger step. Our proposed rules only impact when arrays are converted
to span types. This is yet another element to a decoder ring, another special case that users need to know to understand how
overload resolution will be applied to their APIs. Could we instead make a broader, simpler rule, where we just always prefer
ReadOnlySpan<T>
over Span<T>
? We don't think we have enough data to make such a decision today. We don't know of any APIs that
this would negatively affect, but more investigation needs to be done before making a call.
Finally on this topic, we considered how restrictive to make the preference on type parameters. It seems a little suspicious to
us to prefer ReadOnlySpan<object>
over Span<string>
; can we really make the assertions we said earlier about the expected behavior
of the API for this type of shape? We'll investigate this as well, and see whether there are examples of this pattern in the wild.
We adopt the rule preferring ReadOnlySpan<T>
over Span<T>
for array conversions. We will investigate broader rules as well, and
their potential impact on the BCL.
Our last topic of the day was considering whether we should try and handle the conversion specially in expression trees, as this
can be a breaking change. We had a
similar discussion
with params ReadOnlySpan
, and we continue to find our reasoning from that issue compelling. Therefore, we will not make any
changes to how binding occurs in expression trees; they will potentially pick up new span-based overloads and users may have to
insert explicit casts to get old behavior back.
No special cases for expression trees.