[Proposal]: Collection expressions (VS 17.7, .NET 8) #8651
Replies: 494 comments 1 reply
-
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: |
Beta Was this translation helpful? Give feedback.
-
@YairHalberstadt All good points. Thank you :) |
Beta Was this translation helpful? Give feedback.
-
Linking to https://github.com/bartdesmet/csharplang/.../proposals/params-builders.md on the builder pattern. |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
@orthoxerox Once you're able to target-type literals of unknown length to |
Beta Was this translation helpful? Give feedback.
-
Can you explain this to me, please? Wouldn't whatever type you choose as a natural type implement |
Beta Was this translation helpful? Give feedback.
-
@erikhermansson79 Besides |
Beta Was this translation helpful? Give feedback.
-
I agree, enumerating the collection literal just by changing the type of the variable it's assigned to sounds confusing. |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
It will look like: |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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"),
]; |
Beta Was this translation helpful? Give feedback.
-
I'm pretty firmly against approaches that either:
|
Beta Was this translation helpful? Give feedback.
-
Why not use curly braces like we have them for arrays already? So you could write: List<int> myInts = {1, 2, 3, 4}; |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
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"] |
Beta Was this translation helpful? Give feedback.
-
Because it's not yet available :) PS. I decided to table this for now. The problem is that if I add a |
Beta Was this translation helpful? Give feedback.
-
Idea: flexible natural type: var a = [1, 2, 3]; // int[]
var b = [1, 2, 3, ..]; // List<int> |
Beta Was this translation helpful? Give feedback.
-
If you want a definite type, just use that type instead of |
Beta Was this translation helpful? Give feedback.
-
There are other situations, e.g., where you have to pass an argument to a parameter of type |
Beta Was this translation helpful? Give feedback.
-
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]" |
Beta Was this translation helpful? Give feedback.
-
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? |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
@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: |
Beta Was this translation helpful? Give feedback.
-
Is natural types of Collection expressions is still developing? |
Beta Was this translation helpful? Give feedback.
-
@CyrusNajmabadi Can you add to working set? https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md |
Beta Was this translation helpful? Give feedback.
-
I don't think it's in the working set currently. |
Beta Was this translation helpful? Give feedback.
-
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
Beta Was this translation helpful? Give feedback.
All reactions