ReadOnlyCollection oder IEnumerable zum Anzeigen von Mitgliedssammlungen?

124

Gibt es einen Grund, eine interne Sammlung als ReadOnlyCollection und nicht als IEnumerable verfügbar zu machen, wenn der aufrufende Code nur die Sammlung durchläuft?

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

Aus meiner Sicht ist IEnumerable eine Teilmenge der Schnittstelle von ReadOnlyCollection und erlaubt dem Benutzer nicht, die Sammlung zu ändern. Wenn also die IEnumberable-Schnittstelle ausreicht, ist dies die zu verwendende. Ist das eine richtige Art, darüber nachzudenken, oder fehlt mir etwas?

Danke / Erik

Erik Öjebo
quelle
6
Wenn Sie .NET 4.5 verwenden, möchten Sie möglicherweise die neuen schreibgeschützten Erfassungsschnittstellen ausprobieren . Sie möchten die zurückgegebene Sammlung weiterhin in eine ReadOnlyCollectionumschließen, wenn Sie paranoid sind, aber jetzt sind Sie nicht an eine bestimmte Implementierung gebunden.
StriplingWarrior

Antworten:

96

Modernere Lösung

Sofern die interne Sammlung nicht veränderbar sein muss, können Sie das System.Collections.ImmutablePaket verwenden, Ihren Feldtyp in eine unveränderliche Sammlung ändern und diese dann direkt verfügbar machen - vorausgesetzt Foonatürlich, sie ist unveränderlich.

Die Antwort wurde aktualisiert, um die Frage direkter zu beantworten

Gibt es einen Grund, eine interne Sammlung als ReadOnlyCollection und nicht als IEnumerable verfügbar zu machen, wenn der aufrufende Code nur die Sammlung durchläuft?

Es hängt davon ab, wie sehr Sie dem aufrufenden Code vertrauen. Wenn Sie die vollständige Kontrolle über alles haben, was dieses Mitglied jemals anrufen wird, und Sie garantieren, dass kein Code jemals verwendet wird:

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

Dann wird sicher kein Schaden angerichtet, wenn Sie die Sammlung direkt zurückgeben. Ich versuche im Allgemeinen, ein bisschen paranoider zu sein.

Ebenso, wie Sie sagen: Wenn Sie nur brauchen IEnumerable<T> , warum binden Sie sich dann an etwas Stärkeres?

Ursprüngliche Antwort

Wenn Sie .NET 3.5 verwenden, können Sie das Erstellen einer Kopie und die einfache Umwandlung vermeiden, indem Sie einen einfachen Aufruf von Überspringen verwenden:

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

(Es gibt viele andere Optionen für das einfache Umbrechen - das Schöne an SkipSelect / Where ist, dass es keinen Delegaten gibt, der für jede Iteration sinnlos ausgeführt werden kann.)

Wenn Sie .NET 3.5 nicht verwenden, können Sie einen sehr einfachen Wrapper schreiben, um dasselbe zu tun:

public static IEnumerable<T> Wrapper<T>(IEnumerable<T> source)
{
    foreach (T element in source)
    {
        yield return element;
    }
}
Jon Skeet
quelle
15
Beachten Sie, dass dies eine Leistungsbeeinträchtigung darstellt: AFAIK Die Enumerable.Count-Methode ist für Sammlungen optimiert, die in IEnumerables umgewandelt wurden, jedoch nicht für Bereiche, die von Skip (0)
shojtsy
6
-1 Bin ich es nur oder ist dies eine Antwort auf eine andere Frage? Es ist nützlich, diese "Lösung" zu kennen, aber die Operation hat um einen Vergleich zwischen der Rückgabe einer ReadOnlyCollection und IEnumerable gebeten. In dieser Antwort wird bereits davon ausgegangen, dass Sie IEnumerable ohne Begründung zurückgeben möchten, um diese Entscheidung zu unterstützen.
Zaid Masud
1
Die Antwort zeigt, wie ein a ReadOnlyCollectionin ein umgewandelt ICollectionund dann Adderfolgreich ausgeführt wird. Soweit ich weiß, sollte das nicht möglich sein. Das evil.Add(...)sollte einen Fehler auslösen. dotnetfiddle.net/GII2jW
Shaun Luttin
1
@ShaunLuttin: Ja genau - das wirft. Aber in meiner Antwort besetze ich keine ReadOnlyCollection- ich besetze eine, IEnumerable<T>die eigentlich nicht schreibgeschützt ist ... es ist, anstatt eine zu entlarvenReadOnlyCollection
Jon Skeet
1
Aha. Ihre Paranoia würde Sie dazu bringen, ein ReadOnlyCollectionanstelle eines IEnumerablezu verwenden, um sich vor dem bösen Casting zu schützen.
Shaun Luttin
43

Wenn Sie nur die Sammlung durchlaufen müssen:

foreach (Foo f in bar.Foos)

dann reicht es aus, IEnumerable zurückzugeben .

Wenn Sie zufälligen Zugriff auf Elemente benötigen:

