开发者

List<int> test = {1, 2, 3} - is it a feature or a bug?

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.


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.


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.


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.


var test = (new [] { 1, 2, 3}).ToList();


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.


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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