ReadOnlyCollection lub IEnumerable do ujawniania kolekcji elementów członkowskich?

124

Czy istnieje jakiś powód, aby uwidaczniać kolekcję wewnętrzną jako ReadOnlyCollection, a nie IEnumerable, jeśli kod wywołujący wykonuje iterację tylko po kolekcji?

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);

Jak widzę, IEnumerable jest podzbiorem interfejsu ReadOnlyCollection i nie pozwala użytkownikowi modyfikować kolekcji. Więc jeśli interfejs IEnumberable jest wystarczający, to jest ten, którego należy użyć. Czy to właściwy sposób rozumowania, czy czegoś mi brakuje?

Dzięki / Erik

Erik Öjebo
źródło
6
Jeśli używasz platformy .NET 4.5, możesz wypróbować nowe interfejsy kolekcji tylko do odczytu . Nadal będziesz chciał opakować zwróconą kolekcję w, ReadOnlyCollectionjeśli masz paranoję, ale teraz nie jesteś przywiązany do konkretnej implementacji.
StriplingWarrior

Odpowiedzi:

96

Bardziej nowoczesne rozwiązanie

O ile nie potrzebujesz, aby kolekcja wewnętrzna była zmienna, możesz użyć System.Collections.Immutablepakietu, zmienić typ pola na niezmienną kolekcję, a następnie ujawnić to bezpośrednio - Foooczywiście zakładając , że jest niezmienny.

Zaktualizowana odpowiedź, aby bardziej bezpośrednio odpowiedzieć na pytanie

Czy istnieje jakiś powód, aby uwidaczniać kolekcję wewnętrzną jako ReadOnlyCollection, a nie IEnumerable, jeśli kod wywołujący wykonuje iterację tylko po kolekcji?

To zależy od tego, jak bardzo ufasz kodowi dzwoniącemu. Jeśli masz pełną kontrolę nad wszystkim, co kiedykolwiek zadzwoni do tego członka i gwarantujesz, że żaden kod nigdy nie użyje:

ICollection<Foo> evil = (ICollection<Foo>) bar.Foos;
evil.Add(...);

to z pewnością nic się nie stanie, jeśli po prostu zwrócisz kolekcję bezpośrednio. Jednak generalnie staram się być trochę bardziej paranoikiem.

Podobnie, jak mówisz: jeśli tylko potrzebujesz IEnumerable<T> , to po co przywiązywać się do czegoś mocniejszego?

Oryginalna odpowiedź

Jeśli używasz .NET 3.5, możesz uniknąć tworzenia kopii i prostego rzutowania, używając prostego wywołania Skip:

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

(Istnieje wiele innych opcji zawijania w trywialny sposób - fajną rzeczą Skipw przypadku opcji Select / Where jest to, że nie ma delegata do bezcelowego wykonywania dla każdej iteracji).

Jeśli nie używasz .NET 3.5, możesz napisać bardzo prosty wrapper, który zrobi to samo:

public static IEnumerable<T> Wrapper<T>(IEnumerable<T> source)
{
    foreach (T element in source)
    {
        yield return element;
    }
}
Jon Skeet
źródło
15
Zauważ, że wiąże się to z obniżeniem wydajności: AFAIK metoda Enumerable.Count jest zoptymalizowana pod kątem kolekcji rzutowanych na IEnumerables, ale nie dla zakresów utworzonych przez Skip (0)
shojtsy
6
-1 Czy to tylko ja, czy jest to odpowiedź na inne pytanie? Warto znać to „rozwiązanie”, ale operator poprosił o porównanie między zwracaniem ReadOnlyCollection i IEnumerable. W tej odpowiedzi założono już, że chcesz zwrócić IEnumerable bez uzasadnienia tej decyzji.
Zaid Masud
1
Odpowiedź pokazuje, że przesyłano a ReadOnlyCollectiondo an, ICollectiona następnie działano Addpomyślnie. O ile wiem, nie powinno to być możliwe. evil.Add(...)Powinny rzucać się błąd. dotnetfiddle.net/GII2jW
Shaun Luttin
1
@ShaunLuttin: Tak, dokładnie - to rzuca. Ale w mojej odpowiedzi nie przesyłam ReadOnlyCollection- rzucam, IEnumerable<T>który w rzeczywistości nie jest tylko do odczytu ... zamiast ujawniaćReadOnlyCollection
Jon Skeet
1
Aha. Twoja paranoja sprawiłaby, że użyjesz a ReadOnlyCollectionzamiast an IEnumerable, aby chronić się przed rzucaniem zła.
Shaun Luttin
43

Jeśli potrzebujesz tylko iterować po kolekcji:

foreach (Foo f in bar.Foos)

wtedy wystarczy zwrócić IEnumerable .

