52

As you know, it is not allowed to use the Array-initialisation syntax with Lists. It will give a compile-time error. Example:

List<int> test = { 1, 2, 3} 
// At compilation the following error is shown:
// Can only use array initializer expressions to assign to array types. 

However today I did the following (very simplified):

class Test
{
     public List<int> Field;
}

List<Test> list = new List<Test>
{
    new Test { Field = { 1, 2, 3 } }
};

The code above compiles just fine, but when run it will give a "Object references is not set to an object" run-time error.

I would expect that code to give a compile-time error. My question to you is: Why doesn't it, and are there any good reasons for when such a scenario would run correctly?

This has been tested using .NET 3.5, both .Net and Mono compilers.

Cheers.

abatishchev
  • 98,240
  • 88
  • 296
  • 433
mikabytes
  • 1,818
  • 2
  • 18
  • 30
  • 1
    The simpler example `Test test= new Test { Field = { 1, 2, 3 }};` has the same behavior. – CodesInChaos Feb 04 '11 at 14:53
  • 3
    The "Object references is not set to an object" is quite logical since your list Field is not initialized. Changing the Test body to public List Field = new List(); makes this run without any problem also. – Øyvind Bråthen Feb 04 '11 at 14:56
  • take a reflector, and see how the compiler treats this, and you'll understand. also look at how var stuff = new List() {4,5}; is handeled – AK_ Feb 04 '11 at 18:33

6 Answers6

51

I think this is a by-design behavior. The Test = { 1, 2, 3 } is compiled into code that calls Add method of the list stored in the Test field.

The reason why you're getting NullReferenceException is that Test is null. If you initialize the Test field to a new list, then the code will work:

class Test {    
  public List<int> Field = new List<int>(); 
}  

// Calls 'Add' method three times to add items to 'Field' list
var t = new Test { Field = { 1, 2, 3 } };

It is quite logical - if you write new List<int> { ... } then it creates a new instance of list. If you don't add object construction, it will use the existing instance (or null). As far as I can see, the C# spec doesn't contain any explicit translation rule that would match this scenario, but it gives an example (see Section 7.6.10.3):

A List<Contact> can be created and initialized as follows:

var contacts = new List<Contact> {
    new Contact {
        Name = "Chris Smith",
        PhoneNumbers = { "206-555-0101", "425-882-8080" }
    },
    new Contact {
        Name = "Bob Harris",
        PhoneNumbers = { "650-555-0199" }
    }
};

which has the same effect as

var contacts = new List<Contact>();
Contact __c1 = new Contact();
__c1.Name = "Chris Smith";
__c1.PhoneNumbers.Add("206-555-0101");
__c1.PhoneNumbers.Add("425-882-8080");
contacts.Add(__c1);
Contact __c2 = new Contact();
__c2.Name = "Bob Harris";
__c2.PhoneNumbers.Add("650-555-0199");
contacts.Add(__c2);

where __c1 and __c2 are temporary variables that are otherwise invisible and inaccessible.

Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • A little bit relevant: http://stackoverflow.com/questions/459652/why-do-c-collection-initializers-work-this-way – Elian Ebbing Feb 04 '11 at 15:12
  • Thank you, excellent answer. With this description the behaviour is logical. However what still does not seem logical to me is to allow = {} syntax for adding items, this way I would read "This field equals that", but in fact the list may contain more items than the ones I specified if any were added upon object construction. I would rather have a compilation error ;-) – mikabytes Feb 04 '11 at 16:02
  • 2
    Yes, there is a bit of inconsistency. Using the logic from this sample, one should be able to write `var l = new List(); l = { 1, 2, 3 };` - but that doesn't work! – Tomas Petricek Feb 04 '11 at 16:13
25

I would expect that code to give a compile-time error.

Since your expectation is contrary to both the specification and the implementation, your expectation is going to go unfulfilled.

Why doesn't it fail at compile time?

Because the specification specifically states that is legal in section 7.6.10.2, which I quote here for your convenience:


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 field or property, the elements given in the initializer are added to the collection referenced by the field or property.


when would such code run correctly?

As the spec says, the elements given in the initializer are added to the collection referenced by the property. The property does not reference a collection; it is null. Therefore at runtime it gives a null reference exception. Someone has to initialize the list. I would recommend changing the "Test" class so that its constructor initializes the list.

What scenario motivates this feature?

LINQ queries need expressions, not statements. Adding a member to a newly-created collection in a newly-created list requires calling "Add". Since "Add" is void-returning, a call to it can only appear in an expression statement. This feature allows you to either create a new collection (with "new") and populate it, or populate an existing collection (without "new"), where the collection is a member of an object you are creating as the result of a LINQ query.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Thank you for a very direct answer to my questions. As I also commented on the other answers it is logical in its behaviour in the context on what we want to be able to do, although it is not very logical in its syntax as one would expect an `=` operation to do an exact assignment. Your LINQ motivation contributes to this, and I can see why they decided to design it this way. – mikabytes Feb 04 '11 at 16:32
  • Adding it to the MSDN documentation at http://msdn.microsoft.com/en-us/library/bb384062.aspx would be nice. It only shows other variants of collection-/object-initializers. – CodesInChaos Feb 04 '11 at 16:35
  • @Mika the meaning of `=` in your code matches the `=` on normal collection initializers. The unusual thing about your code is that it has no `new List` in there. – CodesInChaos Feb 04 '11 at 16:37
  • @Mika: see this question for some more musings about this syntax. http://stackoverflow.com/questions/4773889/covariant-object-initializers/4774958#4774958 – Eric Lippert Feb 04 '11 at 16:40
  • @CodeInChaos The unusual thing being that `=` in this context means "append this for me". Yes, it matches because it is done the same way in both cases. – mikabytes Feb 04 '11 at 17:51
  • @Eric Lippert Thank you for the link, it is interesting reading :-) – mikabytes Feb 04 '11 at 17:54
