- Nothing particularly amusing was said today
#5354
https://github.com/dotnet/csharplang/blob/d7bbc5456e51bf29ece89112a2bb10153e98a524/proposals/csharp-12.0/collection-expressions.md#should-collection-expression-conversion-require-availability-of-a-minimal-set-of-apis-for-construction
Today we looked at another scenario in collection expressions brought up by our work on params
improvements. This time, we considered scenarios
that would be an error when used as params
, but due to how collection expressions were specified, cannot be considered an error at definition, only
at usage; basically any type that implements IEnumerable
or IEnumerable<T>
is valid to be used as a params
type, even if it cannot be constructed
in any real scenario. This means that, rather than the method author getting an error that their params
parameter is invalid, the end user gets a worse
error about being unable to construct the parameter type. To address that, we are looking at two potential changes: require that the type has an accessible
constructor, and an accessible Add
method that can be invoked with the iteration type of the collection expression. We would then further restrict
params
to require that these are as accessible as the method itself, to ensure that if something is defined as params
, it can be used as params
when
seen.
We do, however, have some concerns, particularly around API evolution. There's a tension here between what we want to be considered convertible to collection
expressions, and what we want to prevent. A prime example is System.Collection.ImmutableArray<T>
; this type has always supported collection initializers
at compile-time, because it has an Add
method, but it then blows up at runtime because Add
doesn't actually mutate the underlying array, it instead
returns a new ImmutableArray
, and produces a NullReferenceException
when doing so on a new ImmutableArray<T>()
. We could try to prevent the compiler
from recognizing invalid Add
methods, but we do still want to recognize the older version of ImmutableArray<T>
for collection expressions. By doing
so, we ensure that overload resolution gives good errors (ImmutableArray<T>
isn't constructible), rather than giving errors that no applicable methods
could be found; worse, not recognizing older versions of ImmutableArray<T>
as valid conversion targets for collection expressions could then mean that
upgrading System.Collections.Immutable
potentially causes a source-breaking change in overload resolution. The point is somewhat moot for ImmutableArray<T>
,
as it has already been upgraded and the compiler has taken a bit of liberty with older versions to mark them as bad at compile-time, but we have no guarantees
that other community-created types that have similar construction foibles have indeed upgraded to use CollectionBuilderAttribute
. We debated a few different
possible restrictions:
- Only allow
Add
methods that returnvoid
orbool
.- This would mean that older collection types like
ImmutableArray<T>
version 7.0 wouldn't be considered convertible.
- This would mean that older collection types like
- Only forbid
Add
methods that return the type itself, likeImmutableArray<T>.Add
does.- This doesn't solve the v7.0 problem from the above problem.
- It may still exclude valid types that have a fluent calling style.
- What if we made the existence of a conversion only look for an accessible
Add
, and then further restrict "creatability" further down the line?- This would us to keep overload resolution stable for older APIs, and then let them upgrade to
CollectionBuilderAttribute
in new versions without potentially introducing a source-breaking change.
- This would us to keep overload resolution stable for older APIs, and then let them upgrade to
We distilled an important characteristic from these discussions: we don't think that IEnumerable<T>
, by itself, is enough of a signal to make a type
be a creatable collection type. Instead, what we're looking for is a set of restrictions that signal the intent of the type author that users should be able
to construct the collection type. For CollectionBuilderAttribute
, that is enough of a signal by itself. However, we need a similar rule for IEnumerable<T>
,
and we think the existing signal for collection initializers serves as a good, well-established signal. Given this, we re-examined our handling of string
.
We intentionally left string
as a valid conversion target for collection expressions, but under the newly proposed rules it would not be. After some more
thinking, we're fine with this. In particular, we think that, even if we were to implement support for collection expressions to create string
s in the future,
we'd need to deprioritize it in overload resolution compared to char[]
. Given that, it doesn't make sense to hold a place for it, and we'll let the new rules
mark it as not a valid conversion target.
Finally today, we looked at additional restrictions for params
parameters, on top of what we considered today. In particular, we want to make sure that, when
a user declares a method with a params
parameter, you don't have to have a specific using
in order to use it in that format. To that end, we will require
that the accessible Add
method for params
be an instance member, rather than an extension method.
We accept the proposed rules for collection expressions:
For a struct or class type that implements System.Collections.IEnumerable and that does not have a create method conversions section should require presence of at least the following APIs:
- An accessible constructor that is applicable with no arguments.
- An accessible Add instance or extension method that can be invoked with value of iteration type as the argument.
We will not save a spot for string
.
For params
parameters, we additionally require that the Add
method be an instance method.