Jeśli potrzebujesz losowego dostępu do przedmiotów:

Foo f = bar.Foos[17];

następnie zawiń go w ReadOnlyCollection .

Vojislav Stojkovic
źródło
@Vojislav Stojkovic, +1. Czy ta rada ma nadal zastosowanie?
w0051977
1
@ w0051977 To zależy od twojego zamiaru. Używanie System.Collections.Immutablezgodnie z zaleceniami Jona Skeeta jest całkowicie uzasadnione, gdy ujawniasz coś, co nigdy się nie zmieni po uzyskaniu do niego odniesienia. Z drugiej strony, jeśli ujawniasz widok tylko do odczytu zbioru zmiennego, to ta rada jest nadal ważna, chociaż prawdopodobnie ujawniłbym ją jako IReadOnlyCollection(która nie istniała, kiedy to pisałem).
Vojislav Stojkovic
@VojislavStojkovic nie można korzystać bar.Foos[17]z IReadOnlyCollection. Jedyną różnicą jest dodatkowa Countnieruchomość. public interface IReadOnlyCollection<out T> : IEnumerable<T>, IEnumerable
Konrad
@Konrad Dobrze, jeśli potrzebujesz bar.Foos[17], musisz to ujawnić jako ReadOnlyCollection<Foo>. Jeśli chcesz poznać rozmiar i iterować przez niego, wyświetl go jako IReadOnlyCollection<Foo>. Jeśli nie musisz znać rozmiaru, użyj IEnumerable<Foo>. Istnieją inne rozwiązania i inne szczegóły, ale wykraczają one poza zakres dyskusji na temat tej konkretnej odpowiedzi.
Vojislav Stojkovic
31

Jeśli to zrobisz, nic nie powstrzyma twoich rozmówców przed rzutowaniem IEnumerable z powrotem do ICollection, a następnie zmodyfikowaniem go. ReadOnlyCollection usuwa tę możliwość, chociaż nadal można uzyskać dostęp do podstawowej zapisywalnej kolekcji za pośrednictwem odbicia. Jeśli kolekcja jest niewielka, bezpiecznym i łatwym sposobem obejścia tego problemu jest zwrócenie kopii.

Stu Mackellar
źródło
14
To rodzaj „paranoicznego projektu”, z którym serdecznie się nie zgadzam. Uważam, że wybieranie nieodpowiedniej semantyki w celu egzekwowania polityki jest złą praktyką.
Vojislav Stojkovic
24
To, że się nie zgadzasz, nie czyni tego źle. Jeśli projektuję bibliotekę do dystrybucji zewnętrznej, wolałbym raczej mieć paranoiczny interfejs niż zajmować się błędami zgłaszanymi przez użytkowników, którzy próbują niewłaściwie używać API. Zobacz wytyczne dotyczące projektowania w języku C # na stronie msdn.microsoft.com/en-us/library/k2604h5s(VS.71).aspx
Stu Mackellar
5
Tak, w konkretnym przypadku projektowania biblioteki do zewnętrznej dystrybucji sensowne jest posiadanie paranoicznego interfejsu dla wszystkiego, co ujawniasz. Mimo to, jeśli semantyka właściwości Foos to dostęp sekwencyjny, użyj ReadOnlyCollection, a następnie zwróć IEnumerable. Nie trzeba używać złej semantyki;)
Vojislav Stojkovic
9
Ty też nie musisz tego robić. Możesz zwrócić iterator tylko do odczytu bez kopiowania ...
Jon Skeet
1
@shojtsy Twój wiersz kodu nie działa. as generuje wartość null . Rzut rzuca wyjątek.
Nick Alexeev
3

Unikam używania ReadOnlyCollection tak bardzo, jak to możliwe, w rzeczywistości jest to znacznie wolniejsze niż zwykłe używanie zwykłej listy. Zobacz ten przykład:

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());
James Madison
źródło
15
Przedwczesna optymalizacja. Według moich pomiarów iteracja po ReadOnlyCollection trwa około 36% dłużej niż zwykła lista, zakładając, że nic nie robisz w pętli for. Są szanse, że nigdy nie zauważysz tej różnicy, ale jeśli jest to gorący punkt w Twoim kodzie i wymaga każdego ostatniego kawałka wydajności, który możesz wycisnąć, dlaczego nie użyć zamiast tego Array? To byłoby jeszcze szybsze.
StriplingWarrior
Twój benchmark zawiera wady, całkowicie ignorujesz przewidywanie gałęzi.
Trojaner
0

Czasami możesz chcieć użyć interfejsu, być może dlatego, że chcesz mockować kolekcję podczas testów jednostkowych. Zobacz mój wpis na blogu, aby dodać własny interfejs do ReadonlyCollection za pomocą adaptera.


źródło