C# 12: Collection Expressions

In the previous blog post you learned about C# 12 primary constructors. In this blog post, you will learn about another C# 12 feature that is called collection expressions. They allow you to initialize collections – and so arrays – in a simpler way.

Initialize Arrays Before C# 12

To initialize an array in C#, you can use the following syntax since the very beginning of C# 1.0. It declares a names variable of type string[], and it assigns a new string[] to it with the string values Julia, Anna, and Thomas.

string[] names = new string[] { "Julia", "Anna", "Thomas" };

With the release of C# 3.0, the var keyword was introduced. It allows you to use the keyword var instead of a concrete type for the variable declaration. The C# compiler infers the type from the value that you assign to the variable. In case of the statement below, the type of the variable is string[], as you assign a string[] to it.

var names = new string[] { "Julia", "Anna", "Thomas" };

In addition, you can also omit the type on the right side of the equal sign like you see it in the code snippet below. This is possible because of a feature called implicitly typed arrays. This feature was also introduced with C# 3.0. Based on the types withing the curlies the C# compiler infers the type of the array:

var names = new[] { "Julia", "Anna", "Thomas" };

Before we look at how this syntax improved even more with C# 12, let’s also take a look at initializing collections before C# 12.

Initialize Collections Before C# 12

Let’s take here the generic List<T> class as an example for collections. Generic collections like the List<T> class where introduced with .NET 2.0 and C# 2.0. Already with C# 3.0, Microsoft introduced the so-called collection initializers. They allow you to initialize a collection in a similar way like an array by putting the values into a pair of curlies. The following code snippet shows this.

var names = new List<string> { "Julia", "Anna", "Thomas" };

Before C# 3.0, the initialization of a List<string> was not so straight forward. One way was to use its Add method like you see it in the following code snippet.

List<string> names = new List<string>();
names.Add("Julia");
names.Add("Anna");
names.Add("Thomas");

Another way was to use its constructor that takes an IEnumerable<T> as a parameter. Then you can pass in an array that allows you to specify values in curlies. So, this was the typical way in C# 2.0 to initialize a List<T> with some values.

List<string> names = new List<string>(new string[] { "Julia", "Anna", "Thomas" });

But this makes it clear that since C# 3.0 and the introduction of collection initalizers, initializing an array and a collection is very similar. The following code snippet shows this.

var namesArray1 = new string[] { "Julia", "Anna", "Thomas" }; 
// Type "string" can be omitted if you want, like shown below
var namesArray2 = new[] { "Julia", "Anna", "Thomas" }; 

var namesList  = new List<string> { "Julia", "Anna", "Thomas" };

But with C# 12, things are even better. Let’s take a look at collection expressions.

Use Collection Expressions

Let’s assume that you wrote the following array initialization in a .NET 8.0 project. A .NET 8.0 project is using C# 12 by default.

var names = new[] { "Julia", "Anna", "Thomas" };

When you put the cursor on the names variable, Visual Studio shows a light bulb. You can click on it or press CTRL+. to open it. As you can see in the screenshot below, Visual Studio suggests to use an explicit type instead of ‘var’. This is nothing new and you would get this suggestion already for C# 3.0, the version that introduced the var keyword.

For collection expressions in C# 12, you must use an explicit type for your variable declaration, and not the var keyword.

For collection expressions in C# 12, you must NOT use the var keyword. Instead, you must define an explicit type for your variable declaration. So, let’s make use of Visual Studio’s suggestion to use an explicit type instead of ‘var’. This leads to the following statement, in which the names variable has now the explicit type string[].

string[] names = new[] { "Julia", "Anna", "Thomas" };

Now let’s put the cursor again on the names variable. Visual Studio will show again a light bulb. Again, you can click on it or you can press CTRL+. to open it. As you can see in the screenshot below, Visual Studio suggests now to use a collection expression.

Visual Studio suggests to use a collection expression to initialize the variable.

