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 с помощью адаптера.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.