Почему комбинация инициализаторов объекта и коллекции использует метод Add? - PullRequest
0 голосов
/ 20 октября 2018

Следующая комбинация инициализаторов объекта и коллекции не приводит к ошибке компиляции, но в корне неверна (https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#examples),, поскольку при инициализации будет использоваться метод Add:

public class Foo
{
    public List<string> Bar { get; set; }
}

static void Main()
{
    var foo = new Foo
    {
        Bar =
        {
            "one",
            "two"
        }
    };
}

Таким образом, вы получите NullReferenceException. В чем причина принятия такого небезопасного решения при разработке синтаксиса языка? Почему бы не использовать инициализацию новой коллекции, например?

Ответы [ 4 ]

0 голосов
/ 31 октября 2018

Во-первых, это не только для комбинации инициализаторов объекта и коллекции.То, что вы здесь имеете в виду, называется инициализаторами вложенных коллекций , и то же правило (или проблема, по вашему мнению) применимо к инициализаторам вложенных объектов .Таким образом, если у вас есть следующие классы:

public class Foo
{
    public Bar Bar { get; set; }
}

public class Bar
{
    public string Baz { get; set; }
}

и вы используете следующий код

var foo = new Foo
{
    Bar = { Baz = "one" }
};

, вы получите то же NRE во время выполнения, потому что new Bar не будет создано,но попытайтесь установить Baz свойство Foo.Bar.

Обычно синтаксис для инициализатора объекта / коллекции имеет вид

target = source

, где source может быть выражение , инициализатор объекта или инициализатор коллекции.Обратите внимание, что new List<Bar> { … } является , а не инициализатором коллекции - это выражение создания объекта (в конце концов, все является объектом, включая коллекцию) в сочетании с инициализатором коллекции,И вот в чем разница - идея состоит в том, чтобы не опустить new, но дать вам выбор либо использовать выражение создания + инициализатор объекта / коллекции, либо только инициализаторы.

К сожалению, документация C # не объясняет эту концепцию, но спецификация C # делает это в разделе Инициализаторы объектов :

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

и

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


Так почему же это так?Во-первых, потому что он явно делает именно то, что вы говорите.Если вам нужно new, используйте new, в противном случае оно работает как назначение (или добавление для коллекций).

Другие причины: целевое свойство не может быть установлено (уже упоминалось в других ответах).Но также это может быть не создаваемый тип (например, интерфейс, абстрактный класс), и даже когда это конкретный класс, кроме структуры, как он решит, что он должен использовать new List<Bar> (или new Bar в моем примере) вместо new MyBarList, если у нас есть

class MyBarList : List<Bar> { }

или new MyBar, если у нас есть

class MyBar : Bar { }

Как видите, компилятор не может делать такие предположения, поэтому IMOФункция языка предназначена для работы достаточно понятным и логичным способом.Единственная запутанная часть, вероятно, заключается в использовании оператора = для чего-то другого, но я думаю, что это было компромиссное решение - используйте тот же оператор = и добавьте new после этого, если необходимо.

0 голосов
/ 28 октября 2018

Посмотрите на этот код и его вывод из-за Debug.WriteLine ():

public class Foo
{
    public ObservableCollection<string> _bar = new ObservableCollection<string>();

    public ObservableCollection<string> Bar
    {
        get
        {
            Debug.WriteLine("Bar property getter called");
            return _bar;
        }

        set
        {
            Debug.WriteLine("Bar allocated");
            _bar = value;
        }
    }

    public Foo()
    {
        _bar.CollectionChanged += _bar_CollectionChanged;
    }

    private void _bar_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        Debug.WriteLine("Item added");
    }
}

public MainWindow()
{
    Debug.WriteLine("Starting..");

    var foo = new Foo
    {
        Bar =
        {
            "one",
            "two"
        }
    };

    Debug.WriteLine("Ending..");
}

Вывод:

Starting..
Bar property getter called
Item added
Bar property getter called
Item added
Ending..

Для вас вопросы: В чем причина принятия такого небезопасного решения при разработке синтаксиса языка?Почему бы не использовать инициализацию новой коллекции, например?

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

Надеюсь, это прояснится;)

0 голосов
/ 29 октября 2018

Я считаю, что ключевая вещь, которую нужно понять, это то, что в игре есть два синтаксических вкуса сахара (или, по крайней мере, так должно быть):

  1. Инициализация объекта
  2. Инициализация коллекции