18

This code:

Test t = new Test { Field = { 1, 2, 3 } };

Is translated to this:

Test t = new Test();
t.Field.Add(1);
t.Field.Add(2);
t.Field.Add(3);

Since Field is null, you get the NullReferenceException.

This is called a collection initializer, and it will work in your initial example if you do this:

List<int> test = new List<int> { 1, 2, 3 };

You really need to new up something in order to be able to use this syntax, i.e., a collection initializer can only appear in the context of an object creation expression. In the C# spec, section 7.6.10.1, this is the syntax for an object creation expression:

object-creation-expression:
  new type ( argument-list? ) object-or-collection-initializer?
  new type object-or-collection-initializer
object-or-collection-initializer:
  object-initializer
  collection-initializer

So it all starts with a new expression. Inside the expression, you can use a collection initializer without the new (section 7.6.10.2):

object-initializer:
  { member-initializer-list? }
  { member-initializer-list , }
member-initializer-list:
  member-initializer
  member-initializer-list , member-initializer
member-initializer:
  identifier = initializer-value
initializer-value:
  expression
  object-or-collection-initializer // here it recurses

Now, what you're really missing is some kind of list literal, which would be really handy. I proposed one such literal for enumerables here.

Community
  • 1
  • 1
Jordão
  • 55,340
  • 13
  • 112
  • 144
  • Yes, exactly. The original code is syntactically correct but there is actually no List to put those items into. – Al Kepp Feb 04 '11 at 15:01
  • Your last example is well known. But I didn't know you could use a collection-initializer inside an object-initializer without `new MyCollection`. And the MSDN article you linked doesn't mention that either. – CodesInChaos Feb 04 '11 at 15:01
  • Another good answer. I guess they had to do it this way to allow adding of items without overriding the values that may have been added in the constructor of the initial object. It's a bit ambiguous since you expect the resulting value of an `=` operation to be an exact match of the right-side assignment, but in this case it is only adding a set of values. – mikabytes Feb 04 '11 at 16:14
  • @CodeInChaos: you have to look at the C# grammar in order to _see_ that. I've updated appropriately. – Jordão Feb 04 '11 at 16:40
3
var test = (new [] { 1, 2, 3}).ToList();
Kris Ivanov
  • 10,476
  • 1
  • 24
  • 35
  • 5
    He's not asking how to make it work, he's asking why the original program even compiles. – Anthony Pegram Feb 04 '11 at 14:53
  • 5
    it compiles because it is valid C#, it fails at run time because he did not instantiate the list object – Kris Ivanov Feb 04 '11 at 14:55
  • @K Ivanov: What a fantastic explanation. – BoltClock Feb 04 '11 at 14:56
  • 2
    next we will ask why division by zero compiles and how come C# compiler did not catch that, right – Kris Ivanov Feb 04 '11 at 14:57
  • K Ivanov you are sarcastic but true. +1 – Al Kepp Feb 04 '11 at 15:11
  • 1
    @K Ivanov: You might want to try answering the question in your answer instead of in comments. Though of course others have now answered it better. And FYI the c# compiler does catch basic divide by zero errors. eg `int i = 1 / 0;` does not compile. – Chris Feb 04 '11 at 15:16
  • 3
    @Al Kepp: In my view the root of the answer is not that he did't instantiate the list object but that the compiler turned the List = Array type thing in the initialiser into a series of separate adds rather than just treating it as a single assignment. If this is obvious to you then great but judging by the number of people who upvoted the question it clearly wasn't obvious to all. – Chris Feb 04 '11 at 15:18
  • @K Ivanov: when you write a=1/0, you get a compilation error; when you write a = 1/b, everyone can guess that you are assigning a new value that will be computed at run time. When someone writes a = {x,y} , you have to be an expert to spot that it is not an assignment. – jmster Feb 04 '11 at 15:43
2

The reason for this is that the second example is a member list initialiser - and the MemberListBinding expression from System.Linq.Expressions give an insight into this - please see my answer to this other question for more detail: What are some examples of MemberBinding LINQ expressions?

This type of initialiser requires that the list is already initialised, so that the sequence you provide can be added to it.

As a result - syntactically there is absolutely nothing wrong with the code - the NullReferenceException is a runtime error caused by the List not actually having been created. A default constructor which news the list, or an inline new in the code body, will solve the runtime error.

As for why there is a difference between that and the first line of code - in your example it's not allowed because this type of expression can't be on the right hand side of an assignment because doesn't actually create anything, it's only shorthand for Add.

Community
  • 1
  • 1
Andras Zoltan
  • 41,961
  • 13
  • 104
  • 160
  • I took a look at the link. Good stuff for getting more in-depth knowledge of how things are actually done. Thanks! As for my example, I think it's very clear now with all the answers I've gotten :) – mikabytes Feb 04 '11 at 16:21
1

Change your code to this:

class Test
{
   public List<int> Field = new List<int>();
}

The reason is that you must explicitly create a collection object before you can put items to it.

Al Kepp
  • 5,831
  • 2
  • 28
  • 48