wolfgang ziegler


„make stuff and blog about it“

Different Ways of Initializing a Dictionary in .NET

August 31, 2022

I was today years old when I found out that you can initialize .NET Dictionarys like this.

var dict = new Dictionary<int, string>
{
  [23] = "foo",
  [42] = "bar"
};

The way I was used to doing that was like this:

var dict = new Dictionary<int, string>
{
  { 23, "foo"},
  { 23, "bar"}
};

So, what's the difference there?

Let's fire up a .NET disassembler (like ildasm) and look at the generated IL code.

Note: I you are using Mac or Linux, there is AvaloniaILSpy for decompiling .NET assemblies using a GUI tool.

ILSpy on Mac

The first version (using the [ x ] = y notation) produces following code.

.method private hidebysig static 
  void '<Main>$' (
    string[] args
  ) cil managed 
  {
  // Method begins at RVA 0x2050
  // Header size: 1
  // Code size: 33 (0x21)
  .maxstack 8
  .entrypoint

  IL_0000: newobj instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, string>::.ctor()
  IL_0005: dup
  IL_0006: ldc.i4.s 23
  IL_0008: ldstr "foo"
  IL_000d: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, string>::set_Item(!0, !1)
  IL_0012: nop
  IL_0013: ldc.i4.s 42
  IL_0015: ldstr "bar"
  IL_001a: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, string>::set_Item(!0, !1)
  IL_001f: nop
  IL_0020: ret
} // end of method Program::'<Main>$'

Without the syntactic sugar of the initializer, this decompiles to:

Dictionary<int, string> dictionary = new Dictionary<int, string>();
dictionary[23] = "foo";
dictionary[42] = "bar";

So, we are basically using the set_Item indexer to add a value to the dictionary.

On the other hand, by using the initializer syntax I have been used to ({ x, y }), we get following IL code.

.method private hidebysig static 
  void '<Main>$' (
    string[] args
  ) cil managed 
{
  // Method begins at RVA 0x2050
  // Header size: 1
  // Code size: 33 (0x21)
  .maxstack 8
  .entrypoint

  IL_0000: newobj instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, string>::.ctor()
  IL_0005: dup
  IL_0006: ldc.i4.s 23
  IL_0008: ldstr "foo"
  IL_000d: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, string>::Add(!0, !1)
  IL_0012: nop
  IL_0013: ldc.i4.s 42
  IL_0015: ldstr "bar"
  IL_001a: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, string>::Add(!0, !1)
  IL_001f: nop
  IL_0020: ret
} // end of method Program::'<Main>$'

Which would decompile to this (without using the initializer syntactic sugar);

Dictionary<int, string> dictionary = new Dictionary<int, string>();
dictionary.Add(23, "foo");
dictionary.Add(42, "bar");

This means, the Dictionarys Add method gets used here.

Note the difference?

This means, with the bracket initializer syntax, supplying duplicate keys would totally work and the last added value wins.

var dict = new Dictionary<int, string>
{
  [23] = "foo",
  [42] = "bar"
  [23] = "BAZ!", // This is fine.
};

Whereas using this initializer syntax, duplicate keys result in a System.ArgumentException: An item with the same key has already been added. error.

var dict = new Dictionary<int, string>
{
  { 23, "foo"},
  { 42, "bar"}
  { 23, "BAZ!"}  // This results in an exception.
};

To be honest, I never ran into this situation yet where this would be relevant but at least I learned something new.