Инициализатор объекта для свойств только для чтения в С#

Если у вас есть класс:

class Foo {
      Bar Bar { get; } = new Bar();
}

class Bar {
      string Prop {get; set; }
}

Вы можете использовать инициализацию объекта, например:

var foo = new Foo { 
    Bar = { Prop = "Hello World!" }
}

Если у вас есть класс

class Foo2 {
      ICollection<Bar> Bars { get; } = new List<Bar>();
}

Ты можешь написать

var foo = new Foo2 { 
    Bars = { 
        new Bar { Prop = "Hello" }, 
        new Bar { Prop = "World" }
    }
}

но я хотел бы написать что-то вроде

var items = new [] {"Hello", "World"};
var foo = new Foo2 { 
    Bars = { items.Select(s => new Bar { Prop = s }) }
}

Однако приведенный выше код не компилируется:

не может назначить IEnumerable на панель

Я не могу писать:

var foo = new Foo2 { 
    Bars = items.Select(s => new Bar { Prop = s })
}

Панели свойств доступны только для чтения.

Это можно заархивировать?


person Stijn Van Antwerpen    schedule 22.11.2018    source источник
comment
@Hans Passant: это даст List‹Bar›, который не может быть назначен Bar   -  person Stijn Van Antwerpen    schedule 22.11.2018
comment
Вам придется пройти долгий путь и вызвать Bars.Add для каждого отдельного элемента, если все, что у вас есть, это ICollection. Инициализаторы объектов — это всего лишь синтаксическое сокращение; C#, как правило, опасается добавлять синтаксис, который увеличивает нагрузку во время выполнения, и предпочитает, чтобы вы говорили об этом явно (конечно, есть исключения).   -  person Jeroen Mostert    schedule 22.11.2018
comment
@Flater Мне кажется правильным. Какой из них вы считаете неправильным?   -  person Patrick Hofman    schedule 22.11.2018
comment
@DiegoRafaelSouza Нет, это только для чтения.   -  person Patrick Hofman    schedule 22.11.2018
comment
@Flater, у Bars нет сеттера, так что он доступен только для чтения, верно?   -  person Stijn Van Antwerpen    schedule 22.11.2018
comment
Слишком много синтаксического сахара вызывает упадок языка. Foo2 нужен конструктор, чтобы вы могли инициализировать Bars.   -  person Hans Passant    schedule 22.11.2018
comment
Это также может быть полезно stackoverflow.com/a/38666937/2946329.   -  person Salah Akbari    schedule 22.11.2018


Ответы (2)


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

CS1950: лучший метод инициализации перегруженной коллекции System.Collections.Generic.ICollection<Bar>.Add(Bar) имеет несколько недопустимых аргументов.

CS1503: аргумент #1 не может преобразовать выражение System.Collections.Generic.IEnumerable<Bar> в тип Bar

Таким образом, синтаксис SomeCollection = { someItem } будет скомпилирован в SomeCollection.Add(someItem). И вы не можете добавить IEnumerable<Bar> в коллекцию Bar.

Вам нужно вручную добавить все элементы:

foreach (bar in items.Select(s => new Bar { Prop = s }))
{
    foo.Bars.Add(bar);
}

Или, если вашей целью является более короткий код, сделайте то же самое в конструкторе Foo2:

public class Foo2 
{
    public ICollection<Bar> Bars { get; }
    
    public Foo2() : this(Enumerable.Empty<Bar>()) { }
    
    public Foo2(IEnumerable<Bar> bars)
    {
        Bars = new List<Bar>(bars);
    }
}

Затем вы можете инициализировать Foo2 следующим образом:

var foo = new Foo2(items.Select(...));

Для забавного злоупотребления синтаксисом инициализатора коллекции, как предполагает @JeroenMostert, вы можете использовать метод расширения:

public static class ICollectionExtensions
{
    public static void Add<T>(this ICollection<T> collection, IEnumerable<T> items)
    {
        foreach (var item in items)
        {
            collection.Add(item);
        }
    }
}

Что позволяет это:

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

var foo = new Foo
{
    Bar = { new [] { "foo", "bar", "baz" } }
};

    

Но это просто противно.

person CodeCaster    schedule 22.11.2018
comment
Ну, эта ошибка перегруженного вызова дает нам интересный способ сделать это: Bars = { items.Select(s => new Bar { Prop = s }) } будет принято, если мы напишем метод расширения public static void Add<T>(this ICollection<T> collection, IEnumerable<T> items). Я бы не был фанатом этого (насколько легко запутаться, опустив фигурные скобки, и что произойдет, если у нас есть коллекция коллекций?), но это что-то. - person Jeroen Mostert; 22.11.2018
comment
@JeroenMostert Я бы хотел превратить этот классный комментарий в ответ, но как...? Можем ли мы составить вопрос вместе? Может быть, поговорить где-нибудь в чате? - person Patrick Hofman; 22.11.2018
comment
@PatrickHofman: насколько я понимаю, это замечание можно просто включить в один из ответов на этот вопрос. Я бы сам не стал отвечать на этот вопрос, поскольку, как я уже упоминал, я действительно не хочу видеть это в производственном коде (хотя, размышляя об этом, я, возможно, где-то сам использовал вариант этого - плохой я) . Мне плевать на атрибуцию, так что не стесняйтесь украсть ее, если вам так хочется. - person Jeroen Mostert; 22.11.2018
comment
@CodeCaster Спасибо за добавление. - person Patrick Hofman; 22.11.2018

Bars = { ... } Не выполняет задание. Вместо этого он вызывает Add для каждого элемента в инициализаторе. Вот почему это не работает.

Вот почему Bars = items.Select(s => new Bar { Prop = s }) выдает ту же ошибку: это присваивание, а не список для добавления.

Нет другого варианта, кроме как использовать конструктор для передачи значений или использовать обычные операторы Add или AddRange после запуска конструктора.

person Patrick Hofman    schedule 22.11.2018
comment
Ну нет, второй бы работал, если бы Барс не был только для чтения. Это не удается, потому что он доступен только для чтения. Я хочу добавить каждый элемент в селектор. - person Stijn Van Antwerpen; 22.11.2018
comment
Это то, что я сказал другими словами. Поскольку это присваивание, свойство не может быть доступно только для чтения. Поэтому не назначайте, а вместо этого вызывайте Add. - person Patrick Hofman; 22.11.2018
comment
@MichaWiedenmann: это обычный инициализатор объекта, а не инициализатор коллекции. Синтаксис Bar = { Prop = ... } отличается от Bar = { Prop }, несмотря на внешнее сходство. Является ли Bar доступным только для чтения, не имеет значения для инициализатора объекта, пока Prop доступен для записи - он не назначает новый Bar. Это задокументировано в проекте спецификации, поэтому он должен вернуться к C# 6. - person Jeroen Mostert; 22.11.2018
comment
@MichaWiedenmann: { Bar = new Bar { Prop = ... } } — это инициализатор объекта, который инициализирует свойство Bar вновь выделенным объектом Bar, поле или свойство которого Prop инициализируется. Это не удастся, если Bar доступен только для чтения (во всех языковых версиях). { Bar = { Prop = ... } } — это инициализатор вложенных объектов, который инициализирует поле Prop или свойство Bar, которое будет работать, даже если Bar доступно только для чтения. Оба они являются инициализаторами объектов (хотя и с разной семантикой), а не инициализаторами коллекций. - person Jeroen Mostert; 22.11.2018
comment
@JeroenMostert Инициализатор вложенных объектов TIL. Спасибо за терпеливость. - person Micha Wiedenmann; 22.11.2018