Foo f = bar.Foos[17];

Wickeln Sie es dann in ReadOnlyCollection ein .

Vojislav Stojkovic
quelle
@Vojislav Stojkovic, +1. Gilt dieser Rat noch heute?
w0051977
1
@ w0051977 Es hängt von deiner Absicht ab. Die Verwendung System.Collections.Immutablegemäß den Empfehlungen von Jon Skeet ist absolut gültig, wenn Sie etwas verfügbar machen, das sich nie ändern wird, nachdem Sie einen Verweis darauf erhalten haben. Wenn Sie dagegen eine schreibgeschützte Ansicht einer veränderlichen Sammlung verfügbar machen, ist dieser Rat weiterhin gültig, obwohl ich ihn wahrscheinlich als verfügbar machen würde IReadOnlyCollection(was nicht existierte, als ich dies schrieb).
Vojislav Stojkovic
@VojislavStojkovic sie können nicht bar.Foos[17]mit IReadOnlyCollection. Der einzige Unterschied ist zusätzliches CountEigentum. public interface IReadOnlyCollection<out T> : IEnumerable<T>, IEnumerable
Konrad
@Konrad Richtig, wenn Sie brauchen bar.Foos[17], müssen Sie es als aussetzen ReadOnlyCollection<Foo>. Wenn Sie die Größe kennen und durchlaufen möchten, legen Sie sie als offen IReadOnlyCollection<Foo>. Wenn Sie die Größe nicht kennen müssen, verwenden Sie IEnumerable<Foo>. Es gibt andere Lösungen und andere Details, aber es würde den Rahmen der Diskussion dieser speziellen Antwort sprengen.
Vojislav Stojkovic
31

Wenn Sie dies tun, hindert nichts Ihre Anrufer daran, die IEnumerable zurück in ICollection zu übertragen und sie dann zu ändern. ReadOnlyCollection entfernt diese Möglichkeit, obwohl es weiterhin möglich ist, über Reflektion auf die zugrunde liegende beschreibbare Sammlung zuzugreifen. Wenn die Sammlung klein ist, können Sie dieses Problem sicher und einfach umgehen, indem Sie stattdessen eine Kopie zurückgeben.

Stu Mackellar
quelle
14
Das ist die Art von "paranoidem Design", mit der ich nicht einverstanden bin. Ich denke, es ist eine schlechte Praxis, eine unzureichende Semantik zu wählen, um eine Richtlinie durchzusetzen.
Vojislav Stojkovic
24
Sie nicht einverstanden zu machen macht es nicht falsch. Wenn ich eine Bibliothek für die externe Verteilung entwerfe, hätte ich lieber eine paranoide Oberfläche als Fehler, die von Benutzern behoben werden, die versuchen, die API zu missbrauchen. Siehe die C # -Design-Richtlinien unter msdn.microsoft.com/en-us/library/k2604h5s(VS.71).aspx
Stu Mackellar
5
Ja, im speziellen Fall des Entwurfs einer Bibliothek für die externe Verteilung ist es sinnvoll, eine paranoide Schnittstelle für alles zu haben, was Sie verfügbar machen. Wenn die Semantik der Foos-Eigenschaft ein sequentieller Zugriff ist, verwenden Sie ReadOnlyCollection und geben Sie dann IEnumerable zurück. Keine Notwendigkeit, falsche Semantik zu verwenden;)
Vojislav Stojkovic
9
Sie müssen auch nicht tun. Sie können einen schreibgeschützten Iterator zurückgeben, ohne ihn zu kopieren ...
Jon Skeet
1
@shojtsy Ihre Codezeile scheint nicht zu funktionieren. as erzeugt eine Null . Eine Besetzung löst eine Ausnahme aus.
Nick Alexeev
3

Ich vermeide es so oft wie möglich, ReadOnlyCollection zu verwenden. Es ist tatsächlich erheblich langsamer als nur die Verwendung einer normalen Liste. Siehe dieses Beispiel:

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
quelle
15
Vorzeitige Optimierung. Nach meinen Messungen dauert das Iterieren über eine ReadOnlyCollection ungefähr 36% länger als eine reguläre Liste, vorausgesetzt, Sie tun nichts in der for-Schleife. Wahrscheinlich werden Sie diesen Unterschied nie bemerken, aber wenn dies ein Hotspot in Ihrem Code ist und die letzte Leistung erfordert, die Sie daraus ziehen können, warum nicht stattdessen ein Array verwenden? Das wäre noch schneller.
StriplingWarrior
Ihr Benchmark enthält Fehler, Sie ignorieren die Verzweigungsvorhersage vollständig.
Trojaner
0

Manchmal möchten Sie möglicherweise eine Schnittstelle verwenden, möglicherweise, weil Sie die Sammlung während des Komponententests verspotten möchten. In meinem Blogeintrag finden Sie Informationen zum Hinzufügen Ihrer eigenen Benutzeroberfläche zu ReadonlyCollection mithilfe eines Adapters.


quelle