Let’s select that option. Now you end up with the statement that you can see below. On the right side of the equal sign is just a pair of brackets with the values, and that is the collection expression. Wow, doesn’t this look great and readable? I must admit: I love it!

string[] names = ["Julia", "Anna", "Thomas"];

Now the great thing is that you can use exactly the same syntax to initialize a collection like a List<string>:

List<string> names = ["Julia", "Anna", "Thomas"];

You can also use exactly the same syntax to initialize a Span<string>:

Span<string> names = ["Julia", "Anna", "Thomas"];

So, you see, collection expressions make it very straight forward to initialize any kind of collection, like an array, a list, or a span. This is really powerful.

If you are familiar with other programming languages, like for example Python or JavaScript, the syntax might also look familiar to you. For example, the following statement is the JavaScript variant to initialize a names variable with an array. As you can see, the code on the right side of the equal sign is exactly the same as in C#:

let names = ["Anna", "Julia", "Thomas"];

Var Is Not the Collection Expression’s Friend

In the previous section you learned that collection expressions require an explicit variable type. The var keyword does not work. If you try it, you’ll get the error like in the screenshot below that says that there is no target type for the collection expression.

The var keyword does not work with collection expressions.

When you look at the screenshot above, you might think: Hey, but isn’t the C# compiler able to see that it’s a collection expression with strings? Of course the C# compiler is able to see that it’s a collection expression with strings. But what should it create out of it: An array, a List<string>, a Span<string> etc.? This is not clear from the statement above, and that’s the reason why you have to use an explicit type for your variable instead of the var keyword, like you see it in the code snippet below.

string[] names = ["Julia", "Anna", "Thomas"];

Of course, you could use the var keyword and a casting. But really, unless you have a var tattoo you might not want to write code like in the following statement, but it compiles fine.

var names = (string[])["Julia", "Anna", "Thomas"];

So, the point is that the compiler needs to know what type it should create from the collection expression, and that’s the reason why you cannot use the var keyword. Of course, the C# compiler could default to a type when using var, like for example to List<string> in our case. But it does not do this, which means var is not an option when using a collection expression.

Besides initalizing variables, there are also other cases for collection expressions. You can use them for example as a return value of a method like in the following code snippet. Here the C# compiler knows that it has to create a string[], because that’s the return type of the method.

public static string[] GetNames()
{
    return ["Julia", "Anna", "Thomas"];
}

And yes, you’re right, for such a small method, it would make sense to write it with an expression body.

public static string[] GetNames() => ["Julia", "Anna", "Thomas"];

Use The Spread Operator

So far you learned how to use collection expressions with simple strings as values. But there is even more. With C# 12, Microsoft introduced the spread operator that can be used with collection expressions. The spread operator (just two dots like this: ..) allows you to pass all the values of another collection to a collection expression. A picture piece of code tells more than a thousand words, so let’s take a look at an example.

The following code snippet defines two string arrays, family and friends. Next, it creates another string array called familyAndFriends. This third string array is initialized with a collection expression. That collection expression uses the spread operator .. to grab the values from the family and from the friends array. The result is that the familyAndFriends variable contains a string array with all five strings: Julia, Anna, Thomas, Katrin, and Alex.

string[] family = ["Julia", "Anna", "Thomas"];
string[] friends = ["Katrin", "Alex"];

string[] familyAndFriends = [.. family, .. friends]; 
// Contains all the 5 string values

So, in the code snippet above you can see how to use the spread operator. You just pass the array like a normal value to the collection expression, and in front of it you use the spread operator. This tells the C# compiler that you want to put the values of the array (or list, or any collection, you name it) into the collection expression.

If you pass an array to a collection expression without a spread operator, you will get an error. The following screenshot shows what happens in this example if you omit the spread operator in front of the family array. As you can see, you get the error “Cannot implicitly convert type string[] to string. As the familyAndFriends variable is of type string[], the C# compiler expects the values in the collection expression without a spread operator in front of them to be of type string.

