Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Note
This article is a feature specification. The specification serves as the design document for the feature. It includes proposed specification changes, along with information needed during the design and development of the feature. These articles are published until the proposed spec changes are finalized and incorporated in the current ECMA specification.
There may be some discrepancies between the feature specification and the completed implementation. Those differences are captured in the pertinent language design meeting (LDM) notes.
You can learn more about the process for adopting feature speclets into the C# language standard in the article on the specifications.
Champion issue: https://github.com/dotnet/csharplang/issues/8374
Summary
Updates to the better conversion rules to be more consistent with params, and better handle current ambiguity scenarios. For example, ReadOnlySpan<string> vs ReadOnlySpan<object> can currently
cause ambiguities during overload resolution for [""].
Detailed Design
The following are the better conversion from expression rules. These replace the rules in https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/collection-expressions.md#overload-resolution.
These rules are:
Given an implicit conversion
C₁that converts from an expressionEto a typeT₁, and an implicit conversionC₂that converts from an expressionEto a typeT₂,C₁is a better conversion thanC₂if one of the following holds:
Eis a collection expression, andC₁is a better collection conversion from expression thanC₂Eis not a collection expression and one of the following holds:
Eexactly matchesT₁andEdoes not exactly matchT₂Eexactly matches both or neither ofT₁andT₂, andT₁is a better conversion target thanT₂Eis a method group, ...
We add a new definition for better collection conversion from expression, as follows:
Given:
Eis a collection expression with element expressions[EL₁, EL₂, ..., ELₙ]T₁andT₂are collection typesE₁is the element type ofT₁E₂is the element type ofT₂CE₁ᵢare the series of conversions fromELᵢtoE₁CE₂ᵢare the series of conversions fromELᵢtoE₂
If there is an identity conversion from E₁ to E₂, then the element conversions are as good as each other. Otherwise, the element conversions to E₁ are better than the element conversions to E₂ if:
- For every
ELᵢ,CE₁ᵢis at least as good asCE₂ᵢ, and - There is at least one i where
CE₁ᵢis better thanCE₂ᵢOtherwise, neither set of element conversions is better than the other, and they are also not as good as each other.
Conversion comparisons are made using better conversion from expression ifELᵢis not a spread element. IfELᵢis a spread element, we use better conversion from the element type of the spread collection toE₁orE₂, respectively.
C₁ is a better collection conversion from expression than C₂ if:
- Both
T₁andT₂are not span types, andT₁is implicitly convertible toT₂, andT₂is not implicitly convertible toT₁, or E₁does not have an identity conversion toE₂, and the element conversions toE₁are better than the element conversions toE₂, orE₁has an identity conversion toE₂, and one of the following holds:T₁isSystem.ReadOnlySpan<E₁>, andT₂isSystem.Span<E₂>, orT₁isSystem.ReadOnlySpan<E₁>orSystem.Span<E₁>, andT₂is an array_or_array_interface with element typeE₂
Otherwise, neither collection type is better, and the result is ambiguous.
Note
These rules mean that methods that expose overloads that take different element types and without a conversion between the collection types are ambiguous for empty collection expressions. As an example:
public void M(ReadOnlySpan<int> ros) { ... }
public void M(Span<int?> span) { ... }
M([]); // Ambiguous
Scenarios:
In plain English, the collection types themselves must be either the same, or unambiguously better (ie, List<T> and List<T> are the same, List<T> is unambiguously better than IEnumerable<T>, and List<T> and HashSet<T> cannot be compared), and
the element conversions for the better collection type must also be the same or better (ie, we can't decide between ReadOnlySpan<object> and Span<string> for [""], the user needs to make that decision). More examples of this are:
T₁ |
T₂ |
E |
C₁ Conversions |
C₂ Conversions |
CE₁ᵢ vs CE₂ᵢ |
Outcome |
|---|---|---|---|---|---|---|
List<int> |
List<byte> |
[1, 2, 3] |
[Identity, Identity, Identity] |
[Implicit Constant, Implicit Constant, Implicit Constant] |
CE₁ᵢ is better |
List<int> is picked |
List<int> |
List<byte> |
[(int)1, (byte)2] |
[Identity, Implicit Numeric] |
Not applicable | T₂ is not applicable |
List<int> is picked |
List<int> |
List<byte> |
[1, (byte)2] |
[Identity, Implicit Numeric] |
[Implicit Constant, Identity] |
Neither is better | Ambiguous |
List<int> |
List<byte> |
[(byte)1, (byte)2] |
[Implicit Numeric, Implicit Numeric] |
[Identity, Identity] |
CE₂ᵢ is better |
List<byte> is picked |
List<int?> |
List<long> |
[1, 2, 3] |
[Implicit Nullable, Implicit Nullable, Implicit Nullable] |
[Implicit Numeric, Implicit Numeric, Implicit Numeric] |
Neither is better | Ambiguous |
List<int?> |
List<ulong> |
[1, 2, 3] |
[Implicit Nullable, Implicit Nullable, Implicit Nullable] |
[Implicit Numeric, Implicit Numeric, Implicit Numeric] |
CE₁ᵢ is better |
List<int?> is picked |
List<short> |
List<long> |
[1, 2, 3] |
[Implicit Numeric, Implicit Numeric, Implicit Numeric] |
[Implicit Numeric, Implicit Numeric, Implicit Numeric] |
CE₁ᵢ is better |
List<short> is picked |
IEnumerable<int> |
List<byte> |
[1, 2, 3] |
[Identity, Identity, Identity] |
[Implicit Constant, Implicit Constant, Implicit Constant] |
CE₁ᵢ is better |
IEnumerable<int> is picked |
IEnumerable<int> |
List<byte> |
[(byte)1, (byte)2] |
[Implicit Numeric, Implicit Numeric] |
[Identity, Identity] |
CE₂ᵢ is better |
List<byte> is picked |
int[] |
List<byte> |
[1, 2, 3] |
[Identity, Identity, Identity] |
[Implicit Constant, Implicit Constant, Implicit Constant] |
CE₁ᵢ is better |
int[] is picked |
ReadOnlySpan<string> |
ReadOnlySpan<object> |
["", "", ""] |
[Identity, Identity, Identity] |
[Implicit Reference, Implicit Reference, Implicit Reference] |
CE₁ᵢ is better |
ReadOnlySpan<string> is picked |
ReadOnlySpan<string> |
ReadOnlySpan<object> |
["", new object()] |
Not applicable | [Implicit Reference, Identity] |
T₁ is not applicable |
ReadOnlySpan<object> is picked |
ReadOnlySpan<object> |
Span<string> |
["", ""] |
[Implicit Reference] |
[Identity] |
CE₂ᵢ is better |
Span<string> is picked |
ReadOnlySpan<object> |
Span<string> |
[new object()] |
[Identity] |
Not applicable | T₁ is not applicable |
ReadOnlySpan<object> is picked |
ReadOnlySpan<InterpolatedStringHandler> |
ReadOnlySpan<string> |
[$"{1}"] |
[Interpolated String Handler] |
[Identity] |
CE₁ᵢ is better |
ReadOnlySpan<InterpolatedStringHandler> is picked |
ReadOnlySpan<InterpolatedStringHandler> |
ReadOnlySpan<string> |
[$"{"blah"}"] |
[Interpolated String Handler] |
[Identity] - But constant |
CE₂ᵢ is better |
ReadOnlySpan<string> is picked |
ReadOnlySpan<string> |
ReadOnlySpan<FormattableString> |
[$"{1}"] |
[Identity] |
[Interpolated String] |
CE₂ᵢ is better |
ReadOnlySpan<string> is picked |
ReadOnlySpan<string> |
ReadOnlySpan<FormattableString> |
[$"{1}", (FormattableString)null] |
Not applicable | [Interpolated String, Identity] |
T₁ isn't applicable |
ReadOnlySpan<FormattableString> is picked |
HashSet<short> |
Span<long> |
[1, 2] |
[Implicit Constant, Implicit Constant] |
[Implicit Numeric, Implicit Numeric] |
CE₁ᵢ is better |
HashSet<short> is picked |
HashSet<long> |
Span<short> |
[1, 2] |
[Implicit Numeric, Implicit Numeric] |
[Implicit Constant, Implicit Constant] |
CE₂ᵢ is better |
Span<short> is picked |
Open questions
How far should we prioritize ReadOnlySpan/Span over other types?
As specified today, the following overloads would be ambiguous:
C.M1(["Hello world"]); // Ambiguous, no tiebreak between ROS and List
C.M2(["Hello world"]); // Ambiguous, no tiebreak between Span and List
C.M3(["Hello world"]); // Ambiguous, no tiebreak between ROS and MyList.
C.M4(["Hello", "Hello"]); // Ambiguous, no tiebreak between ROS and HashSet. Created collections have different contents
class C
{
public static void M1(ReadOnlySpan<string> ros) {}
public static void M1(List<string> list) {}
public static void M2(Span<string> ros) {}
public static void M2(List<string> list) {}
public static void M3(ReadOnlySpan<string> ros) {}
public static void M3(MyList<string> list) {}
public static void M4(ReadOnlySpan<string> ros) {}
public static void M4(HashSet<string> hashset) {}
}
class MyList<T> : List<T> {}
How far do we want to go here? The List<T> variant seems reasonable, and subtypes of List<T> exist aplenty. But the HashSet version has very different semantics, how sure are we that it's actually "worse"
than ReadOnlySpan in this API?
C# feature specifications