List<int> test = {1, 2, 3} - is it a feature or a bug?
C#Compiler ErrorsC# Problem Overview
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.
C# Solutions
Solution 1 - C#
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.
Solution 2 - C#
> 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.
Solution 3 - C#
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.
Solution 4 - C#
var test = (new [] { 1, 2, 3}).ToList();
Solution 5 - C#
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: https://stackoverflow.com/questions/2917448/what-are-some-examples-of-memberbinding-linq-expressions/2917498#2917498
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 new
s 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
.
Solution 6 - C#
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.