So, you see, the spread operator allows you to use a collection expression to combine other collections to a single flat collection. Of course, you can also use a combination of individual values and collections. The following code snippet shows you another example of a collection expression. First, the two strings Sara and Elsa are added, then the spread operator is used to add the values of the family array. Next, the string Mike is added, and finally, the spread operator is used again, this time to add the values of the friends array.

string[] familyAndFriends = ["Sara", "Elsa", .. family, "Mike", .. friends];

Another important thing to mention is that the spread operator requires just a collection type like an array or a List<T>. The collection type does not have to be the same as the target type, only the type of the values must match (In our examples here string values). The following code snippet shows this. As you can see, the family variable is of type string[] and the friends variable is of type List<string>. Both are used in a collection expression with the spread operator to initialize the familyAndFriends variable, which is of type Span<string>. So, the spread operator is a powerful and flexible feature that works with any collection type.

string[] family = ["Julia", "Anna", "Thomas"];
List<string> friends = ["Katrin", "Alex"];

Span<string> familyAndFriends = [.. family, .. friends];

You learned in this section about the spread operator. It allows you to create collection expressions not only with individual values, but also with values of other collections.

What About Empty Collections?

You can also create and work with empty collections. The code snippet below shows that you can initialize an empty family array with an empty pair of brackets. When the array is used with the spread operator in another collection expression, the spread operator will detect that it’s empty. This means that in the code snippet below the familyAndFriends array will have a length of 2 and it will contain just the two strings Katrin and Alex.

string[] family = [];
string[] friends = ["Katrin", "Alex"];

string[] familyAndFriends = [.. family, .. friends]; // Contains the 2 friends

Use the Spread Operator and Empty Collections

The spread operator and empty collections are quite handy if you want to check a condition within the collection expression. You can use for example a ternary conditional operator like in the code snippet below. Take a look at the last statement that initializes the familyAndFriends variable. If the includeFriends variable is true, the friends array is included. Else, an empty collection is included, which means no friends are added. Note also how the spread operator .. is at the beginning of the ternary conditional operator. It is no more directly in front of the friends variable.

string[] family = ["Julia", "Anna", "Thomas"];
string[] friends = ["Katrin", "Alex"];

bool includeFriends = true;

string[] familyAndFriends = [.. family, .. includeFriends ? friends : []];

As collection expressions are expressions, you can also include other expressions within them. Instead of using a ternary conditional operator like above, you could also use a switch expression like you see it in the code snippet below.

string[] familyAndFriends = [.. family, 
                             .. includeFriends switch { true => friends, _ => [] }];

So, you see, a lot is possible from a syntactical point of view. The question is what’s still readable. Many developers have the opinion that nothing can beat separate variables when it comes to nested expressions and readability. In the code snippet below, the separate friendsToInclude variable keeps the collection expression to initialize the familyAndFriends variable very clean and straight forward.

string[] friendsToInclude = includeFriends ? friends : [];

string[] familyAndFriends = [.. family, .. friendsToInclude];

Now, let’s look at another very interesting thing: Using the IEnumerable<T> interface directly as a target type for your collection expression.

Use the IEnumerable<T> Interface Directly

If you’re familiar with C#, you know that arrays and collections like List<T> implement the IEnumerable<T> interface. This interface allows you to walk through a collection. To do this, you can either use a foreach loop or you can use the interface directly. This interface allows you also to query collections by using C#’s language integrated query (LINQ) syntax.

But hey, what’s the point, why do I write this here?

The interesting part is that the C# compiler allows you to initialze a variable of type IEnumerable<T> with a collection expression. That’s interesting, because IEnumerable<T> is an interface, an abstract type, and not a concrete type like a class or a struct. The following statement is valid and compiles.

IEnumerable<string> family = ["Julia", "Anna", "Thomas"];

So, the first question is: What type does the C# compiler create behind the scenes? Does it create a string[] or a List<T>? Neither. Instead, the C# compiler creates a ReadOnlyArray<T>, which is a private .NET class that implements IEnumerable<T>.

