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, ви можете уникнути створення копії та уникнути простого перенесення, використовуючи простий виклик для пропуску:

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

(Існує безліч інших варіантів для завершення тривіального завершення - приємна річ щодо SkipOver / Select, де немає делегата, який би безглуздо виконувався для кожної ітерації.)

Якщо ви не використовуєте .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
У відповіді видно, що ви переймете "a" ReadOnlyCollectionна "", ICollectionа потім Addуспішно працює . Наскільки мені відомо, це не повинно бути можливим. evil.Add(...)Повинен видавати помилку. dotnetfiddle.net/GII2jW
Шон Луттін

1
@ShaunLuttin: Так, саме - це кидає. Але у своїй відповіді я не кидаю ReadOnlyCollection- я кидаю те, IEnumerable<T>що насправді не є лише для читання ... це замість викриттяReadOnlyCollection
Джон Скіт

1
Ага. Ваша параноїя призведе до того, щоб ви скористалися ReadOnlyCollectionзамість IEnumerableтого, щоб захиститись від злого кастингу.
Shaun Luttin

43

Якщо вам потрібно лише повторити колекцію:

foreach (Foo f in bar.Foos)

потім повертається IE Чимало досить.

Якщо вам потрібен випадковий доступ до предметів:

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
Stu Mackellar

5
Так, у конкретному випадку проектування бібліотеки для зовнішнього розповсюдження має сенс мати параноїдальний інтерфейс для того, що ви експонуєте. Тим не менш, якщо семантика властивості Foos є послідовним доступом, використовуйте ReadOnlyCollection, а потім повертайте IEnumerable. Не потрібно використовувати неправильну семантику;)
Воїслав Стойкович

9
Вам також не потрібно робити. Ви можете повернути ітератор, доступний лише для читання, не копіюючи ...
Джон Скіт

1
@shojtsy Здається, ваш рядок коду не працює. як генерує нуль . Акторський склад кидає виняток.
Нік Алексєєв

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

Ваш орієнтир містить недоліки, ви повністю ігноруєте передбачення галузей.
Троянер

0

Іноді ви можете використовувати інтерфейс, можливо, тому що ви хочете знущатися над колекцією під час тестування одиниць. Будь ласка, дивіться мій запис у щоденнику щодо додавання власного інтерфейсу до ReadonlyCollection за допомогою адаптера.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.