- "What's the second question?" "Type inference for collection literals." "Oh that's easy"
- "Second2, also known as third"
- "Oh, I thought [redacted] was making a 'type inference walks into a bar' joke"
Today we looked at whether overload resolution should have special tie-breaking rules for the Span<T>
vs IEnumerable<T>
case. There are a
few cases that can this affect, and potentially blocks the runtime from adding overloads that take ReadOnlySpan<T>
for fear of breaking users.
var ia1 = ImmutableArray.CreateRange<int>(new[] { 1, 2, 3 }); // error: CreateRange() is ambiguous
var ia2 = ImmutableArray.CreateRange<char>("Test"); // error: CreateRange() is ambiguous
public static class ImmutableArray
{
public static ImmutableArray<T> CreateRange<T>(IEnumerable<T> items) { ... }
public static ImmutableArray<T> CreateRange<T>(ReadOnlySpan<T> items) { ... }
}
For both examples, these are types that are convertible to both a Span<T>
and to IEnumerable<T>
. There is no commonality between those two
types, other than object
, so the overloads are ambiguous. There are two questions here:
- Do we want to mimic a subtype relationship between
IEnumerable<T>
andSpan<T>
as best we can withoutSpan<T>
actually implementing the interface? - Is
Span<T>
special here? Are there other user-definedref struct
s that can run into this same problem with ambiguous overloads?
We're a bit concerned with chasing a subtype relationship here. Ideologically, Span<T>
is indeed an IEnumerable<T>
, but since ref struct
s
cannot implement interfaces today, the relationship isn't there. But that relationship plays in more than just overload resolution. Generics, for
example, which is another place ref struct
s cannot be used today. It feels like trying to mimic the behavior as best as possible would be a
large rabbit hole to go down, when we do want ref struct
s to be able to implement interfaces at some point. Implementing that would solve this
issue and in a more general fashion.
We also talked a bit about workaround the runtime could do for these cases. Defining more overloads is an obvious answer for some scenarios, such
as the ImmutableArray
case above. That could define CreateRange<T>(T[] array)
and CreateRange(string s)
; indeed, this is the solution that
DefaultInterpolatedStringHandler
used to avoid ambiguities in AppendFormatted
. But that doesn't work in every case. For example, constructors
of generic types cannot have specialized versions for a specific generic. In other words, HashSet<char>
(and only HashSet<char>
) cannot have
a constructor that takes a string
to resolve that ambiguity. Extensions might be able to solve that, but it doesn't seem like it would give the
actual end experience we want, namely the subtype relationship between Span<T>
and IEnumerable<T>
. Given this, we plan on waiting until
ref struct
s can implement interfaces and then all of these issues will work themselves out with no additional rules needed.
No language change here.
We also looked at the proposed type inference for collection literals. We explored our existing behavior here around several areas:
- Collection initializers on arrays - Arrays are not target-typed today, so conversion-from-expressions do not carry through the collection initializer here. We think this actually makes array collection initializers a poor analogy for where to draw behavior from.
- Switch expressions - These are target typed, and arm types can influence the type inference of a method.
- Tuple literals - These might be the best analogy. They are target typed, and have a general desired behavior of "you should be able to replace
the tuple with the elements and have the same behavior". In other words, for a signature of
M<T>((T, T) tuple)
, passing a tuple literal should have the same outcome as passing each element individually to a signature ofM<T>(T t1, T t2)
.
We like the tuple analogy for collection literals. The behavior we want is the equivalent, spelled out as:
If there is a signature M<T>(T[] array)
, passing a collection literal of [1, 2]
should have the same effect as if the signature was
M<T>(T t1, T t2)
.
Treat collection literals transparently for type inference.