The second question is: Why would you want to use an IEnumerable<string> instead of a concrete type like string[] or List<string>? Maybe you have places in your code that require an IEnumerable<string>. Also at these places you can use collection expressions, as IEnumerable<string> is a valid target type. So, this gives you as a developer the flexibility to use a collection expression wherever an IEnumerable<string> is required.

A scenario could be for example to return some default values from methods that have IEnumerable<string> as a return type. Look for example at the LoadNames method in the code snippet below. It returns an IEnumerable<string>. First, it calls a LoadNamesFromDatabase method and it stores the result in a namesFromDb variable of type string[]. In the return statement, the ternary conditional operator is used to check if the Length property of the namesFromDb array is not equal to 0. If the condition is true, it means that the namesFromDb array contains values, and so that array is returned. If the condition is false and so the namesFromDb array does not contain any values, then the collection expression with the values Default, Thomas, Claudius, Huber is returned.

public static IEnumerable<string> LoadNames()
{
    string[] namesFromDb = LoadNamesFromDatabase();

    return namesFromDb.Length != 0
        ? namesFromDb
        : ["Default", "Thomas", "Claudius", "Huber"];
}

Without the functionality to store a collection expression in an IEnumerable<T>, the code in the code snippet above would not compile. You would have to cast the collection expression first into a concrete type, like in this case for example to a string[] or to a List<string>.

So, you see, collection expressions are really powerful and you can use them at many places in your code, and they will just work!

Note: Also other interfaces are supported.

IEnumerable<T> is just one interface that can be used as a target for a collection expression. The C# compiler also supports other interface types as targets: IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T> and IList<T>. If you want to learn more, check out the Conversions section in the official documentation about collection expressions.

Work with Multi-dimensional Arrays

You can also nest collection expressions to create multi-dimensional arrays. The following code snippet creates a two-dimensional array. Actually, an array that contains in this case three other arrays.

int[][] twoDimensionalArray = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];

The code snippet below uses two nested for loops to iterate over the two-dimensional array. It writes the items to the console. \t inserts a tab in the console (Let’s not start a tab vs spaces discussion here ;-)).

int[][] twoDimensionalArray = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];

for (int i = 0; i < twoDimensionalArray.Length; i++)
{
    for (int j = 0; j < twoDimensionalArray[i].Length; j++)
    {
        Console.Write(twoDimensionalArray[i][j]);
        Console.Write("\t");
    }

    Console.WriteLine();
}

The screenshot below shows the output in the console window. The values of every nested array are written to a single line.

What About Dictionaries?

Unfortunately, the C# 12 collection expressions are not made for dictionaries. In this section, let’s take a look at the current state for dictionaries and let’s see what you might try in your code after you know how collection expressions work. Finally, let’s take a look at some plans for C# 13.

To initialize a dictionary, you can use since C# 3.0 collection initalizers like you see it in the code snippet below. Every key value pair is in a pair of curlies, and the key and the value are separated by a comma.

Dictionary<int, string> namesById = new Dictionary<int, string>()
{
    { 1, "Julia" },
    { 2, "Anna" },
    { 3, "Thomas" },
};

With C# 6.0, Microsoft introduced the so-called dictionary intializers. The code snippet below shows you the syntax. Instead of using a pair of curlies for every key value pair, you use a pair of brackets to define the key. With an equal sign you assign a value to a key. This looks quite natural if you’ve worked already with indexers in C#. The key value pairs themselves are separated with a comma.

Dictionary<int, string> namesById = new Dictionary<int, string>()
{
    [1] = "Julia",
    [2] = "Anna",
    [3] = "Thomas"
};

Since C# 9.0, you can also use target-typed new expressions. This means in this case that you don’t have to repeat the Dictionary<int,string> type after the new keyword, as you specified it already on the left side of the equal sign. So, you can write code like you see it in the snippet below.

Dictionary<int, string> namesById = new()
{
    [1] = "Julia",
    [2] = "Anna",
    [3] = "Thomas"
};

Instead of a target-typed new expression, you can also use the var keyword like in the code snippet below.