Заберите List на мгновение и посмотрите на поле как на объект:

public class Foo
{
    public object Bar { get; set; }
}

При использовании Инициализация объекта , вы назначаетеobject (или null):

var foo = new Foo()
{
    Bar = new object(); //or Bar = null
}

Теперь вернемся к исходному примеру и добавим Инициализация коллекции поверх этого.На этот раз компилятор понимает, что это свойство реализует IEnumerable, и предоставленный вами массив имеет правильный тип, поэтому он пытается вызвать метод Add интерфейса.Сначала он должен искать объект, который в вашем случае равен null, потому что вы не инициализировали его внутри.Если вы выполните отладку через это, вы обнаружите, что получатель вызывается и возвращает null, что приводит к ошибке.

Правильный способ смешивания обеих функций будет для вас назначением new объект, который вы инициализируете своими значениями:

var foo = new Foo()
{
    Bar = new List<string>(){ "one", "two" }
};

Если вы отладите эту версию, вы обнаружите, что вместо этого вызывается установщик с новым инициализированным экземпляром.

В качестве альтернативы,вы можете инициализировать ваше свойство внутренне:

public List<string> Bar { get; set; } = new List<string>();

Если вы отладите эту версию, вы обнаружите, что свойство сначала инициализируется значением, а ваша версия кода затем выполняется без ошибок (сначала вызывая получатель):

var foo = new Foo()
{    
    Bar = {"one", "two"}
};

Чтобы проиллюстрировать синтаксический аспект сахара, Инициализация коллекции работает только в пределах оператора вызова конструктора:

List<string> bar = {"one", "two" }; //ERROR: Can only use array initializer expressions to assign to array types. Try using a new expression instead.

List<string> bar = new[] { "one", "two" }; //ERROR: Cannot implicitly convert type 'string[]' to 'System.Collections.Generic.List<string>'

List<string> bar = new List<string>() { "one", "two" }; //This works!

Если вы хотитеразрешить инициализацию, как в исходном примере, тогда ожидается, что переменная будет установлена ​​в экземпляр до Add meэто можно назвать.Это правда, используете ли вы синтаксический сахар или нет.С тем же успехом я мог бы столкнуться с той же ошибкой, выполнив следующее:

var foo = new Foo();
foo.Bar.Add("one");

Так что вы можете инициализировать переменную для охвата всех баз, если, конечно, значение null не имеет семантического значенияв вашем приложении.

0 голосов
/ 28 октября 2018

Рассмотрим следующий код:

class Program
{
    static void Main()
    {
        var foo = new Foo
        {
            Bar =
            {
                "one",
                "two"
            }
        };
    }
}

public class Foo
{
    public List<string> Bar { get; set; } = new List<string>();
}

Компилятор не знает, создал ли вы новый экземпляр списка в конструкторе класса (или в другом методе).

Напомним эту коллекциюинициализатор - это серия вызовов метода Add в существующей коллекции!

См. также: Инициализаторы пользовательских коллекций

Также обратите внимание, что этот инициализатор применяется к экспонированной коллекциикак собственность.Следовательно, инициализатор коллекции возможен как часть инициализатора внешнего объекта (объект Foo в вашем примере).

Однако, если бы это была простая переменная, компилятор не позволил бы вам инициализировать коллекцию таким образом.Вот пример:

List<string> list = 
{
    "one",
    "two"
};

Это приведет к ошибке компиляции.

Как и в последнем примере, следующий код будет выглядеть так: «один, два, три, четыре»,Я думаю, что теперь вы понимаете, почему.Обратите внимание на статический экземпляр списка, а также на модификатор private в «наборе» свойства Bar, что не имеет значения, поскольку инициализатор просто вызывает метод Add, который доступен, даже если «множество» Bar является закрытым..

class Program
{
    static void Main()
    {
        var foo1 = new Foo
        {
            Bar =
            {
                "one",
                "two"
            }
        };

        var foo2 = new Foo
        {
            Bar =
            {
                "three",
                "four"
            }
        };

        PrintList(foo1.Bar);
    }

    public static void PrintList(List<string> list)
    {
        foreach (var item in list)
        {
            Console.Write(item + ", ");
        }
        Console.WriteLine();
    }

}

public class Foo
{
    private static readonly List<string> _bar = new List<string>();
    public List<string> Bar { get; private set; } = _bar;
}
...