ReadOnlyCollection или IEnumerable для отображения коллекций членов?

Есть ли причина предоставлять внутреннюю коллекцию как ReadOnlyCollection, а не как IEnumerable, если вызывающий код выполняет только итерацию по коллекции?

class Bar
{
    private ICollection<Foo> foos;

    // Which one is to be preferred?
    public IEnumerable<Foo> Foos { ... }
    public ReadOnlyCollection<Foo> Foos { ... }
}


// Calling code:

foreach (var f in bar.Foos)
    DoSomething(f);

Насколько я понимаю, IEnumerable является подмножеством интерфейса ReadOnlyCollection и не позволяет пользователю изменять коллекцию. Итак, если интерфейса IEnumberable достаточно, то его следует использовать. Это правильный способ рассуждать об этом или я что-то упускаю?

Спасибо / Эрик


person Erik Öjebo    schedule 29.01.2009    source источник
comment
Если вы используете .NET 4.5, вы можете попробовать новый read- только интерфейсы коллекции. Вы все равно захотите обернуть возвращенную коллекцию в ReadOnlyCollection, если вы параноик, но теперь вы не привязаны к конкретной реализации.   -  person StriplingWarrior    schedule 13.11.2012


Ответы (5)


Более современное решение

Если вам не нужно, чтобы внутренняя коллекция была изменяемой, вы можете использовать пакет System.Collections.Immutable. измените тип поля на неизменяемую коллекцию, а затем выставьте ее напрямую - конечно, при условии, что Foo сам неизменяемый.

Обновленный ответ для более прямого ответа на вопрос

Есть ли причина предоставлять внутреннюю коллекцию как ReadOnlyCollection, а не как IEnumerable, если вызывающий код выполняет только итерацию по коллекции?

Это зависит от того, насколько вы доверяете вызывающему коду. Если вы полностью контролируете все, что когда-либо будет вызывать этого члена, и гарантируете, что никакой код никогда не будет использовать:

ICollection<Foo> evil = (ICollection<Foo>) bar.Foos;
evil.Add(...);

Тогда конечно, ничего страшного не будет, если вы просто вернете коллекцию напрямую. Я вообще стараюсь быть немного более параноиком, чем это.

Точно так же, как вы говорите: если вам только нужно IEnumerable<T>, тогда зачем привязывать себя к чему-то более сильному?

Исходный ответ

Если вы используете .NET 3.5, вы можете избежать создания копии и избежать простого приведения, используя простой вызов Skip:

public IEnumerable<Foo> Foos {
    get { return foos.Skip(0); }
}

(Есть много других вариантов тривиального обертывания - хорошая вещь в Skip над Select / Where заключается в том, что нет делегата, который бессмысленно выполнялся бы для каждой итерации.)

Если вы не используете .NET 3.5, вы можете написать очень простую оболочку, которая будет делать то же самое:

public static IEnumerable<T> Wrapper<T>(IEnumerable<T> source)
{
    foreach (T element in source)
    {
        yield return element;
    }
}
person Jon Skeet    schedule 29.01.2009
comment
Этот ответ просто позволил мне выполнить одно из лучших упрощений кода, которое я когда-либо писал. Спасибо. - person Chris Marasti-Georg; 19.11.2009
comment
Обратите внимание, что это снижает производительность: AFAIK метод Enumerable.Count оптимизирован для коллекций, переданных в IEnumerables, но не для диапазонов, созданных Skip (0) - person shojtsy; 02.02.2010
comment
Если обертывать List в класс, было бы лучше сделать то же самое или реализовать IEnumerable ‹T› и передать GetEnumerator () классу обертки? - person andleer; 25.07.2010
comment
@ Андрей: Это зависит от конкретной ситуации. Вы хотите, чтобы ваш класс-оболочка действовал как сама коллекция или просто содержал коллекцию? - person Jon Skeet; 25.07.2010
comment
@Jon, хороший вопрос. Я предполагаю, что в зависимости от того, каким образом кто-то хотел ответить на этот вопрос, любой подход может быть хорошим. В моем случае я думаю, что это сама коллекция, поэтому, если предположить, что передача GetEnumerator () является допустимым подходом, тогда я бы пошел именно так. Спасибо, - person andleer; 26.07.2010
comment
Я немного удивлен, что foos.Skip (0) не компилируется в тот же MSIL, что и foos.Select (x = ›x) ... Всегда ли использование делегата несет накладные расходы? - person silasdavis; 24.05.2012
comment
@silasdavis: Зачем компилятору знать о вызовах Skip и Select? Выполнение делегата всегда сопряжено с накладными расходами, хотя они, вероятно, будут минимальными. - person Jon Skeet; 24.05.2012
comment
-1 Это только у меня или это ответ на другой вопрос? Полезно знать это решение, но оператор попросил сравнить между возвратом ReadOnlyCollection и IEnumerable. В этом ответе уже предполагается, что вы хотите вернуть IEnumerable без каких-либо оснований для поддержки этого решения. - person Zaid Masud; 08.08.2012
comment
Ответ показывает преобразование ReadOnlyCollection в ICollection, а затем успешное выполнение Add. Насколько я знаю, это невозможно. evil.Add(...) должен выдать ошибку. dotnetfiddle.net/GII2jW - person Shaun Luttin; 26.10.2015
comment
@ShaunLuttin: Да, именно так - это бросает. Но в своем ответе я не использую ReadOnlyCollection - я использую IEnumerable<T>, который на самом деле не предназначен только для чтения ... это вместо отображения ReadOnlyCollection - person Jon Skeet; 26.10.2015
comment
Ага. Ваша паранойя заставит вас использовать ReadOnlyCollection вместо IEnumerable, чтобы защититься от злого заклинания. - person Shaun Luttin; 26.10.2015
comment
@ShaunLuttin: Вместо того, чтобы раскрывать лежащий в основе List<T> или что-то еще, да. - person Jon Skeet; 26.10.2015
comment
@JonSkeet - можно ли использовать foos.AsEnumerable () вместо foos.Skip (0)? - person Ananth; 07.08.2017
comment
@Ananth: Нет, потому что это не работает - возвращаемое значение все еще может быть возвращено к типу коллекции. - person Jon Skeet; 07.08.2017
comment
Учитывая популярность этого ответа, я задаюсь вопросом, стоит ли добавить к нему пару центов на System.Collections.Immutable? - person fuglede; 17.08.2017
comment
@fuglede: Я кратко об этом упомяну, но у меня есть ограниченное количество времени, чтобы отредактировать ответы 8-летней давности :) - person Jon Skeet; 17.08.2017
comment
@JonSkeet Я полагаю, что написание более 30000 из них, вероятно, не поможет облегчить задачу. В любом случае спасибо, ты герой! Обладание этими маленькими кусочками мудрости очень помогает тому, кто плохо знаком с фреймворком и пытается ориентироваться в менее очевидных его частях. - person fuglede; 17.08.2017
comment
@fuglede: Нет проблем - теперь определенно лучше :) - person Jon Skeet; 17.08.2017

Если вам нужно только перебрать коллекцию:

foreach (Foo f in bar.Foos)

тогда достаточно возврата IEnumerable.

Если вам нужен произвольный доступ к элементам:

Foo f = bar.Foos[17];

затем оберните его в ReadOnlyCollection.

person Vojislav Stojkovic    schedule 29.01.2009
comment
@ Воислав Стойкович, +1. Этот совет применим и сегодня? - person w0051977; 16.04.2018
comment
@ w0051977 Это зависит от ваших намерений. Использование System.Collections.Immutable, как рекомендует Джон Скит, совершенно справедливо, когда вы раскрываете то, что никогда не изменится после того, как вы получите ссылку на это. С другой стороны, если вы открываете доступное только для чтения представление изменяемой коллекции, тогда этот совет все еще актуален, хотя я, вероятно, выставил бы его как IReadOnlyCollection (которого не существовало, когда я писал это). - person Vojislav Stojkovic; 16.04.2018
comment
@VojislavStojkovic, вы не можете использовать bar.Foos[17] с IReadOnlyCollection. Единственное отличие - дополнительное свойство Count. public interface IReadOnlyCollection<out T> : IEnumerable<T>, IEnumerable - person Konrad; 13.06.2019
comment
@Konrad Верно, если вам нужно bar.Foos[17], вы должны выставить его как ReadOnlyCollection<Foo>. Если вы хотите узнать размер и перебрать его, укажите его как IReadOnlyCollection<Foo>. Если вам не нужно знать размер, используйте IEnumerable<Foo>. Есть другие решения и другие детали, но это выходит за рамки обсуждения этого конкретного ответа. - person Vojislav Stojkovic; 13.06.2019

Если вы сделаете это, то ничто не помешает вашим вызывающим абонентам вернуть IEnumerable обратно в ICollection и затем изменить его. ReadOnlyCollection устраняет эту возможность, хотя по-прежнему можно получить доступ к базовой записываемой коллекции через отражение. Если коллекция небольшая, то безопасный и простой способ обойти эту проблему - вместо этого вернуть копию.