var namesById = new Dictionary<int, string>()
{
    [1] = "Julia",
    [2] = "Anna",
    [3] = "Thomas"
};

But now, with collection expressions, wouldn’t it be great if you could write something like this:

Dictionary<int, string> namesById = [{1, "Julia"}, {2, "Anna"}, {3, "Thomas"}];

Or maybe an expression that uses tuples like this:

Dictionary<int, string> namesById = [(1, "Julia"), (2, "Anna"), (3, "Thomas")]

C# 12 does not support any of these. But there are things you can do today, for example with tuples. The following code snippet shows a static DictionaryExtensions class with an Init extension method. It’s an extension method for the type Dictionary<TKey, TValue>. So, that type is the first method parameter, prefixed with the this keyword to make this method an extension method for that type. The second method parameter is an IEnumerable<(TKey, TValue)>, so a collection with tuples that have a key and a value. Inside of the method, the items of that IEnumerable<(TKey, TValue)> are added to the dictionary. Finally, the dictionary is returned from the method.

public static class DictionaryExtensions
{
    public static Dictionary<TKey, TValue> Init<TKey, TValue>(
        this Dictionary<TKey, TValue> dictionary, 
        IEnumerable<(TKey Key, TValue Value)> values)
          where TKey : notnull
    {
        foreach (var val in values)
        {
            dictionary.Add(val.Key, val.Value);
        }

        return dictionary;
    }
}

With the Init extension method in place, you can call it on a dictionary instance like in the code snippet below. To the Init method, you can pass a collection expression with a tuple for every key-value-pair.

var namesById = new Dictionary<int, string>()
                .Init([(1, "Julia"), (2, "Anna"), (3, "Thomas")]);

So, you see, there are ways to use collection expressions with dictionaries. But they are not perfect. When you read the discussions in the C# language repository on https://github.com/dotnet/csharplang/discussions/7821, you can find out that Microsoft has plans for C# 13. There the following comment was made by Fred Silberberg, who works on the C# compiler (which has the name Roslyn):

So, according to Fred’s comment above, the syntax Microsoft is considering for C# 13 to support collection expressions for dictionaries is the following:

Dictionary<int, string> namesById = [1:"Julia", 2:"Anna", 3:"Thomas"];

I like this syntax, it’s quite nice in my opinion. It’s readable, it’s compact, it’s easy to type, and it does not have a character overload with {}()[], and it looks a bit like initializing an object in JSON. So, let’s see what C# 13 brings for dictionaries and collection expressions. If you want to take a closer look at what is planned, read this proposal for collection expressions in C# 13 and beyond.

Summary

You learned in this blog post about collection expressions that were introduced with C# 12. They let you initialize collections like arrays or lists in a very nice and readable way. As usual, Visual Studio’s refactoring tools help you to apply this new feature in your .NET projects that target .NET 8.0 or later.

I hope you enjoyed reading this blog post about C# collection expressions.

In the next blog post, you will learn about another C# 12 feature: The possibility to alias any type.

Share this post

Comments (5)

  • Dew Drop – March 20, 2024 (#4153) – Morning Dew by Alvin Ashcraft Reply

    […] C# 12: Collection Expressions (Thomas Claudius Huber) […]

    March 20, 2024 at 1:07 pm
  • Eugene Reply

    You missed „Init“ extension method name in its implementation.

    March 23, 2024 at 6:02 pm
    • Thomas Claudius Huber Reply

      Hi Eugene, I don’t think I missed it, it is there. It is a generic method though, called Init<TKey, TValue>(…).

      March 23, 2024 at 7:25 pm
  • stuartd Reply

    > The collection type must not be the same as the target type, only the type of the values must match

    I think you meant “The collection type *does not have to be* be the same as the target type..?

    January 23, 2025 at 6:00 am
    • Thomas Claudius Huber Reply

      Thank you, much appreciated. Of course, you’re right. Fixed now. Thx.

      January 23, 2025 at 11:03 am

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.