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₁and- T₂are collection types
- E₁is the element type of- T₁
- E₂is the element type of- T₂
- CE₁ᵢare the series of conversions from- ELᵢto- E₁
- CE₂ᵢare the series of conversions from- ELᵢto- E₂
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 to- E₂, and the element conversions to- E₁are better than the element conversions to- E₂, or
- E₁has an identity conversion to- E₂, and one of the following holds:- T₁is- System.ReadOnlySpan<E₁>, and- T₂is- System.Span<E₂>, or
- T₁is- System.ReadOnlySpan<E₁>or- System.Span<E₁>, and- T₂is an array_or_array_interface with element type- E₂
 
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₁ᵢvsCE₂ᵢ | 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