- First-class span types questions
- Delegate conversions
- Variant conversion existence
- Overload resolution priority questions
- Attribute shape and inheritance
- Extension overload resolution
- "That's my interpretation, and you can't check my math anyways."
- "Who's on first should be on the citizenship test"
First up, we considered whether we should try and replicate covariance in method group assignment conversions. On the one hand, the example code seems sensible enough, and would certainly work today
with arrays. However, we don't have precedent for emitting thunks for delegate assignments. Further, while we could make the initial conversion work easily enough, we couldn't then make converting from
Func<ReadOnlySpan<string>>
to Func<ReadOnlySpan<object>>
transparent, like converting from Func<string[]>
to Func<object[]>
is seamless and preserves reference identity. Looking at the BCL use cases
for this, we don't see anything that is planning to take advantage of this type of method-group variance; given these caveats, we have decided to not support variance in method-group assignment here.
Variance in delegate assignment is not supported.
We next considered whether we should support covariance in ReadOnlySpan<T>
when the helper method, ReadOnlySpan<T>.CastUp<TDerived>
, is not present. Supporting this down-level could be potentially quite
complicated. We considered a few different approaches:
- We could consider the conversion to only exist when
CastUp<TDerived>
exists. This ensures that if anyone depends on the behavior, it must be present, but it causes overload resolution to be unstable within a single language version, and increases the decoder ring of what a user would need to know to understand why a particular overload was or wasn't picked. It avoids breaking changes when a user turns on C# 13 and doesn't upgrade to .NET 9, but the complexity tradeoff seems like it will not be worth it. - We could consider the conversion to always exist, but issue an error when we cannot find the required
CastUp<TDerived>
method. This is fairly in-line with what we do for other language features that require specific features, but we're concerned about the breaking change potential here. When a user upgrades to C# 13, these new conversions may cause overload resolution to pick a new member. That resolution could then cause an error becauseCastUp<TDerived>
doesn't exist. - We could generate a
<PrivateImplementationDetails>
method for converting the span. On platforms with built-inReadOnlySpan<T>
support, and in particular .NET Core 2.2 and later, this seems like it would work fine. However, we're concerned about other platforms; users can bring their ownReadOnlySpan<T>
implementations, and there's no guarantee that whatever bit-blit strategy the compiler choses would be safe for that implementation. We also aren't prepared to make strong statements on whether this would always be safe on the .NET Framework implementation ofReadOnlySpan<T>
. - We thought about whether we could ship an update to
System.Memory
. However, this would be quite complicated; we'd actually need to ship a new package, perhapsSystem.MemoryEx
orSystem.Memory2
, otherwise code that compiled against .NET Standard 2.0 and depended on the new functionality might break when run on .NET 8. - We considered whether we should abandon variance entirely. After all, the main thrust of the proposal is not variance; that's a "nice to have" on top of the type inference and extension method features. However, we think it would be a shame to avoid a useful language feature because we may issue an error down-level; especially given that the feature is a corner case scenario and users that want to use newer C# features on down-level platforms have to contend with far more obvious missing things.
Given all these options, we eventually settled on 2: when in C# 13+, the conversion will always exist, but the compiler will issue an error when it cannot find CastUp<TDerived>
.
Finally, we thought about whether extensions should be able to provide ReadOnlySpan<T>.CastUp<TDerived>
. That method is a static method on ReadOnlySpan<T>
, so it could theoretically be provided by extension
types. However, we don't think we're ready to decide, in case or in general. We'll have to consider extension types more generally with compiler pattern matching; for today, we'll simply say it cannot be provided
by an extension type, and revisit when we're ready to consider the question holistically across C#.
In C# 13, the variance conversion will always exist, and the compiler will issue an error if it cannot find ReadOnlySpan<T>.CastUp<TDerived>
.
First up in overload resolution priority, we considered the shape of the attribute. As part of this, we need to consider the inheritance behavior, to at least some degree, in order to decide what the Inherited
attribute usage property should be. For this, we looked to prior art in C#: method types, params
, parameter names, and default values. Unfortunately, there's no singular consistent rule here, but we do have
the principles to guide us; most things look at the least-derived members. For parameter names and default values, however, we look at the most-derived, which keeps us consistent with the semantics VB already
had for these scenarios. We don't think that there's reason to use that most-derived logic here; if anything, we think that would run counter to the proposal itself, which is designed to only be used within a
single type. Allowing overrides to change the priority of a member would defeat this; derived types can always hide the member and create their own version if they wish to adjust priority, as they can today.
Given this, Inherited
should be false, as we don't want it to show up in reflection on overriding members when it's not permitted in source.
We are satisfied with the proposed shape of the attribute. Overload resolution will always look at the least-derived member definition for its priority. We still have open questions on whether we should hard-block the attribute application on overrides, and what we will do for implementations of interface members that have a priority.
Finally today, we considered the question of whether overload resolution should behave identically for instance and extension lookups, or if we should not group extension methods by type before doing overload resolution priority sorting. We don't have any current use-cases for inter-extension priority, and we also think that not grouping would again run against the "this is a tool within a type only" principle from the proposal.
We will always group by declaring type before removing lower priority overloads. The given example will print "Ext2 ReadOnlySpan"
.