Back to Csharplang

C# Language Design Meeting for May 7th, 2025

meetings/2025/LDM-2025-05-07.md

latest6.5 KB
Original Source

C# Language Design Meeting for May 7th, 2025

Agenda

Quote of the Day

  • "with(out var x) sounds like we're leaving var x behind"

Discussion

Collection Expression Arguments

Champion issue: https://github.com/dotnet/csharplang/issues/8887
Spec: https://github.com/dotnet/csharplang/blob/0eddbeca5dc6edfbf91f915ecea3379b8e337cc7/proposals/collection-expression-arguments.md

Interface target type

Spec section: https://github.com/dotnet/csharplang/blob/0eddbeca5dc6edfbf91f915ecea3379b8e337cc7/proposals/collection-expression-arguments.md#interface-target-type

We started today by looking at the proposed set of constructors for interface types to ratify the list. The main bit of feedback here was on the readonly types; the proposal does not accept capacity for any of these types, and we briefly explored whether it might be useful to do so. We don't know of any examples where this would be useful from a runtime perspective, but we did want to consider whether it would allow lower code churn when switching from a mutable interface to a readonly interface. Ultimately, we don't think that this is useful, and we approve the constructors as proposed.

Conclusion

Interface constructor list approved as proposed.

Constructor binding behavior

Spec section: https://github.com/dotnet/csharplang/blob/0eddbeca5dc6edfbf91f915ecea3379b8e337cc7/proposals/collection-expression-arguments.md#constructors

Next, we looked at the rules for binding constructors and create methods in general. A particular wart that has come up as part of this is that we may end up skipping optional parameters as part of a signature without any user-specificity; further, users may decide that they need to mark parameters as optional even if they normally wouldn't do so, so that they can mark the collection as optional in their Create methods. For example, a user might have a method like this:

cs
public static MyCollection<T> Create<T>(IComparer<T> comparer = null, ReadOnlySpan<T> elements = default);

Here, users are forced to mark elements as optional so that they can mark comparer as optional, even though they might not want to do so. Namely, this design might force API designers into non-standard approaches so that their methods compile. There are also questions around Create methods that take multiple ROS elements: which should be the one we use for the elements? While there were good reasons to try and prefer the ROS as the last element in the argument list, we do also think that moving it to be the first element would be beneficial for these questions, so we want the working group to go and take a look at this question again.

We also looked at out parameters and params parameters. We're ok with the meaning of these being the same in collection expression arguments. as currently defined, params is mainly useful in constructors, not in Create methods. We will not allow params parameters to be split across collection expression arguments and the collection expression itself.

Conclusion

out and params are approved. The working group will revisit the rules around collection builder signature matching, and whether we can put the ROS first.

Syntax

Finally, we got to a long-simmering question: what is the real syntax we want to use for collection expression arguments. with was intended as a way to move forward with the various semantics, without needing to block implementation. We have always intended to revisit it, and now we're doing so. We threw a lot of syntaxes into a block to give inspiration:

cs
// Trailing arguments
[a, b, c] with (comparer)

// Leading arguments
new(comparer, ......) [...]
with(comparer, ......) [...]
init(comparer, ......) [...]
args(comparer, ......) [...]

// Direct parentheses - conflicts with tuples in the wild
[ (a, b, c), 1, 2, 4]

// new as the keyword - conflicts with target-typed new in the wild
[  new(a, c, c), 1, 2, 4]

// Sigils - no conflict, but is it confusing?
[ @(a, b, c), 1, 2, 4]

// Already used for record mutation
[ with(a, b, c), 1, 2, 4]

// Already used for property setters
[ init(a, b, c), 1, 2, 4]

// Already used for main method arguments in top-level statements
[ args(a, b, c), 1, 2, 4]

// Previous examples, using semi-colons instead of commas
[ (a, b, c); 1, 2, 4]
[ with(a, b, c); 1, 2, 4]
[ init(a, b, c); 1, 2, 4]
[ args(a, b, c); 1, 2, 4]

To make it easier to start whittling down our options, we created some broad categories. First up, we looked at the location of the arguments; before the expression, inside the expression, or after the expression? We don't like after, due to order of operations, as it implies that the collection contents have to be evaluated entirely before the arguments can be evaluated. Before is better, but we don't like the mental stutter that is involved: you start reading an argument list, then see the [ and realize what you read previously were the arguments for a collection expression, not a method call. Inside was the clear winner in our eyes.

Next, we looked using a sigil, or a keyword. We very unanimously felt that a sigil was not C#-y, and too cryptic. Therefore, we will use a keyword.

Next, we looked at the separation sigil. Our two leading contenders are , and ;. : was eliminated early because it is used as the dictionary expression key:value separator. Between ; and ,, we lean slightly towards the ,. The ; is a harder separation, but we also don't have any usage of ; inside an expression today, besides statements inside block-bodied lambdas, and we're not sure that this is a good location to change that. , does come with less visual separation, but we're very used to using ,s inside expressions, which is a positive. We're leaning in the , direction, and will come back next time to settle for certain.

Finally, in the waning minutes of the meeting, we looked at the various keywords that were proposed. The first choice of most LDT members was either with or args, and init as a distant 3rd. We'll come back next time as well to settle between with and args.

Conclusion

The syntax will definitely be inside the collection expression at the front, and use a keyword of some kind. We've narrowed separator options to , vs ;, and keyword options to with vs args. We will settle those choices next time.