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

Спасибо/Erik

Ответ 1

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

Если вам не нужна внутренняя коллекция, которая может быть изменчивой, вы можете использовать пакет 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 is that no delegate, чтобы выполнить бессмысленно для каждой итерации.)

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

public static IEnumerable<T> Wrapper<T>(IEnumerable<T> source)
{
    foreach (T element in source)
    {
        yield return element;
    }
}

Ответ 2

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

foreach (Foo f in bar.Foos)

после чего возвращается IEnumerable.

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

Foo f = bar.Foos[17];

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

Ответ 3

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

Ответ 4

Я избегаю использовать 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());

Ответ 5

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