Skip to content

Latest commit

 

History

History
72 lines (55 loc) · 6.03 KB

LDM-2024-01-10.md

File metadata and controls

72 lines (55 loc) · 6.03 KB

C# Language Design Meeting for January 10th, 2024

Agenda

Quote of the Day

  • Nothing particularly amusing was said today

Discussion

Collection expressions: conversion vs construction

#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 return void or bool.
    • This would mean that older collection types like ImmutableArray<T> version 7.0 wouldn't be considered convertible.
  • Only forbid Add methods that return the type itself, like ImmutableArray<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.

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 strings 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.

Conclusions

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.