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

124

Есть ли причина предоставлять внутреннюю коллекцию как 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 достаточно, то его следует использовать. Это правильный способ рассуждать об этом или я что-то упускаю?

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

Эрик Оджебо
источник
6
Если вы используете .NET 4.5, вы можете попробовать новые интерфейсы сбора только для чтения . Вы все равно захотите обернуть возвращенную коллекцию в, ReadOnlyCollectionесли вы параноик, но теперь вы не привязаны к конкретной реализации.
StriplingWarrior

Ответы:

96

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

Если вам не нужно, чтобы внутренняя коллекция была изменяемой, вы можете использовать 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;
    }
}
Джон Скит
источник
15
Обратите внимание, что это снижает производительность: AFAIK метод Enumerable.Count оптимизирован для коллекций, переданных в IEnumerables, но не для диапазонов, созданных Skip (0)
shojtsy
6
-1 Это только у меня или это ответ на другой вопрос? Полезно знать это «решение», но оператор попросил сравнить между возвратом ReadOnlyCollection и IEnumerable. В этом ответе уже предполагается, что вы хотите вернуть IEnumerable без каких-либо оснований для поддержки этого решения.
Зайд Масуд
1
Ответ показывает приведение ReadOnlyCollectionк ICollectionи Addуспешное выполнение. Насколько я знаю, это невозможно. evil.Add(...)Должен выдавать ошибку. dotnetfiddle.net/GII2jW
Шон Латтин
1
@ShaunLuttin: Да, именно так - это бросает. Но в моем ответе, я не бросил ReadOnlyCollection- я бросил , IEnumerable<T>который на самом деле не только для чтения ... это вместо того , чтобы разоблачать вReadOnlyCollection
Джон Скит
1
Ага. Ваша паранойя заставит вас использовать a ReadOnlyCollectionвместо a IEnumerable, чтобы защититься от злых заклинаний.
Шон Латтин,
43

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

foreach (Foo f in bar.Foos)

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

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

Foo f = bar.Foos[17];

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

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

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

Стю Маккеллар
источник
14
Я категорически не согласен с таким «параноидальным дизайном». Я считаю плохой практикой выбирать неадекватную семантику для обеспечения соблюдения политики.
Воислав Стойкович
24
Если вы не согласны, это не значит, что это неправильно. Если я разрабатываю библиотеку для внешнего распространения, я бы предпочел иметь параноидальный интерфейс, чем разбираться с ошибками, поднятыми пользователями, которые пытаются злоупотреблять API. См. Рекомендации по проектированию C # на msdn.microsoft.com/en-us/library/k2604h5s(VS.71).aspx
Стю Маккеллар,
5
Да, в конкретном случае разработки библиотеки для внешнего распространения имеет смысл иметь параноидальный интерфейс для всего, что вы раскрываете. Даже в этом случае, если семантика свойства Foos является последовательным доступом, используйте ReadOnlyCollection, а затем верните IEnumerable. Не нужно использовать неправильную семантику;)
Воислав Стойкович
9
Тебе тоже не нужно этого делать. Вы можете вернуть итератор, доступный только для чтения, без копирования ...
Джон Скит,
1
@shojtsy Кажется, ваша строка кода не работает. as генерирует нуль . Приведение вызывает исключение.
Ник Алексеев
3

Я стараюсь максимально избегать использования 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());
Джеймс Мэдисон
источник
15
Преждевременная оптимизация. По моим измерениям, перебор ReadOnlyCollection занимает примерно 36% больше времени, чем обычный список, если вы ничего не делаете внутри цикла for. Скорее всего, вы никогда не заметите этой разницы, но если это горячая точка в вашем коде и требует максимальной производительности, которую вы можете выжать из него, почему бы вместо этого не использовать массив? Так было бы еще быстрее.
StriplingWarrior
Ваш тест содержит недостатки, вы полностью игнорируете предсказание ветвлений.
Trojaner
0

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


источник