Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dictionary initializer has different behavior and raises run-time exception when used in combination of array initializer

I have the following C# code which initializes a new dictionary with int keys and List<string> values:

var dictionary =
    new Dictionary<int, List<string>>
    {
        [1] = new List<string> { "str1", "str2", "str3" },
        [2] = new List<string> { "str4", "str5", "str6" }
    };

If I decompile an executable made from this snippet back to C# the corresponding part looks like this:

Dictionary<int, List<string>> expr_06 = new Dictionary<int, List<string>>();
expr_06[1] = new List<string>
{
    "str1",
    "str2",
    "str3"
};
expr_06[2] = new List<string>
{
    "str4",
    "str5",
    "str6"
};

Everything seems normal and is working properly here.

But when I have the following code:

var dictionary2 =
    new Dictionary<int, List<string>>
    {
        [1] = { "str1", "str2", "str3" },
        [2] = { "str4", "str5", "str6" }
    };

which again seems like normal code and compiles successfully, but during runtime I get the following exception:

System.Collections.Generic.KeyNotFoundException: 'The given key was not present in the dictionary.'

When I look into the decompiled code of the second example I can see that it is different from the first one:

Dictionary<int, List<string>> expr_6E = new Dictionary<int, List<string>>();
expr_6E[1].Add("str1");
expr_6E[1].Add("str2");
expr_6E[1].Add("str3");
expr_6E[2].Add("str4");
expr_6E[2].Add("str5");
expr_6E[2].Add("str6");

And of course this explains the exception.

So now my questions are:

  1. Is this expected behavior and is it documented somewhere?

  2. Why is the syntax above allowed but the following syntax is not?

    List<string> list = { "test" };
    
  3. Why is the following syntax not allowed then?

    var dict = new Dictionary<int, string[]>
    {
        [1] = { "test1", "test2", "test3" },
        [2] = { "test4", "test5", "test6" }
    };
    

Similar but different questions:

  • What happens under the hood when using array initialization syntax to initialize a Dictionary instance on C#?
  • How to make inline array initialization work like e.g. Dictionary initialization?
like image 613
Nikolay Kostov Avatar asked Jun 16 '17 14:06

Nikolay Kostov


2 Answers

Using a list-initializer (that is using = { "test1", "test2", "test3" }) assumes the list already was initialized in some way, e.g. in the class´ constructor to something like this:

class MyClass
{
    List<string> TheList = new List<string>();
}

Now you can use the list-initializer:

var m = new MyClass { TheList = { myElementsHere } };

As you already showed this is just a shortcut to calling the Add-method.

However in your case the list is not initialized at all making the call to the list-initializer raise the mentioned exception. Just declaring a Dictionary whose values are lists of any type doesn´t mean you already initialized those lists. You have to do this as with any other class before you can call any of its methods (e.g. Add which is just wrapped within the initializer).

like image 52
MakePeaceGreatAgain Avatar answered Nov 02 '22 07:11

MakePeaceGreatAgain


Let me try to answer all of your questions:


  1. Is this expected behavior and is it documented somewhere?

Yes, it is documented in the C# 6.0 Language Specification under sections §7.6.11.2 Object initializers and §7.6.11.3 Collection initializers.

The syntax

var a =
    new Test
    {
        [1] = "foo"
        [2] = "bar"
    };

was actually newly introduced in C# 6.0 as an extension of the previous object initialization syntax to indexers. An object initializer used together with new (see object creation expression, §7.6.11) always translates to object instantiation and member access of the corresponding object (using a temporary variable), in this case:

var _a = new Test();
_a[1] = "foo";
_a[2] = "bar";
var a = _a;

The collection initializer goes similar besides that each element of the initializer is passed as an argument to the Add method of the newly created collection:

var list = new List<int> {1, 2};

becomes

var _list = new List<int>();
_list.Add(1);
_list.Add(2);
var list = _list;

An object initializer can also contain other object or collection initializers. The specification states for the case of collection initializers:

A member initializer that specifies a collection initializer after the equals sign is an initialization of an embedded collection. Instead of assigning a new collection to the target field, property or indexer, the elements given in the initializer are added to the collection referenced by the target.

So a sole collection initalizer used within an object initializer will not attempt to create a new collection instance. It will only try to add the elements to an exisiting collection, i.e. a collection that was already instantiated in the constructor of the parent object.

Writing

[1] = new List<string> { "str1", "str2", "str3" }

is actually a totally different case because this is an object creation expression which only contains an collection initializer, but isn't one.


  1. Why is the syntax above allowed but the following syntax is not?

    List<string> list = { "test" };
    

Now, this is not a collection initializer anymore. A collection initalizer can only occur inside an object initializer or in an object creation expression. A sole { obj1, obj2 } next to an assignment is actually an array initializer (§12.6). The code does not compile since you can't assign an array to a List<string>.


  1. Why is the following syntax not allowed then?

    var dict = new Dictionary<int, string[]> 
    {
        [1] = { "test1", "test2", "test3" },
        [2] = { "test4", "test5", "test6" }     
    };
    

It is not allowed because collection initalizers are only allowed to initialize collections, not array types (since only collections have an Add method).

like image 29
adjan Avatar answered Nov 02 '22 05:11

adjan