Collection expressions and initializers

Arseni Mourzenko
Founder and lead developer
179
articles
July 8, 2025
Tags: c# 1

It has been more than a year Mi­crosoft re­leased C# 12. With this ver­sion came a bunch of re­al­ly nice things, that in­clude col­lec­tion ex­pres­sions. You know, in­stead of writ­ing ICollection<int> x = new Collection<int> { 1, 3, 8 }, you can now write ICollection<int> x = [1, 3, 8], nice and sweet.

And sweet it is, un­til you start find­ing things that may not be im­me­di­ate­ly in­tu­itive. For in­stance, I was at some mo­ment puz­zled at the IL code I spot­ted when check­ing what ex­act­ly col­lec­tion ex­pres­sions get trans­lat­ed to. With the help of sev­er­al Stack Over­flow users, it made every­thing clear. But I could have spent a long time try­ing to guess it my­self.

Now, such nice new lan­guage fea­tures have a draw­back: once you start us­ing them, you just can't stop. When I was writ­ing this ar­ti­cle, I had at some point to move a tiny lit­tle piece of code from C# 13 back to C# 3—that was a rather un­pleas­ant ex­pe­ri­ence to be de­prived of all the good things, and to have to write a lot of ex­tra code. As I got an habit of re­ly­ing a bit too much on col­lec­tion ex­pres­sions, I found my­self star­ing at the fol­low­ing piece of code:

var line = new Line
{
    Flagged = new Dictionary<int, bool> { [-1] = false, [1] = true, [2] = true },
    MarkHints = [-1, 1],
};

As Flagged is of type IDictionary<int, bool>, there is no way for me to re­move that ugly type wast­ing all this space in the mid­dle of the ini­tial­iza­tion. Or is it?

I de­cid­ed to try a lit­tle change.

var line = new Line
{
    Flagged = { [-1] = false, [1] = true, [2] = true },
    MarkHints = [-1, 1],
};

To my sur­prise, it com­piled. And ran. But it only worked by chance, as it was not do­ing what I was think­ing it is do­ing.

Down the rab­bit hole

Let's re­pro­duce this thing through a more com­plete but short ex­am­ple. Some­thing that any­one could run by sim­ply do­ing dotnet new console.

using System;
using System.Collections.Generic;

var r = new R() { D = { [2] = 4 } };

foreach (var x in r.D)
{
    Console.WriteLine($"{x.Key}: {x.Value}");
}

class R
{
    public Dictionary<int, int> D { get; set; } = [];
}

Don't mind the foreach block: it just dis­plays the val­ues from the dic­tio­nary, and, in this case, shows 2: 4. What mat­ters is that the class con­tains a dic­tio­nary prop­er­ty, and that when the in­stance of this class is ini­tial­ized, it looks like the dic­tio­nary is be­ing ini­tial­ized as well.

Ex­cept that this is not what the code ac­tu­al­ly does. Here's the in­ter­est­ing part of IL that cor­re­sponds to the ini­tial­iza­tion of r. I cheat­ed a bit and re­moved ful­ly qual­i­fied types to make the IL a bit short­er.

IL_0000: newobj instance void R::.ctor()
IL_0005: dup
IL_0006: callvirt instance class [...]Dictionary`2<int32, int32> R::get_D()
IL_000b: ldc.i4.2
IL_000c: ldc.i4.4
IL_000d: callvirt instance void class [...]Dictionary`2<int32, int32>::set_Item(!0, !1)
IL_0012: nop
IL_0013: stloc.0

See? It doesn't set the prop­er­ty D. At no point does it call the set­ter. What it calls, how­ev­er, is the get­ter of D! And then it ma­nip­u­lates the dic­tio­nary that is be­ing re­turned by the get­ter.

There is a way to check it even with­out look­ing at IL. If I re­place:

public Dictionary<int, int> D { get; set; } = [];

by:

public Dictionary<int, int> D { get; set; }

with­out the de­fault val­ue, the ap­pli­ca­tion would crash with a null ref­er­ence ex­cep­tion. What's even fun­nier is when I try to fill the ini­tial dic­tio­nary with val­ues.

public Dictionary<int, int> D { get; set; } = new() { [1] = 6, [2] = 5 };

The out­put would be:

1: 6
2: 4

So in­stead of be­ing a sort of dic­tio­nary ex­pres­sion sim­i­lar to a col­lec­tion ex­pres­sion, this thing is some­thing else. But what?

Back to year 2007

I thought that it should be some rel­a­tive­ly new lan­guage fea­ture that I was un­aware of, so I tried to get back in time, try­ing to low­er the ver­sion of C#, un­til it would say that this syn­tax is not valid any longer. I was pret­ty sure I would find it around C# 10, but the voy­age took much, much longer.

Eigh­teen years ago, C# 3 was re­leased. As any re­spectable ver­sion of this lan­guage, C# 3 brought its own dose of love­li­ness, in­clud­ing ob­ject and col­lec­tion ini­tial­iz­ers. You know, that thing that we take for grant­ed, which makes it pos­si­ble to write new Point { X = 5, Y = 10 } or to do new Collection<int> { 7, 9 }—that didn't al­ways ex­ist.

And it's ex­act­ly this lan­guage fea­ture that ap­pears in my orig­i­nal piece of code. Let's il­lus­trate it with a sim­ple ob­ject:

using System;

var r = new R() { P = { Y = 6 } };

Console.WriteLine($"{r.P.X}, {r.P.Y}");

class R
{
    public Point P { get; set; } = new() { X = 8, Y = 3 };
}

class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

Here, an ob­ject ini­tial­iz­er does ex­act­ly the same thing: it grabs the ex­ist­ing ob­ject by call­ing the get­ter P, and then mu­tates it. This piece of code out­puts 8, 6—8 be­ing the val­ue that was un­changed, and 6 be­ing the one that was re­as­signed. The doc­u­men­ta­tion is very clear (well, maybe not very, but still) about that (em­pha­sis mine):

This syn­tax, Property = { ... }, al­lows you to ini­tial­ize mem­bers of ex­ist­ing nest­ed ob­jects

The doc­u­men­ta­tion con­tin­ues by point­ing to a ded­i­cat­ed sec­tion that—this time it's true—is ab­solute­ly clear about the dif­fer­ence be­tween ob­ject ini­tial­iz­er with new(), and one with­out it.

Nat­u­ral­ly, the same works with col­lec­tion ini­tial­iz­ers:

using System;
using System.Collections.ObjectModel;
using System.Linq;

var r = new R() { C = { 9 } };
Console.WriteLine(string.Join(", ", r.C.Select(x => x.ToString())));

class R
{
    public Collection<int> C { get; set; } = [5, 5];
}

would out­put 5, 5, 9. An ex­ist­ing col­lec­tion is not re­placed—rather, a new val­ue is ap­pend­ed to it.