person Stu Mackellar    schedule 29.01.2009
comment
Я категорически не согласен с таким параноидальным замыслом. Я считаю плохой практикой выбирать неадекватную семантику для обеспечения соблюдения политики. - person Vojislav Stojkovic; 29.01.2009
comment
Если вы не согласны, это не значит, что вы ошибаетесь. Если я разрабатываю библиотеку для внешнего распространения, я бы предпочел иметь параноидальный интерфейс, чем заниматься ошибками, поднятыми пользователями, которые пытаются злоупотребить API. См. Рекомендации по проектированию C # на странице msdn.microsoft.com/en -us / library / k2604h5s (VS.71) .aspx. - person Stu Mackellar; 29.01.2009
comment
Да, в конкретном случае разработки библиотеки для внешнего распространения имеет смысл иметь параноидальный интерфейс для всего, что вы раскрываете. Даже в этом случае, если семантика свойства Foos является последовательным доступом, используйте ReadOnlyCollection, а затем верните IEnumerable. Не нужно использовать неправильную семантику;) - person Vojislav Stojkovic; 29.01.2009
comment
Тебе тоже не нужно этого делать. Вы можете вернуть итератор, доступный только для чтения, без копирования ... - person Jon Skeet; 29.01.2009
comment
Вам не нужно отражение, чтобы вернуть базовую доступную для записи коллекцию из ReadOnlyCollection. Только (readCollection.GetEnumerator () как ICollection ‹Foo›). - person shojtsy; 02.02.2010
comment
@shojtsy Кажется, ваша строка кода не работает. as генерирует null. Приведение вызывает исключение. - person Nick Alexeev; 31.05.2011
comment
Я считаю, что правильный синтаксис readOnlyCollection.Items, который возвращает IList<T>, который можно изменить. - person MEMark; 07.09.2011
comment
Почему вы сказали, что ReadOnlyCollection устраняет эту возможность? ReadOnlyCollection явно реализовал ICollection ‹T›, поэтому ничто не мешает вам также назначить экземпляр ReadOnlyCollection переменной ICollect ‹T›? - person joe; 25.02.2021

Я стараюсь максимально избегать использования ReadOnlyCollection, это на самом деле значительно медленнее, чем использование обычного списка. См. Этот пример:

List<int> intList = new List<int>();
        //Use a ReadOnlyCollection around the List
        System.Collections.ObjectModel.ReadOnlyCollection<int> mValue = new System.Collections.ObjectModel.ReadOnlyCollection<int>(intList);

        for (int i = 0; i < 100000000; i++)
        {
            intList.Add(i);
        }
        long result = 0;

        //Use normal foreach on the ReadOnlyCollection
        TimeSpan lStart = new TimeSpan(System.DateTime.Now.Ticks);
        foreach (int i in mValue)
            result += i;
        TimeSpan lEnd = new TimeSpan(System.DateTime.Now.Ticks);
        MessageBox.Show("Speed(ms): " + (lEnd.TotalMilliseconds - lStart.TotalMilliseconds).ToString());
        MessageBox.Show("Result: " + result.ToString());

        //use <list>.ForEach
        lStart = new TimeSpan(System.DateTime.Now.Ticks);
        result = 0;
        intList.ForEach(delegate(int i) { result += i; });
        lEnd = new TimeSpan(System.DateTime.Now.Ticks);
        MessageBox.Show("Speed(ms): " + (lEnd.TotalMilliseconds - lStart.TotalMilliseconds).ToString());
        MessageBox.Show("Result: " + result.ToString());
person James Madison    schedule 10.06.2010
comment
Преждевременная оптимизация. По моим измерениям, перебор ReadOnlyCollection занимает примерно 36% больше времени, чем обычный список, если вы не делаете ничего внутри цикла for. Скорее всего, вы никогда не заметите этой разницы, но если это горячая точка в вашем коде и требует максимальной производительности, которую вы можете выжать из него, почему бы вместо этого не использовать массив? Так было бы еще быстрее. - person StriplingWarrior; 13.11.2012
comment
Ваш тест содержит недостатки, вы полностью игнорируете предсказание ветвления. - person Trojaner; 15.02.2019

Иногда вы можете захотеть использовать интерфейс, возможно, потому что вы хотите имитировать коллекцию во время модульного тестирования. См. Мою запись в блоге, чтобы добавить свой собственный интерфейс в ReadonlyCollection с помощью переходник.

person Community    schedule 11.02.2009