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