[Proposal]: Collection Expression Arguments #8886
-
Collection Expression Arguments.
SummaryIntroduce a way for users to create collection expressions (e.g. [a, b, c]) while also passing along arguments to the respective creation construction for that collection. An example strawman syntax for this would be: List values = [args(capacity: 32); a, b, c]; |
Beta Was this translation helpful? Give feedback.
Replies: 25 comments 252 replies
-
|
How about List<string> s = [a, b, c] where { capacity: 32 };
HashSet<Foo> s = [a, b, c] where { comparer: comp };? Asking GPT-4o which one is preferred, and this is the response (click "Details" to expand): DetailsBoth designs have merits, but I lean toward the second design for its readability and clarity. Here’s a comparison:1. First Design:Pros:
Cons:
2. Second Design:Pros:
Cons:
Preferred Design:Second Design ( |
Beta Was this translation helpful? Give feedback.
-
|
Haven't gave it a lot of thought but have you considered something like |
Beta Was this translation helpful? Give feedback.
-
|
I hope explicit parameter names can be omitted, so we can keep the expressions simple, like: and for this reason I don't really like the idea of wrapping arguments in |
Beta Was this translation helpful? Give feedback.
-
|
Are arguments to builders considered when the type is constructed not using a public constructor? |
Beta Was this translation helpful? Give feedback.
-
|
Has anybody considered You could maybe even use it to introduce a scope of sorts, to allow the same parameters to be used for nested collections: using(comparer: StringComparer.InvariantCultureIgnoreCase)
[ // the inner dictionaries use the same case-insensitive comparer as the outer one
"en": [ "one": 1, "two": 2, "three": 3, ... ],
"es": [ "uno": 1, "dos": 2, "tres": 3, ...],
"it": [ "uno": 1, "due": 2, "tre": 3, ...],
] |
Beta Was this translation helpful? Give feedback.
-
|
Note: any solution for arbitrary arguments needs to handle all the facilities we have for arguments today. That means supporting things like ref/out. Supporting named parameters. Supporting params. That's why a syntax that incorporates an argument list is ideal. As that was you get all of that 'for free'. |
Beta Was this translation helpful? Give feedback.
-
|
What if we allowed the collection expression syntax as an appendage to |
Beta Was this translation helpful? Give feedback.
-
|
These all look awful in my opinion. Just call the constructor if arguments need to be passed. If you must use the collection expression because it is the shiny new thing, then it could be one of the parameters of the constructor. I quite like collection expressions and have been using it in my code, but I don't think it needs to fit every scenario. |
Beta Was this translation helpful? Give feedback.
-
With all due respect, I don't find any of those problems compelling at all. The consistency of the proposal being referred to ( To me, options 1 and 2 in that document have the problem of not following existing C# syntax structure (more like python or something), not really being clear and looking like a normal method call (especially in something without syntax coloring) and possibly even conflicting with functions, local functions in particular, with the same name. This is a similar, but maybe less prevalent, problem to using Anyway, if the normal List<string> items = [1, 2, 3] : new (capacity: 32);
List<string> items = [1, 2, 3] : (capacity: 32); // same, just no `new` if that is considered redundantTo me, that aligns with constructor chaining, which actually seems pretty similar, at least conceptually, to what we are doing here: "Construct a thing with this stuff, but before that, it needs to be initialized with this other stuff." |
Beta Was this translation helpful? Give feedback.
-
|
What was the argument against extending object intializers with a kvp kind? new(comparer) {
"k": v
}I don't think allowing spread there would be too bad either, actually I would love to see spreads work with anonymous types: new {
e.Id,
.. e.User
}That comes in handy in efcore projections, for example. |
Beta Was this translation helpful? Give feedback.
-
That doesn't help an the existing collection expr types. It also flights against the goal of just having a unified syntax for all collection types.
This is already an anonymous type. These syntaxes are also not congruent with collection patterns. |
Beta Was this translation helpful? Give feedback.
-
|
I thought I had made another proposal, but it looks like I either didn't submit it or it was deleted (though I see no indication of that and I would think there would be). Hopefully the original really isn't there and I'm not just missing it. So, to attempt to recreate it: I think this discussion introduces and then leaves out the problem of object initializers with collection expressions. If the goal is to allow constructor arguments (apparently no constructor is ever called, but I don't understand that) with collection expressions, then is the idea that if one wants to use object initialization to set a property on the collection then they are just out of luck? So maybe if object initializers and collection expressions could be used together and constructor arguments (or whatever they actually are) could be passed into the object initialization then it might help in both cases. List<int> aList = new()
{
with(capacity: 32)
}I initially proposed the object initialization could come after the collection expression, but after seeing comments about how that is problematic, it could just be the other way around. List<int> aList = // new could be "gone" because we don't like it and it isn't really needed.
{
with(capacity: 32)
} [1, 2, 3];
// or on one line
List<int> anotherList = { with(capacity: 32) } [1, 2, 3];Or if we don't like things to be outside, then something like this: List<int> aList =
{
with(capacity: 32),
[1, 2, 3]
};And the compiler just knows that the bare collection expression inside the object initialization means the list is going to get initialized with that. Another possibility is to just get rid of the List<int> aList =
{
capacity: 32,
[1, 2, 3] // bare collection would use collection expression initialization (and apparently not a parameterless constructor...?)
};
List<int> anotherList =
{
collection: [1, 2, 3],
Capacity = 32
};
List<int> moreList =
{
[1, 2, 3], // bare collection would use collection expression initialization (and apparently not a parameterless constructor...?)
Capacity = 32
};
// if a constructor were being called then this would cause an error, but I've been told no constructors are called when creating the list, so maybe this would also be valid.
List<int> tooMuchList =
{
collection: [1, 2, 3],
capacity: 32
};Still another proposal would be that maybe collection constructors, if they are in fact actually being called, could have an implicit collection argument, similar to named parameters for List<int> aList = new(capacity: 32, [1, 2, 3]);And then you could just use object initialization below it like you can normally, but the compiler knows you also want the collection built from the expression. And if the constructor just takes a collection anyway, then the collection expression would just get passed into that so that the collection can do whatever it needs to do with it (which might also solve a problem with some of the other proposals where you could pass a collection in as one of these arguments but then also tell the compiler to build a collection for you). Both of these proposals would help with situations like in certain frameworks where the objects are containers, and therefore collections of something, but are also objects "first" that have properties other than those they inherit/implement from collections. An example is Maui and its views. In a situation like that you have to choose between object initialization and collection initialization, and then if your needs change you'll have to switch to the other. |
Beta Was this translation helpful? Give feedback.
-
|
Actually, speaking of setting properties on collections, it may be better to go with ControlList c = [args(capacity: 5); init { RegisterControlsWithContainer = true }; new Button(), new Label(), new TextBox()];
c.Add(new Button()); |
Beta Was this translation helpful? Give feedback.
-
|
Looking through this, it looks like we are coming up with a new way of specifying a constructor invocation that may be less succinct and more confusing. Here are a few thoughts: Something like this would be really nice when JSON deserialization is involved because it could conceivable also hint the deserializer into constructor args. Alternatively, the following seems nice to me: Or: |
Beta Was this translation helpful? Give feedback.
-
|
What about Maybe a generalized solution is better than special casing |
Beta Was this translation helpful? Give feedback.
-
|
Collection builder method parameter order. This could be solved by allowing named arguments after My proposal is to relax the rule that a |
Beta Was this translation helpful? Give feedback.
-
|
This feature has merged into roslyn main branch with: dotnet/roslyn#81924 |
Beta Was this translation helpful? Give feedback.
-
|
As mentioned before, it seems to be too late to do any changes to the syntax, but I really feel the need to ask the question, why it is that with every new C# feature (which is really good for what it provides) the "community" (= the people being active here) decides on a syntax that is sooooo way off of everything else we have in C#, instead of at least trying to keep the language a little more consistent. That makes it really hard for new developers to pick the language up, sometimes even read it - and if C# has one big problem, it's getting people onto our boat. For this particular case: I don't know why in general (not only for this case) we don't create new syntaxes by starting with the full syntax and eliminating unnecessary stuff (for example, as we did by making the type after "new" optional, if the type is defined on the left side of the assignment) until we're left with the absolutely necessary - but it would still be consistent with the existing syntax, and we would still know "where we came from". A step by step for this feature:
We are left with a short-hand syntax, that still makes it 100% clear, where we were coming from, like what the original is and what has been omitted throughtout the way. And all this without the need to introduce a new keyword or choose a syntax that is so against every existing syntax and only making it less clear for what is asked from the developer. EDIT 1: EDIT 2: |
Beta Was this translation helpful? Give feedback.
-
|
Man I really don't like that syntax, given the I mean... I don't have an alternative syntax suggestion right now, in the fixed cases we can just stick to: For now, I know it won't allow expanding, e.g. Still though... The suggested syntax here seems rushed at this point and should cook for far more time if you ask me. |
Beta Was this translation helpful? Give feedback.
-
We looked, and found no one doing this. Which is unsurprising. A method named 'with' is just not a thing that arises in .net. We also explained in the design spec the problem with semicolon. It's extremely hard to notice. And having semantics change by a postfix punctuation, just makes understanding a much harder task. Finally, it's a very poor ergonomic experience if you have a collection with no elements in it. |
Beta Was this translation helpful? Give feedback.
-
|
I think some others have replied in the time it took me to write this, so hopefully isn't redundant.
I share your concern with language consistency, but a couple of things: The community as in active people don't make these decisions. The team/collaborators that designs the language makes these decisions. And as you can see by the discussion above, they had already considered all or most of the things that the rest of the community brought up and had already more or less settled on the path they were taking. The only reason we get these discussions is in large part for transparency and to provide for the rare instance of where somebody might propose something or raise a concern that wasn't considered. Point being, it's hard to grasp/accept that this just isn't a democratic process. I know I have trouble with that myself. And as romantic as the idea is, it just isn't realistic. That realistically just wouldn't work. And frankly, in the majority of the rare times where it did, the same concern would still be valid because the majority could just decide on the same kind of inconsistency.
I struggle(d) with this too, but it just isn't always feasible or even the goal. None of the options you listed are really "collection expressions" except maybe the last one, but it is pretty vague/ambiguous, both with existing syntax and also just in terms of intent. The others are just essentially object initialization statements that have been abbreviated. The inconsistency or difference here from existing syntax "is a feature, not a bug". It is the desirable result. Using collection expressions as an example, think about why we would have them if we already have and can just use But what if you don't want to do that? Or maybe more accurately, what if the compiler doesn't want to do that? In other words, that might not be what is happening conceptually or even actually. Obviously a constructor is probably getting called somewhere at some point, because that's what has to happen to create an object. But if the compiler can make intelligent decisions on how to actually produce the collection, then it's not really a good idea to have a developer express that with code that seems to explicitly invoke a constructor. So these aren't just a shorter way to initialize an object that is a collection. They are a way to abstract away something like collection "allocation" from object creation so that the code isn't locked into/dependent on a specific implementation of a collection and a specific algorithm for producing it. For example, I used the word "allocation" above, but there might not need to be any allocation caused by the statement, like in the case of something like an array pool where the allocation may have happened "long ago". Or, maybe, multiple allocations are happening for whatever reason, so that Further examples are just all of the code, methods in particular, used in the production, like static calls, member calls, extension method calls, etc. I.e. your code says
|
Beta Was this translation helpful? Give feedback.
-
|
I think this feature is doing it in reverse, instead of forcing collection expressions everywhere, just allow spread in object initializers instead like this: List<int> v = new(10) { ...list1, ...list2 }This would also allow you to initialize additional other properties using init properties for example: MyType v = new() {
MyProp: "Foo",
MyOtherProp: "Bar",
...list1,
...list2
};Especially for dictionaries this would be incredibly nice to do it like this: Which would translate to: Or for arrays: int[] array = new(){ ...list1, ...list2, [2] = 5, [^1] = 9 }Which would become sth like: |
Beta Was this translation helpful? Give feedback.
-
|
Just some idea maybe it worse but I hope this new feature will be better. |
Beta Was this translation helpful? Give feedback.
-
|
Version 4: Version 5: Version 6: |
Beta Was this translation helpful? Give feedback.
-
Thank you for the discussion and feedback!Before proposing alternative syntax, please read the motivation sections of the specification at: https://github.com/dotnet/csharplang/blob/main/proposals/collection-expression-arguments.md And please also read through the existing discussions. Many suggestions being posted have already been covered, requiring retreading and restatements of things already clearly in the existing record. The motivation sections explain the design principles, priorities, and careful thinking that went into the syntax. Many of the alternative suggestions being proposed run afoul of the core design constraints that drove the decisions made by the LDM. Key Design PrinciplesThe syntax chosen (
It's not acceptable for a syntax to be better for one of these cases at the deep expense of the others. Any viable alternative would need to demonstrate superiority (or at least parity) across all these axes, not just optimize for a single scenario. For dictionaries specifically: they work with collection expressions today but will be significantly better when dictionary expressions land in a future release. The The specification details why specific choices were made around:
Collection Expressions: The FoundationThe LDM is not relitigating collection expressions themselves. Collection expressions shipped in C# 12 and represent the path forward for collections in the language. The original specification and design thinking can be found at: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/collection-expressions.md That proposal reflects the commitment to a readable, minimal, consistent syntax that unified over half a dozen different collection construction forms that existed before. Collection Expression Arguments is a minor augmentation for a very narrow purpose that most codebases will never encounter, and when they do, it should have a very small syntactic price. This feature is not upending collection expressions or taking things in a radically different direction. The LDM is all in on collection expressions, and everything done in this space will fit well with the The Vision Going ForwardDictionary expressions (or more precisely, key-value pair elements) are coming in a near future release: https://github.com/dotnet/csharplang/blob/main/proposals/dictionary-expressions.md Both that proposal and Collection Expression Arguments demonstrate a consistent vision: ensuring the collection boundary is always clear (within the These principles will continue to apply to all future work. For example, conditionally added elements (e.g., Moving ForwardFeedback on Collection Expression Arguments is welcome, but please:
The LDM has had extensive discussions and design sessions, and there is an implementation that addresses the core motivations well. For someone who wants to propose changes, it would need to be a substantive proposal, not already mentioned in the existing discussions, that addresses all the core motivations outlined in the specification. This post is being marked as the answer to help provide clarity on the design direction and what informed feedback looks like. |
Beta Was this translation helpful? Give feedback.
Thank you for the discussion and feedback!
Before proposing alternative syntax, please read the motivation sections of the specification at:
https://github.com/dotnet/csharplang/blob/main/proposals/collection-expression-arguments.md
And please also read through the existing discussions. Many suggestions being posted have already been covered, requiring retreading and restatements of things already clearly in the existing record. The motivation sections explain the design principles, priorities, and careful thinking that went into the syntax. Many of the alternative suggestions being proposed run afoul of the core design constraints that drove the decisions made by the LDM.
Key Design Prin…