Список <int> test = {1, 2, 3} - это функция или ошибка?

Как вы знаете, не разрешается использовать синтаксис инициализации Array со списками. Это даст ошибку времени компиляции. Пример:

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

Однако сегодня я сделал следующее (очень упрощенное):

class Test
{
     public List<int> Field;
}

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

Приведенный выше код компилируется просто отлично, но при запуске он даст сообщение об ошибке "Объект ссылается не на объект".

Я бы ожидал, что код даст ошибку времени компиляции. Мой вопрос к вам: почему это не так, и есть ли веские причины, когда такой сценарий будет работать правильно?

Это было протестировано с использованием .NET 3.5, как компиляторов .Net, так и Mono.

Приветствия.

Ответ 1

Я думаю, что это поведенческое поведение. Test = { 1, 2, 3 } скомпилирован в код, который вызывает метод Add списка, хранящегося в поле Test.

Причина, по которой вы получаете NullReferenceException, состоит в том, что Test - null. Если вы инициализируете поле Test в новом списке, тогда код будет работать:

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 } };

Это вполне логично - если вы пишете new List<int> { ... }, то он создает новый экземпляр списка. Если вы не добавите конструкцию объекта, он будет использовать существующий экземпляр (или null). Насколько я вижу, спецификация С# не содержит явного правила перевода, которое бы соответствовало этому сценарию, но оно дает пример (см. Раздел 7.6.10.3):

A List<Contact> может быть создан и инициализирован следующим образом:

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" }
    }
};

который имеет тот же эффект, что и

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);

где __c1 и __c2 являются временными переменными, которые в противном случае являются невидимыми и недоступными.

Ответ 2

Я бы ожидал, что код даст ошибку времени компиляции.

Поскольку ваше ожидание противоречит спецификации и реализации, ваше ожидание будет невыполненным.

Почему он не работает во время компиляции?

Поскольку спецификация конкретно заявляет, что это законно в разделе 7.6.10.2, которое я цитирую здесь для вашего удобства:


Инициализатор членов, который задает инициализатор коллекции после знака равенства, является инициализацией встроенной коллекции. Вместо того, чтобы назначать новую коллекцию для поля или свойства, элементы, указанные в инициализаторе, добавляются в коллекцию, на которую ссылаются поле или свойство.


если бы такой код работал правильно?

Как говорит спецификация, элементы, указанные в инициализаторе, добавляются в коллекцию, на которую ссылается свойство. Свойство не ссылается на коллекцию; он равен нулю. Поэтому во время выполнения он дает исключение с использованием NULL. Кто-то должен инициализировать список. Я бы рекомендовал изменить класс "Тест", чтобы его конструктор инициализировал список.

Какой сценарий мотивирует эту функцию?

Запросы LINQ требуют выражений, а не операторов. Добавление члена в недавно созданную коллекцию во вновь созданном списке требует вызова "Добавить". Поскольку "Добавить" возвращается в void, вызов к нему может появляться только в выражении. Эта функция позволяет вам либо создать новую коллекцию (с "новым" ), либо заполнить ее, либо заполнить существующую коллекцию (без "новой" ), где коллекция является членом объекта, который вы создаете в результате LINQ запрос.

Ответ 3

Этот код:

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

Переводится на это:

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

Так как Field - null, вы получаете NullReferenceException.

Это называется инициализатор коллекции, и он будет работать в вашем первом примере, если вы это сделаете:

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

Вам действительно нужно что-то новое, чтобы иметь возможность использовать этот синтаксис, т.е. инициализатор коллекции может появляться только в контексте выражения создания объекта. В спецификации С#, раздел 7.6.10.1, это синтаксис выражения для создания объекта:

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

Итак, все начинается с выражения new. Внутри выражения вы можете использовать инициализатор коллекции без new (раздел 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

Теперь, что вам действительно не хватает, это какой-то литерал в списке, что было бы очень удобно. Я предложил один такой литерал для перечислений здесь.

Ответ 4

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

Ответ 5

Причиной этого является то, что вторым примером является инициализатор списка членов, и выражение MemberListBinding из System.Linq.Expressions дает представление об этом - более подробно см. мой ответ на этот другой вопрос: Какие примеры выражений MemberBinding LINQ?

Этот тип инициализатора требует, чтобы список уже был инициализирован, так что добавленная вами последовательность может быть добавлена ​​к нему.

В результате - синтаксически в коде нет ничего плохого - NullReferenceException - это ошибка времени выполнения, вызванная тем, что List фактически не был создан. Конструктор по умолчанию, который new список или встроенный new в теле кода, решит ошибку времени выполнения.

Что касается того, почему существует разница между этой и первой строкой кода - в вашем примере это не разрешено, потому что этот тип выражения не может быть в правой части назначения, потому что фактически ничего не создает, это только сокращение для Add.

Ответ 6

Измените код следующим образом:

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

Причина в том, что вы должны явно создать объект коллекции, прежде чем вы сможете поместить в него элементы.