Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collection expression arguments: open questions #9158

Merged
merged 9 commits into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 68 additions & 18 deletions proposals/collection-expression-arguments.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,13 +398,55 @@ Should arguments with `dynamic` type be allowed? That might require using the ru

## Open questions

### Should arguments affect collection expression conversion?

Should collection arguments and the applicable methods affect convertibility of the collection expression?
```csharp
Print([with(comparer: null), 1, 2, 3]); // ambiguous or Print<int>(HashSet<int>)?

static void Print<T>(List<T> list) { ... }
static void Print<T>(HashSet<T> set) { ... }
```

If the arguments affect convertibility based on the applicable methods, arguments should probably affect type inference as well.
```csharp
Print([with(comparer: StringComparer.Ordinal)]); // Print<string>(HashSet<string>)?
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to above, the following is not legal today: UseMyCollection(new(1));. So i would expect the same to be true with CEs


For reference, similar cases with target-typed `new()` result in errors.
```csharp
Print<int>(new(comparer: null)); // error: ambiguous
Print(new(comparer: StringComparer.Ordinal)); // error: type arguments cannot be inferred
```

### Target types where arguments are *required*

Should collection expression conversions be supported to target types where arguments must be supplied because all of the constructors or factory methods require at least one argument?

Such types could be used with collection expressions that include explicit `with()` arguments but the types could not be used for `params` parameters.

A collection type where the constructor is called directly:
For example, consider the following type constructed from a factory method:
```csharp
MyCollection<object> c;
c = []; // error: no arguments
c = [with(capacity: 1)]; // ok

[CollectionBuilder(typeof(MyBuilder), "Create")]
class MyCollection<T> : IEnumerable<T> { ... }

class MyBuilder
{
public static MyCollection<T> Create<T>(ReadOnlySpan<T> items, int capacity) { ... }
}
```

The same question applies for when the constructor is called directly as in the example below.
However, for the target types where the constructor is called directly, the collection expression *conversion* currently requires a constructor callable with no arguments. We would need to remove that conversion requirement to support such types.

```csharp
c = []; // error: no arguments
c = [with(capacity: 1)]; // error: no constructor callable with no arguments?

class MyCollection<T> : IEnumerable<T>
{
public MyCollection(int capacity) { ... }
Expand All @@ -413,36 +455,44 @@ class MyCollection<T> : IEnumerable<T>
}
```

A collection type constructed with a builder method:
```csharp
[CollectionBuilder(typeof(MyBuilder), "Create")]
class MyCollection<T> : IEnumerable<T> { ... }
### Collection builder method parameter order

class MyBuilder
For *collection builder* methods, should the span parameter be before or after any parameters for collection arguments?

Elements first would allow the arguments to be declared as optional.
```csharp
class MySetBuilder
{
public static MyCollection<T> Create<T>(ReadOnlySpan<T> items, int capacity) { ... }
public static MySet<T> Create<T>(ReadOnlySpan<T> items, IEqualityComparer<T> comparer = null) { ... }
}
```

For either of those cases, arguments are required:
Arguments first would allow the span to be a `params` parameter, to support calling directly in expanded form.
```csharp
MyCollection<object> x;
x = []; // error: no arguments
x = [with()]; // error: no 'capacity'
x = [with(capacity: 1)]; // ok
var s = MySetBuilder.Create(StringComparer.Ordinal, x, y, z);

class MySetBuilder
{
public static MySet<T> Create<T>(IEqualityComparer<T> comparer, params ReadOnlySpan<T> items) { ... }
}
```

### Construction overloads for *interface types*
### Arguments for *interface types*

Should the constructor candidates for `ICollection<T>` and `IList<T>` be the accessible constructors from `List<T>`, or specific signatures independent from `List<T>`, say `new()` and `new(int capacity)`?
Should arguments be supported for interface target types?

Similarly, should the constructor candidates for `IDictionary<TKey, TValue>` be the accessible constructors from `Dictionary<TKey, TValue>`, or specific signatures, say `new()`, `new(int capacity)`, `new(IEqualityComparer<K> comparer)`, and `new(int capacity, IEqualityComparer<K> comparer)`?
```csharp
ICollection<int> c = [with(capacity: 4)];
IReadOnlyDictionary<string, int> d = [with(comparer: StringComparer.Ordinal), ..values];
```

What about `IReadOnlyDictionary<TKey, TValue>` which may be implemented by a synthesized type?
If so, which method signatures are used when binding the arguments?

### Construction overloads for *collection builder* types
For `ICollection<T>` and `IList<T>` should we use the accessible constructors from `List<T>`, or specific signatures independent from `List<T>`, say `new()` and `new(int capacity)`?

Should the candidate methods for collection builder types include all overloads on the builder type with the required name, or should the candidates be limited as described [above](#create-method-candidates), for instance by requiring the first parameter is `ReadOnlySpan<T>`?
For `IDictionary<TKey, TValue>` should we use the accessible constructors from `Dictionary<TKey, TValue>`, or specific signatures, say `new()`, `new(int capacity)`, `new(IEqualityComparer<K> comparer)`, and `new(int capacity, IEqualityComparer<K> comparer)`?

What about `IReadOnlyDictionary<TKey, TValue>` which may be implemented by a synthesized type?

### Allow empty argument list for any target type

Expand Down
10 changes: 5 additions & 5 deletions proposals/csharp-12.0/collection-expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ An implicit *collection expression conversion* exists from a collection expressi
* A *span type*:
* `System.Span<T>`
* `System.ReadOnlySpan<T>`
in which cases the *element type* is `T`
In which case the *element type* is `T`
* A *type* with an appropriate *[create method](#create-methods)*, in which case the *element type* is the [*iteration type*](https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/statements.md#1295-the-foreach-statement) determined from a `GetEnumerator` instance method or enumerable interface, not from an extension method
* A *struct* or *class type* that implements `System.Collections.IEnumerable` where:
* The *type* has an *[applicable](https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/expressions.md#11642-applicable-function-member)* constructor that can be invoked with no arguments, and the constructor is accessible at the location of the collection expression.
Expand All @@ -125,11 +125,11 @@ An implicit *collection expression conversion* exists from a collection expressi
* `System.Collections.Generic.IReadOnlyList<T>`
* `System.Collections.Generic.ICollection<T>`
* `System.Collections.Generic.IList<T>`
in which cases the *element type* is `T`
In which case the *element type* is `T`

The implicit conversion exists if the type has an *element type* `U` where for each *element* `Eᵢ` in the collection expression:
* If `Eᵢ` is an *expression element*, there is an implicit conversion from `Eᵢ` to `U`.
* If `Eᵢ` is a *spread element* `..Sᵢ`, there is an implicit conversion from the *iteration type* of `Sᵢ` to `U`.
The implicit conversion exists if the type has an *element type* `T` where for each *element* `Eᵢ` in the collection expression:
* If `Eᵢ` is an *expression element*, there is an implicit conversion from `Eᵢ` to `T`.
* If `Eᵢ` is a *spread element* `..Sᵢ`, there is an implicit conversion from the *iteration type* of `Sᵢ` to `T`.

There is no *collection expression conversion* from a collection expression to a multi dimensional *array type*.

Expand Down
Loading