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 4 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
54 changes: 54 additions & 0 deletions proposals/dictionary-expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,60 @@ This concern already exists with *collection types*. For those types, the rule

## Open Questions

### Binding to indexer

For concrete dictionary types that do not use `CollectionBuilderAttribute`, where the compiler constructs the resulting instance using a constructor and repeated calls to an indexer, how should the compiler resolve the appropriate indexer for each element?

Options include:
1. For each element individually, use normal lookup rules and overload resolution to determine the resulting indexer based on the element expression (for an expression element) or type (for a spread or key-value pair element). *This corresponds to the binding behavior for `Add()` methods for non-dictionary collection expressions.*
2. Use the target type implementation of `IDictionary<K, V>.this[K] { get; set; }`.
3. Use the accessible indexer that matches the signature `V this[K] { get; set; }`.

### `dynamic` elements

Related to the previous question, how should the compiler bind to the indexer when the element expression has `dynamic` type, or when either the key or value is `dynamic`?

For reference, with non-dictionary targets, an element with `dynamic` type, the compiler binds to the applicable `Add(value)` method at runtime.

For dictionary targets, the situtation is more complicated because we have key-value pairs to consider. ...
Copy link
Contributor

Choose a reason for hiding this comment

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

... implies unfinished, but just in case:

Suggested change
For dictionary targets, the situtation is more complicated because we have key-value pairs to consider. ...
For dictionary targets, the situation is more complicated because we have key-value pairs to consider. ...

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks. I've since removed the question about dynamic elements and clarified the steps in the Construction section so I think the question no longer applies.


There is a related question of *key-value pair conversions* (see below). If we allow dynamic conversion of key or value independently, then we're essentially supporting key-value pair conversions at runtime. If that's the case, we'll probably want to key-value pair conversions at compile-time for non-`dynamic` cases.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
There is a related question of *key-value pair conversions* (see below). If we allow dynamic conversion of key or value independently, then we're essentially supporting key-value pair conversions at runtime. If that's the case, we'll probably want to key-value pair conversions at compile-time for non-`dynamic` cases.
There is a related question of *key-value pair conversions* (see below). If we allow dynamic conversion of key or value independently, then we're essentially supporting key-value pair conversions at runtime. If that's the case, we'll probably want to do key-value pair conversions at compile-time for non-`dynamic` cases.

```csharp
KeyValuePair<int, string> x = new(1, "one");
KeyValuePair<dynamic, string> y = new(2, "two");

Dictionary<long, string> d;
d = [x]; // compile-time key-value pair conversion from int to long?
d = [y]; // runtime conversion from dynamic to long?
```

Options include:
1. No support for dynamic conversion of the element expression, or of key or value.
2. Rewrite dynamic conversion from element expressions as explicit conversion to element typee.
3. Allow dynamic conversion of element expression, and of key or value.

### Key-value pair conversions

Should the compiler support a new [*key-value pair conversion*](#key-value-pair-conversions) within collection expressions to allow implicit conversions from an expression element of type `KeyValuePair<K1, V1>`, or a spread element with an iteration type of `KeyValuePair<K1, V1>` to the collection expression iteration type `KeyValuePair<K2, V2>`?

### Concrete type for `I{ReadOnly}Dictionary<K, V>`

What concrete type should be used for a dictionary expression with target type `IDictionary<K, V>`?

Options include:
1. Use `Dictionary<K, V>`, and state that as a requirement.
2. Use `Dictionary<K, V>` for now, and state the compiler is free to use any conforming implementation.
3. Synthesize an internal type and use that.

What concrete type should be used for `IReadOnlyDictionary<K, V>`?

Options include:
1. Use a BCL type such as `ReadOnlyDictionary<K, V>`, and state that as a requirement.
2. Use a BCL type such as `ReadOnlyDictionary<K, V>` for now, and state the compiler is free to use any conforming implementation.
3. Synthesize an internal type and use that.

We should consider aligning the decisions with the concrete types provided for collection expressions targeting mutable and immutable non-dictionary interfaces.

### Question: Types that support both collection and dictionary initialization

C# 12 supports collection types where the element type is some `KeyValuePair<,>`, where the type has an applicable `Add()` method that takes a single argument. Which approach should we use for initialization if the type also includes an indexer?
Expand Down