-
Notifications
You must be signed in to change notification settings - Fork 1k
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
[Proposal]: Collection expressions (VS 17.7, .NET 8) #5354
Comments
Overall looks good! Just a couple of points.
This only works because at the moment it happens to be only a compiler can call init methods, if you don't use them yourself in one of your init properties. However it doesn't seem like the sort of thing we'd want to rely on not changing in the future. For example we might allow calling init methods in the object initializer, at which point this would no longer be safe: int[] ints = [1,2,3];
var immutArray = new{ Init(ints) };
int[0] = 5;
It seems like this is not the most efficient solution - instead we would want to effectively inline the element, never materializing them into an array in the first place, and instead storing all the sub_elements on the stack.
We could only use those methods when the type of the spread_element exactly matches the parameter type of these methods, meaning we would not be causing virtual dispatch.
This is an extremely common use case - ToArray is the most commonly used Linq method. It would be unfortunate if the one syntax to rule them all required you to do: |
@YairHalberstadt All good points. Thank you :) |
Linking to https://github.com/bartdesmet/csharplang/.../proposals/params-builders.md on the builder pattern. |
What if collection literals with an unknown length have a different natural type from those with a known length? The latter should likely be a |
@orthoxerox Once you're able to target-type literals of unknown length to |
Can you explain this to me, please? Wouldn't whatever type you choose as a natural type implement |
@erikhermansson79 Besides |
I agree, enumerating the collection literal just by changing the type of the variable it's assigned to sounds confusing. |
I think proposals benefit from having a section of various examples. Most people can't look at grammar rules and get a good feel for what the syntax will actually look like. Separately, I will slightly object to this statement,
TS/JS just has a different syntax for initializing arrays. I don't know that its really much more convenient than |
It will look like: |
The statement was that the presence of this literal form has not proven to itself be problematic for these languages. Not that the literal is sufficient for all usages in those languages. In other words, these literals are not in "the bad parts". Nor are there contingents if users recommending people not use these. |
Here's an immediate example I can think of: var processArguments =
[
"pack",
"-o", outputPath,
..(configuration is not null ? (["-c", configuration]) : []),
"/bl:" + Path.Join(artifactsDir, @"logs\pack.log"),
]; |
I'm pretty firmly against approaches that either:
|
Why not use curly braces like we have them for arrays already? So you could write: List<int> myInts = {1, 2, 3, 4}; |
This is referred to in the motivation section, but i'll give a little more information. Effectively I also cover the move from ALso note that |
Not sure if you're being cheeky or what, but that is not a substitute for an example section. Some suggestions:
|
This is covered in the spec outline and the things to discuss. I don't want examples that imply things are not possible when no decision has been made on it. |
This is covered in the spec as well. But I'll call out explicitly. |
Regarding immutability, here's an interesting thought: Introduce "let" as a way of declaring an "immutable" variable. let x=1; Creates a readonly integer. let x = [1,2,3]; Makes an immutable list var x = [1,2,3]; Makes a regular list. |
The language has no concept of immutability (and i doubt it is likely to get one any time soon). I does have a concept of 'readonly' (and 'let' is already somethin we're considering there). So it likely wouldn't be a good fit as there would be inconsistency there. Interesting idea though! |
It would have to work for |
|
Regarding the implicit type ("natural type"), I would say the array has 2 pros and a con. One pro is that it's the only "built-in" type - meaning, it's a type which is part of the language. Second - it's the most efficient collection (correct me if I'm mistaken) - all other collection rely on arrays behind the scenes. The con - arrays already have a simple enough PS. Another reason to go with PPS. On second thought, perhaps we can just say no (for now) to implicit type and be done with it. |
Other cons are that it heap allocates and that it is fixed length. |
Isn't it possible to make |
The |
Oh my, that was such a brain fart - of course the PS. Internally, this type can either 1) use |
You are certainly welcome to create a new type. That's a core part of this proposal that the proposal would work with any type that followed certain shapes. Now, if the BCL would add a type like this? My guess would be no. Such a type would likely be highly problematic. For example, if you passed this ValueList to someone else, and they captured it, then they would only see portions of your mutations. For exaple, if you added items, they would not see it (since their length would not update). HOwever, if you mutated items prior to that point, they would see it (sinced they shared the same array) unless you (or them) also caused a reallocation (where you both would have distinct arrays). Also, if one added an element, and then the other added, the other would overwrit the first. etc. etc. It would be enormously confusing. |
var json = JToken.Parse("""
{
"First": "John",
"Last": "Smith",
"Phones": [ "555-555-5555" ]
}
"""); |
@TahirAhmadov Raw string literals don't use |
Corrected :) |
@TahirAhmadov we already do: Note the classification and errors. |
I just ran across this scenario again, imagine this: class SomeControl
{
public IList<string> StringsOptions { get; } = new() { "a" }; // we want to offer a default list
}
var sc = new SomeControl
{
StringsOptions = { "b", "c" }, // possible to add to the default list in init block, but not to replace it
}; Ideally, below would be possible: class SomeControl
{
public IList<string> StringsOptions { get; } = ["a"]; // we want to offer a default list
}
var sc = new SomeControl
{
StringsOptions += ["b", "c"], // possible to add to the default list in init block,
};
var sc2 = new SomeControl
{
StringsOptions = ["d", "e", "f"], // and to replace the list
}; The assignment in |
Why isn't the collection initializer syntax sufficient to add to the existing collection in a case like this? I could see a desire to want to consistently use collection expressions, and/or allowing for collection expressions to be more efficient than collection initializers by supporting, say, |
@HaloFour those are exactly my reasons for requesting this:
string[] options = ["x", y"];
var sc = new SomeControl
{
SomeEvent += this.sc_SomeEvent,
StringsOptions += ["b", "c", .. options], // possible to add to the default list in init block,
}; |
@TahirAhmadov would you mind linking to the actual proposal for this one? First time I'm hearing about it. I assume it would behave like I used to create my own custom string[] options = ["x", y"];
var sc = new SomeControl
{
StringsOptions = { ["b", "c", .. options] } // This works today
}; |
@julealgon string[] options = ["x", y"];
var sc = new SomeControl();
sc.SomeEvent += this.sc_SomeEvent;
sc.StringsOptions.Add("b");
sc.StringsOptions.Add("c");
sc.StringsOptions.AddRange(options); And this avoids allocating a buffer like with your workaround - which is not a huge deal, good for you that you found a workaround for your situation - but still. |
@julealgon thanks for the extension method idea. var sc = new SomeControl
{
StringsOptions = { "b", "c", options } // This works today
}; The problem with creating such an extension method is the potential for hidden bugs - there was a good reason why they didn't overload
If anybody can provide input on which of these approaches they prefer, I would greatly appreciate it. I'm leaning towards option 2. |
Oh yeah, that's what I actually meant to write 😆 Sorry for the confusion.
I don't disagree. The reason I did this on our side was due to how clean it would look with our custom mapper classes. We were not using automapper in that project, so we'd have stuff like this: public class MyClass
{
public string Prop1 { get; set; }
public ICollection<MySubClass> Collection1 { get; } = new List<MySubClass();
public ICollection<MyOtherSubClass> Collection2 { get; } = new List<MyOtherSubClass();
}
public class MyClassMapper : IMapper<MyClass, MyClassDto>
{
private readonly IMapper<MySubClassMapper, MySubClassDto> subClassMapper;
private readonly IMapper<MyOtherSubClassMapper, MyOtherSubClassDto> otherSubClassMapper;
public MyClassMapper(
IMapper<MySubClassMapper, MySubClassDto> subClassMapper,
IMapper<MyOtherSubClassMapper, MyOtherSubClassDto> otherSubClassMapper)
{
this.subClassMapper = subClassMapper;
this.otherSubClassMapper = otherSubClassMapper;
}
public MyClassDto Map(MyClass value) => new()
{
Prop1 = value.Prop1,
Collection1 = { value.Collection1.Select(subClassMapper.Map) },
Collection2 = { value.Collection2.Select(otherSubClassMapper.Map) },
};
} Since the collection properties are read-only (as is the recommended approach for collections), this allows us to keep a single, unified object initializer. This pattern would repeat dozens and dozens of times as we had many such manual mappers throughout the system, so the added benefit of the extension was huge. In this case of course it would be on public static class EnumerableExtensions
{
public static void Add<T>(this ICollection<T> target, IEnumerable<T> values)
{
foreach (var value in values)
{
target.Add(value);
}
}
}
Why not follow with your previous proposal though? var sc = new SomeControl
{
StringsOptions += ["b", "c", .. options]
}; Having a var sc = new SomeControl();
sc.StringsOptions += ["b", "c", .. options]; Now that operators are possible on interfaces, both var sc = new SomeControl();
sc.StringsOptions += ["b", "c"] + options; And then also allow "adding single elements", which would make this possible: var sc = new SomeControl();
sc.StringsOptions += ["b", "c"] + options + "d"; // same as ["b", "c", .. options, "d"] |
Because it's not yet available :) PS. I decided to table this for now. The problem is that if I add a |
Idea: flexible natural type: var a = [1, 2, 3]; // int[]
var b = [1, 2, 3, ..]; // List<int> |
If you want a definite type, just use that type instead of |
There are other situations, e.g., where you have to pass an argument to a parameter of type |
I see your point, but List<int> b = [1, 2, 3];
List<int> b = [1, 2, 3, ..]; mean the same? Probably, yes? I think the idea of a suffix has been floated already, and I can see it making sense in the scenario described above (where the target type can't be inferred). For example static void DoStuff(object myObj) => Console.WriteLine(myObj);
DoStuff([1, 2, 3]A); // "System.Int32[]"
DoStuff([1, 2, 3]L); // "System.Collections.Generic.List`1[System.Int32]" |
Type suffixes for collection expressions seem interesting. If they were implemented I'd like to see them support tuples as well (or be overloaded by item count)... They're a natural extension to numeric literal suffixes (1.0f, 123.4m) and would be useful for creating types that feel like literals (e.g. a Vector3 I'd like to see them exposed via attributes as follows: namespace System.Collections.Generic;
public class List<T> {
// Option 1
[Suffix("L")]
public List(...) {}
// Option 2
[Suffix("L")]
public static List<T> FromCollectionExpression(...) {}
}
public static class ListHelper {
// Option 3
[Suffix("L")]
public static List<T> FromCollectionExpression(...) {}
} Using the literal would require As an alternative, the converter could be a class: namespace System.Collections.Generics;
// Option 4
[Suffix("L")]
public static class ListSuffix {
public static List<T> Convert(...) {}
} And used the same way: using System.Collections.Generic;
[1, 2, 3]L With the benefit of supporting using aliases?: using zz = System.Collections.Generic.ListSuffix;
[1, 2, 3]zz The largest drawback, IMO, is that supporting empty-collections might still be ugly with this syntax & you potentially end up moving typing from the collection-creation to the item add in many cases... What's the empty hashset or empty dictionary? |
Personally, I'd prefer to see special overloads of To{CollectionType} that can be used on collection expressions and have the same effect as a cast/target typing. |
|
Regardless of the extension method name, I don't think there is an answer to the question of what to do with spreads, are they all pushed to the stack and a The suffixes are a little too narrow-scoped. I think the best solution to this would be generic arguments inference: List<> list = [1,2,3];
ImmutableArray<> ia = [1,2,3];
Dictionary<,> dict = [1:"a", 2:"b", 3:"c"]; And it can be useful for other scenarios, too. WRT |
@OJacot-Descombes Actually, it's not that drastic. You can always specify a target type for any expression, including collection expressions, using explicit cast syntax: |
Is natural types of Collection expressions is still developing? |
Yes |
@CyrusNajmabadi Can you add to working set? https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md |
I don't think it's in the working set currently. |
Collection expressions
Many thanks to those who helped with this proposal. Esp. @jnm2!
Summary
Collection expressions introduce a new terse syntax,
[e1, e2, e3, etc]
, to create common collection values. Inlining other collections into these values is possible using a spread operator..
like so:[e1, ..c2, e2, ..c2]
. A[k1: v1, ..d1]
form is also supported for creating dictionaries.Several collection-like types can be created without requiring external BCL support. These types are:
int[]
.Span<T>
andReadOnlySpan<T>
.List<T>
andDictionary<TKey, TValue>
.Further support is present for collection-like types not covered under the above, such as
ImmutableArray<T>
, through a new API pattern that can be adopted directly on the type itself or through extension methods.Motivation
Collection-like values are hugely present in programming, algorithms, and especially in the C#/.NET ecosystem. Nearly all programs will utilize these values to store data and send or receive data from other components. Currently, almost all C# programs must use many different and unfortunately verbose approaches to create instances of such values. Some approaches also have performance drawbacks. Here are some common examples:
Arrays, which require either
new Type[]
ornew[]
before the{ ... }
values.Spans, which may use
stackalloc
and other cumbersome constructs.Collection initializers, which require syntax like
new List<T>
(lacking inference of a possibly verboseT
) prior to their values, and which can cause multiple reallocations of memory because they use N.Add
invocations without supplying an initial capacity.Immutable collections, which require syntax like
ImmutableArray.Create(...)
to initialize the values, and which can cause intermediary allocations and data copying. More efficient construction forms (likeImmutableArray.CreateBuilder
) are unweildy and still produce unavoidable garbage.Looking at the surrounding ecosystem, we also find examples everywhere of list creation being more convenient and pleasant to use. TypeScript, Dart, Swift, Elm, Python, and more opt for a succinct syntax for this purpose, with widespread usage, and to great effect. Cursory investigations have revealed no substantive problems arising in those ecosystems with having these literals built in.
C# has also added list patterns in C# 10. This pattern allows matching and deconstruction of list-like values using a clean and intuitive syntax. However, unlike almost all other pattern constructs, this matching/deconstruction syntax lacks the corresponding construction syntax.
Getting the best performance for constructing each collection type can be tricky. Simple solutions often waste both CPU and memory. Having a literal form allows for maximum flexibility from the compiler implementation to optimize the literal to produce at least as good a result as a user could provide, but with simple code. Very often the compiler will be able to do better, and the specification aims to allow the implementation large amounts of leeway in terms of implementation strategy to ensure this.
An inclusive solution is needed for C#. It should meet the vast majority of casse for customers in terms of the collection-like types and values they already have. It should also feel natural in the language and mirror the work done in pattern matching.
This leads to a natural conclusion that the syntax should be like
[e1, e2, e3, e-etc]
or[e1, ..c2, e2]
, which correspond to the pattern equivalents of[p1, p2, p3, p-etc]
and[p1, ..p2, p3]
.A form for dictionary-like collections is also supported where the elements of the literal are written as
k: v
like[k1: v1, ..d1]
. A future pattern form that has a corresponding syntax (likex is [k1: var v1]
) would be desirable.Detailed design
The content of the proposal has moved to proposals/collection-expressions.md. Further updates to the proposal should be made there.
Design meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-11-01.md#collection-literals
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-09.md#ambiguity-of--in-collection-expressions
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-28.md#collection-literals
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-04-03.md#collection-literals
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-04-26.md#collection-literals
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#collection-literal-natural-type
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-31.md#collection-literals
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-06-05.md#collection-literals
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-06-19.md#collection-literals
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-07-12.md#collection-literals
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-18.md#collection-expression-questions
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-20.md#collection-expressions
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#defining-well-defined-behavior-for-collection-expression-types
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-27.md#collection-expressions
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-02.md#collection-expressions
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-11.md#collection-expressions
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-11-15.md#nullability-analysis-of-collection-expressions
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-01-10.md
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-02-26.md#collection-expressions
Working group meetings
https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-06.md
https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-14.md
https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-21.md
https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-04-05.md
https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-04-28.md
https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-05-26.md
https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-06-12.md
https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-06-26.md
https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-08-03.md
https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-08-10.md
The text was updated successfully, but these errors were encountered: