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

[Proposal]: Collection expressions #5354

Open
3 of 4 tasks
Tracked by #829
CyrusNajmabadi opened this issue Oct 27, 2021 · 490 comments
Open
3 of 4 tasks
Tracked by #829

[Proposal]: Collection expressions #5354

CyrusNajmabadi opened this issue Oct 27, 2021 · 490 comments

Comments

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Oct 27, 2021

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:

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[] or new[] 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 verbose T) 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 (like ImmutableArray.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 (like x 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

@YairHalberstadt
Copy link
Contributor

YairHalberstadt commented Oct 28, 2021

Overall looks good! Just a couple of points.

This facility thus prevents general use of such a marked method outside of known safe compiler scopes where the instance value being constructed cannot be observed until complete.

In the context of collection literals, the presence of these methods would allow types to trust that data passed into them cannot be mutated outside of them, and that they are being passed ownership of it. This would negate any need to copy data that would normally be assumed to be in an untrusted location.

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;

To resolve this, we could say that when evaluating a literal's spread_element expression, that there was an implicit target type equivalent to the target type of the literal itself. So, in the above, that would rewritten as:

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.

Should we expand on collection initializers to look for the very common AddRange method? It could be used by the underlying constructed type to perform adding of spread elements potentially more efficiently. We might also want to look for things like .CopyTo as well. There may be drawbacks here as those methods might end up causing excess allocations/dispatches versus directly enumerating in the translated code.

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.

Can an unknown length literal create a collection type that needs a known length, like an array, span, or Init(array/span) collection? This would be harder to do efficiently, but it might be possible through clever use of pooled arrays and/or builders.

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: ((List<int>)[1, .. subspread, 2]).ToArray().

@CyrusNajmabadi
Copy link
Member Author

@YairHalberstadt All good points. Thank you :)

@alrz
Copy link
Contributor

alrz commented Oct 28, 2021

Linking to https://github.com/bartdesmet/csharplang/.../proposals/params-builders.md on the builder pattern.

@orthoxerox
Copy link

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 List<T>, but the latter is an IEnumerable<T> with an unspeakable implementation? That is, [..c1, ..c2] where either collection has an unknown length is semantically equivalent to Enumerable.Concat(c1, c2).

@jnm2
Copy link
Contributor

jnm2 commented Oct 28, 2021

@orthoxerox Once you're able to target-type literals of unknown length to T[] and ImmutableArray<T>, which I very much hope is made possible and which is not solved by giving such literals a different natural type, then will it still be advantageous to give them a different natural type?

@erikhermansson79
Copy link

A downside to using an array would be if a natural type is added for collection literals and that natural type is not T[]. There would be a potentially surprising difference when refactoring between var x = [1, 2, 3]; and IEnumerable x = [1, 2, 3];.

Can you explain this to me, please? Wouldn't whatever type you choose as a natural type implement IEnumerable<T>? What would the difference be?

@jnm2
Copy link
Contributor

jnm2 commented Oct 29, 2021

@erikhermansson79 Besides is type checks and pattern matching behaving differently, as well as GetType, and overload resolution if you use dynamic, there's also this for example: casting to IList and reading IsFixedSize will be observably different. If you place this in a public IEnumerable<int> property and databind using a UI framework, the difference in behavior could amount to a user-facing regression. And so on.

@orthoxerox
Copy link

@orthoxerox Once you're able to target-type literals of unknown length to T[] and ImmutableArray<T>, which I very much hope is made possible and which is not solved by giving such literals a different natural type, then will it still be advantageous to give them a different natural type?

I agree, enumerating the collection literal just by changing the type of the variable it's assigned to sounds confusing.

@MgSam
Copy link

MgSam commented Oct 29, 2021

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,

Looking at the surrounding ecosystem, we also find examples everywhere of list creation being more convenient and pleasant to use. TypeScript...

TS/JS just has a different syntax for initializing arrays. I don't know that its really much more convenient than new[] { ... }. It certainly doesn't have a generalized collection initialization syntax whatsoever. If you use a collection type other than Array in JS you are SOL.

@CyrusNajmabadi
Copy link
Member Author

I think proposals benefit from having a section of various examples

It will look like: [a, b, .. c, d]

@CyrusNajmabadi
Copy link
Member Author

Separately, I will slightly object to this statement,

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.

@jnm2
Copy link
Contributor

jnm2 commented Oct 29, 2021

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"),
];

@CyrusNajmabadi
Copy link
Member Author

I'm pretty firmly against approaches that either:

  1. Involve lazy evaluation. For that, use linq.
  2. Change the natural type of the expression based on the values within. I think that will just be too confusing and difficult to reason about.

@bernd5
Copy link
Contributor

bernd5 commented Oct 29, 2021

Why not use curly braces like we have them for arrays already?

So you could write:

List<int> myInts = {1, 2, 3, 4};

@CyrusNajmabadi
Copy link
Member Author

CyrusNajmabadi commented Oct 29, 2021

Why not use curly braces like we have them for arrays already?

This is referred to in the motivation section, but i'll give a little more information. Effectively {s have issues for us in being ambiguous in some situations if they mean lists or properties. So we've moved to [ with list patterns to have no ambiguity and to give a nice and clean syntax for list-like things. When we designed that we were aware that we likely wanted a 'literal correspondence' as well, which this proposal is.

I also cover the move from { to [ in the drawbacks section, albeit with the idea that this would allow us to have uniformity everywhere moving foward.

ALso note that { as an expression-form is potentially highly destabalizing for future work. For example, if we ever want a block expression then we couldnt' have that use { if { is also for lists. We sketched out and very much liked [...] for list patterns and these literals (both for looks, and for sidestepping a lot of issues), so we made the decision back in the pattern design to go this direction. We simply ordered it as "list pattern in c#10" and now hopefully "list literal in c#11".

@MgSam
Copy link

MgSam commented Oct 29, 2021

I think proposals benefit from having a section of various examples

It will look like: [a, b, .. c, d]

Not sure if you're being cheeky or what, but that is not a substitute for an example section. Some suggestions:

  • Before and afters of the various init syntaxes now vs how they'll look if using the proposed
  • Including the full context of where they might be used - declarations, method calls, etc. This is particularly relevant given C# already has the special array initialization syntax { } which only works for declarations
  • Since this is target-typing, presumably this doesn't work with var. Examples would help illustrate that
  • Does initializing collections of mixed types "just work" or does it require casting? For example, today I have to do new object[] { 1, "foo" } because the compiler cannot infer a best type.
  • Since spread operators currently don't work at all in existing collection initialization syntaxes, I think that needs to be segregated from other examples

@CyrusNajmabadi
Copy link
Member Author

Since this is target-typing, presumably this doesn't work with var. Examples would help illustrate that

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.

@CyrusNajmabadi
Copy link
Member Author

Does initializing collections of mixed types "just work"

This is covered in the spec as well. But I'll call out explicitly.

@TonyValenti
Copy link

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.

@CyrusNajmabadi
Copy link
Member Author

Introduce "let" as a way of declaring an "immutable" variable.

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!

@jnm2
Copy link
Contributor

jnm2 commented Oct 30, 2021

It would have to work for let x = new List<MyMutableClass> { new MyMutableClass() }; too.

@BhaaLseN
Copy link

init methods would be cool for other things as well. There have been many issues, discussions etc. in the past that asked for a way to move common initialization code from a ctor into a (specifically marked) method which then may still initialize readonly (and now: init) members which right now is limited to the ctor itself (for readonly) and initialization contexts (for init).

@TahirAhmadov
Copy link

TahirAhmadov commented Nov 1, 2021

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 new[] { ... } syntax, so that leads me to List<T>. In my work, the most annoying boilerplate is the new List<T> { ... } - replacing that with [ ... ] would be a great improvement.

PS. Another reason to go with List<T> - I and probably many others follow the rule of using explicit types for keyword types, like int[], and var for other types, including List<int>. This means if I want an array, I can always do int[] a= [1,2,3]; - which aligns with how I manage explicit/implicit types, and for List<T>, I currently do var a = new List<int> { 1, 2, 3, }; which is greatly simplified to var a = [1, 2, 3];

PPS. On second thought, perhaps we can just say no (for now) to implicit type and be done with it.

@CyrusNajmabadi
Copy link
Member Author

Other cons are that it heap allocates and that it is fixed length.

@TahirAhmadov
Copy link

Isn't it possible to make List<T> work with 1 allocation? Doesn't its new List<T>(123) ctor allocate the internal array to the specified size from the get go? Also, what's stopping us from using a special new ctor or static factory method (which can possibly be made internal) - surely that's not too much to add?

@jnm2
Copy link
Contributor

jnm2 commented Nov 1, 2021

The List<T> instance is one heap allocation, and the internal array is a second heap allocation. If the initial capacity is sufficient, there are no additional heap allocations or memory copies beyond that.

@TahirAhmadov
Copy link

TahirAhmadov commented Nov 1, 2021

Oh my, that was such a brain fart - of course the List<> itself needs an allocation. Which immediately made me think of another idea - can a new type be created for this? Something called ValueList<T>. It'll be a struct, implement IList<T>, and have implicit conversion to List<T>. Internally, it can even perform the necessary analysis - if the # of items is low, keep the items on the stack, too; if it grows beyond a certain limit, say, 1024 bytes, move it to the heap (or go straight to heap if initial capacity is >=1024 bytes).

PS. Internally, this type can either 1) use if statements to determine whether it's operating in stack or heap mode, or 2) have delegates which are assigned either the stack or heap "handlers".

@CyrusNajmabadi
Copy link
Member Author

  • can a new type be created for this?

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.

@zms9110750
Copy link

I hope that someday in the future, when the type is clear, the 'new' used to create objects can be omitted. Let's create an object using an unmodified JSON text.

@KennethHoff
Copy link

@zms9110750 I think something akin to Tagged Templates could be cool.

@HaloFour
Copy link
Contributor

@zms9110750

I hope that someday in the future, when the type is clear, the 'new' used to create objects can be omitted. Let's create an object using an unmodified JSON text.

See: #7708

I would definitely not expect the JSON/Javascript spec to be merged into the C# spec.

@jnm2
Copy link
Contributor

jnm2 commented Dec 12, 2023

Let's create an object using an unmodified JSON text.

It's a interesting idea which has come up, but there are challenges. I would not expect it to be good for a strongly-typed object model, but maybe for something like JNode and JsonNode. There are a lot of challenges though.

The C# language wouldn't have to merge the JSON spec if it doesn't make a guarantee about JSON. It would still be a favorable design if 99.99% of JSON pastes worked in common scenarios. But I'm not sure how common it would end up being when using non-strongly-typed object models. I'm not comfortable with dropping new when instantiating arbitrary non-collection objects.

@CyrusNajmabadi
Copy link
Member Author

The main issue with this is that to support json syntax we'd have to support { ... } and it was exactly { ... } that we found untenable at both a pattern and expression level to support. It's why we came up with [...] in the first place. I'm really not seeing how that can change unfortunately.

@TahirAhmadov
Copy link

TahirAhmadov commented Dec 12, 2023

Let's create an object using an unmodified JSON text.

var json = JToken.Parse("""
{
  "First": "John",
  "Last": "Smith",
  "Phones": [ "555-555-5555" ]
}
""");

@CyrusNajmabadi
Copy link
Member Author

@TahirAhmadov Raw string literals don't use @. Thanks :)

@TahirAhmadov
Copy link

Corrected :)
I think it may be possible to create some kind of an IDE plugin to provide auto-complete experience inside JSON strings?
Still, I'm not sure why one would need to create JSON "constants" like that in C#...

@CyrusNajmabadi
Copy link
Member Author

@TahirAhmadov we already do:

image

Note the classification and errors.

@TahirAhmadov
Copy link

TahirAhmadov commented Dec 14, 2023

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 sc2 encounters a get-only collection property and expects (by to be designed C# spec) a Clear method.
Downsides: it may be difficult to distinguish between = and += when quickly looking through the code.
Alternatives: make StringOptions a get/set property, but this may be impossible/undesirable in some cases; also, then the appending scenario (like in sc init block) will have to continue using the existing { "b", "c" } syntax, which is inconsistent with the desirable theme of using [ and ] for collections.

@HaloFour
Copy link
Contributor

HaloFour commented Dec 14, 2023

@TahirAhmadov

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, AddRange(ReadOnlySpan<T> items).

@TahirAhmadov
Copy link

@HaloFour those are exactly my reasons for requesting this:

  1. Consistency with collection expressions;
  2. More efficient and enhanced usage of collections' APIs;
  3. Consistency with the upcoming += syntax for init block property setting, say, adding a handler to an event.
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, 
};

@julealgon
Copy link

3. Consistency with the upcoming += syntax for init block property setting

@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 AddRange for collections such as StringsOptions there?

I used to create my own custom Add extension method that took ICollection<T> as the parameter so I could do this:

string[] options = ["x", y"];
var sc = new SomeControl
{
  StringsOptions = { ["b", "c", .. options] } // This works today
};

@TahirAhmadov
Copy link

@julealgon
#5176
Yes I imagine my code snippet compiles to:

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.

@TahirAhmadov
Copy link

@julealgon thanks for the extension method idea.
However, you don't need to wrap the list in a collection expression - you can just do:

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 Add in the very beginning and AddRange is separate. I'm currently thinking of how to make use of this mechanism without making Add available everywhere on all ICollection<T>/IList<T>.

  1. Either decorate the collection item type with a marker interface, and constrain the extension method to that;
  2. Sub-class List<T> with something like ListRangeAdd<T> (potentially with IListRangeAdd<T> interface) and add a native method there;
  3. Create a thin struct wrapper with a native method.

If anybody can provide input on which of these approaches they prefer, I would greatly appreciate it. I'm leaning towards option 2.

@julealgon
Copy link

@julealgon thanks for the extension method idea. However, you don't need to wrap the list in a collection expression - you can just do:

var sc = new SomeControl
{
  StringsOptions = { "b", "c", options } // This works today
};

Oh yeah, that's what I actually meant to write 😆 Sorry for the confusion.

The problem with creating such an extension method is the potential for hidden bugs - there was a good reason why they didn't overload Add in the very beginning and AddRange is separate.

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 IEnumerable:

public static class EnumerableExtensions
{
    public static void Add<T>(this ICollection<T> target, IEnumerable<T> values)
    {
        foreach (var value in values)
        {
            target.Add(value);
        }
    }
}

I'm currently thinking of how to make use of this mechanism without making Add available everywhere on all ICollection<T>/IList<T>.

  1. Either decorate the collection item type with a marker interface, and constrain the extension method to that;
  2. Sub-class List<T> with something like ListRangeAdd<T> (potentially with IListRangeAdd<T> interface) and add a native method there;
  3. Create a thin struct wrapper with a native method.

If anybody can provide input on which of these approaches they prefer, I would greatly appreciate it. I'm leaning towards option 2.

Why not follow with your previous proposal though?

var sc = new SomeControl
{
  StringsOptions += ["b", "c", .. options]
};

Having a += operator on mutable collections makes a ton of sense to me and it looks really clean. The operator would work outside of object/collection initializers too:

var sc = new SomeControl();
sc.StringsOptions += ["b", "c", .. options];

Now that operators are possible on interfaces, both + and += could be defined for collections. You could even do this then if you wanted:

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"]

@TahirAhmadov
Copy link

TahirAhmadov commented Jun 13, 2024

Why not follow with your previous proposal though?

var sc = new SomeControl
{
  StringsOptions += ["b", "c", .. options]
};

Because it's not yet available :)

PS. I decided to table this for now. The problem is that if I add a Add(IEnumerable) overload (in any way - extension, sub-class, struct, doesn't matter), then I can't do new SomeControl { Items = { new("abc") } } anymore - the new() syntax fails to compile because IEnumerable<T> cannot be instantiated, obviously. Unfortunately, I did wholesale apply the fixer to use new() everywhere, so it would be too big of a breaking change for me.
However, others may find this useful. Still, I really hope the +=[ ... ] syntax (and the #5176) are added sooner than later.

@OJacot-Descombes
Copy link

Idea: flexible natural type:

var a = [1, 2, 3];     // int[]
var b = [1, 2, 3, ..]; // List<int>

@colejohnson66
Copy link

If you want a definite type, just use that type instead of var.

@OJacot-Descombes
Copy link

If you want a definite type, just use that type instead of var.

There are other situations, e.g., where you have to pass an argument to a parameter of type object or IList where you are not in control of the target type. If you want to use a definite type you fall of the cliff and would have to relay on a new expression with a collection initializer. My Idea is about being able to use a collection expression and still have some control of the outcome.

@mrwensveen
Copy link

If you want a definite type, just use that type instead of var.

There are other situations, e.g., where you have to pass an argument to a parameter of type object or IList where you are not in control of the target type. If you want to use a definite type you fall of the cliff and would have to relay on a new expression with a collection initializer. My Idea is about being able to use a collection expression and still have some control of the outcome.

I see your point, but List<int> b = [1, 2, 3]; already works, so it would be confusing. Would

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 A for array and L for list (L already exists for number primitives, but I'd say the context is clear enough).

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]"

@miyu
Copy link

miyu commented Jun 24, 2024

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 (0, 0, 0)f == 0f3 vs Vector4 (0, 0, 0, 0)f == 0f4). Bonus points if they can be used with source generators for compile-time codegen. 100% people would then start using them as postfixed invocations though (e.g. [a, b, c]min).

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 using System.Collections.Generic to pull the converter into scope.

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? []hs<int> and []dict<string, int>?...

@TonyValenti
Copy link

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.

@mrwensveen
Copy link

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.

ToXX() implies a conversion and would clash with methods in System.Linq.Enumerable. AsXX() would be better, but how would you prevent existing extension methods from being called? This has the potential to break a lot of existing code.

@TahirAhmadov
Copy link

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 Span<T> is passed to the extension method? Or if we go with IEnumerable<T>, it creates an extra allocation. In both cases, now we have to enumerate the spreads once to populate the temporary buffer, and once more to create the actual collection. (However it may be possible to optimize it all away, not sure about that.)

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 var, I still think it should be either List<T> or Dictionary<K,V>, respectively, and not T[] or any other type.

@jnm2
Copy link
Contributor

jnm2 commented Jun 25, 2024

If you want to use a definite type you fall of the cliff and would have to relay on a new expression with a collection initializer.

@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: (List<int>)[1, 2, 3].

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests