Best Practices Rückgabe von schreibgeschützten Objekten

11

Ich habe eine Frage zu "Best Practices" zu OOP in C # (diese gilt jedoch für alle Sprachen).

Erwägen Sie, eine Bibliotheksklasse mit einem Objekt zu haben, das öffentlich zugänglich gemacht werden soll, z. B. über den Eigenschaften-Accessor. Wir möchten jedoch nicht, dass die Öffentlichkeit (Benutzer dieser Bibliotheksklasse) dies ändert.

class A
{
    // Note: List is just example, I am interested in objects in general.
    private List<string> _items = new List<string>() { "hello" }

    public List<string> Items
    {
        get
        {
            // Option A (not read-only), can be modified from outside:
            return _items;

            // Option B (sort-of read-only):
            return new List<string>( _items );

            // Option C, must change return type to ReadOnlyCollection<string>
            return new ReadOnlyCollection<string>( _items );
        }
    }
}

Offensichtlich ist der beste Ansatz "Option C", aber nur sehr wenige Objekte haben die ReadOnly-Variante (und sicherlich keine benutzerdefinierten Klassen haben sie).

Wenn Sie der Benutzer der Klasse A wären, würden Sie Änderungen an erwarten

List<string> someList = ( new A() ).Items;

auf ursprüngliches (A) Objekt übertragen? Oder ist es in Ordnung, einen Klon zurückzugeben, sofern er in Kommentaren / Dokumentationen so geschrieben wurde? Ich denke, dass dieser Klonansatz zu ziemlich schwer zu verfolgenden Fehlern führen könnte.

Ich erinnere mich, dass wir in C ++ const-Objekte zurückgeben konnten und Sie nur Methoden aufrufen konnten, die als const darauf markiert waren. Ich denke, es gibt keine solche Funktion / Muster in der C #? Wenn nicht, warum würden sie es nicht aufnehmen? Ich glaube, es heißt konstante Korrektheit.

Andererseits ist meine Hauptfrage "Was würden Sie erwarten" oder wie man mit Option A gegen B umgeht.

Höre nie auf zu lernen
quelle
2
Schauen Sie sich diesen Artikel über diese Vorschau neuer unveränderlicher Sammlungen für .NET 4.5 an: blogs.msdn.com/b/bclteam/archive/2012/12/18/… Sie können sie bereits über NuGet erhalten.
Kristof Claes

Antworten:

10

Neben der Antwort von @ Jesse, die die beste Lösung für Sammlungen ist: Die allgemeine Idee besteht darin, Ihre öffentliche Schnittstelle von A so zu gestalten, dass sie nur zurückkehrt

  • Werttypen (wie int, double, struct)

  • unveränderliche Objekte (wie "Zeichenfolge" oder benutzerdefinierte Klassen, die nur schreibgeschützte Methoden anbieten, genau wie Ihre Klasse A)

  • (nur wenn Sie müssen): Objektkopien, wie Ihre "Option B" zeigt

IEnumerable<immutable_type_here> ist an sich unveränderlich, daher ist dies nur ein Sonderfall der oben genannten.

Doc Brown
quelle
19

Beachten Sie auch, dass Sie auf Schnittstellen programmieren sollten. Im Großen und Ganzen möchten Sie von dieser Eigenschaft eine zurückgeben IEnumerable<string>. A List<string>ist ein bisschen ein Nein-Nein, weil es im Allgemeinen veränderlich ist und ich werde so weit gehen zu sagen, dass es ReadOnlyCollection<string>als Implementierer von ICollection<string>Gefühlen das Liskov-Substitutionsprinzip verletzt.

Jesse C. Slicer
quelle
6
+1, mit a IEnumerable<string>ist genau das, was man hier will.
Doc Brown
2
In .Net 4.5 können Sie auch verwenden IReadOnlyList<T>.
Svick
3
Hinweis für andere Interessenten. Wenn Sie den Eigenschafts-Accessor in Betracht ziehen: Wenn Sie nur IEnumerable <string> anstelle von List <string> zurückgeben, indem Sie _list (+ implizite Umwandlung in IEnumerable) ausführen, kann diese Liste wieder in List <string> umgewandelt und geändert werden, und die Änderungen werden geändert sich in das Hauptobjekt ausbreiten. Sie können eine neue Liste <string> (_list) (+ nachfolgende implizite Umwandlung in IEnumerable) zurückgeben, um die interne Liste zu schützen. : Mehr dazu finden Sie hier stackoverflow.com/questions/4056013/...
NeverStopLearning
1
Ist es besser zu verlangen, dass jeder Empfänger einer Sammlung, der per Index darauf zugreifen muss, bereit ist, die Daten in einen Typ zu kopieren, der dies erleichtert, oder ist es besser zu verlangen, dass der Code, der die Sammlung zurückgibt, sie in einer Form bereitstellt, die ist über den Index zugänglich? Sofern der Lieferant es nicht wahrscheinlich in einer Form hat, die den Zugriff per Index nicht erleichtert, würde ich davon ausgehen, dass der Wert der Befreiung der Empfänger von der Notwendigkeit, eine eigene redundante Kopie anzufertigen, den Wert der Befreiung des Lieferanten von der Notwendigkeit der Lieferung übersteigt ein Formular mit wahlfreiem Zugriff (z. B. schreibgeschützt IList<T>)
Supercat
2
Eine Sache, die ich an IEnumerable nicht mag, ist, dass Sie nie wissen, ob es als Coroutine ausgeführt wird oder ob es sich nur um ein Datenobjekt handelt. Wenn es als Coroutine ausgeführt wird, kann es zu subtilen Fehlern kommen, daher ist dies manchmal gut zu wissen. Aus diesem Grund bevorzuge ich ReadOnlyCollection oder IReadOnlyList usw.
Steve Vermeulen
3

Ich bin vor einiger Zeit auf ein ähnliches Problem gestoßen, und unten war meine Lösung, aber es funktioniert wirklich nur, wenn Sie auch die Bibliothek steuern:


Beginnen Sie mit Ihrer Klasse A

public class A
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

Leiten Sie dann daraus eine Schnittstelle mit nur dem get-Teil der Eigenschaften (und allen Lesemethoden) ab und lassen Sie A diese implementieren

public interface IReadOnlyA
{
    public int N { get; }
}
public class A : IReadOnlyA
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

Wenn Sie dann die schreibgeschützte Version verwenden möchten, ist natürlich eine einfache Besetzung ausreichend. Dies ist genau das, was Ihre Lösung C ist, aber ich dachte, ich würde die Methode anbieten, mit der ich sie erreicht habe

Andy Hunt
quelle
2
Das Problem dabei ist, dass es möglich ist, es auf eine veränderbare Version zurückzusetzen. Und seine Verletzung von LSP, weil der Zustand, der durch die Methoden der Basisklasse (in diesem Fall der Schnittstelle) beobachtet werden kann, durch neue Methoden aus der Unterklasse geändert werden kann. Aber es kommuniziert zumindest die Absicht, auch wenn es sie nicht erzwingt.
user470365
1

Für alle, die diese Frage finden und dennoch an der Antwort interessiert sind - jetzt gibt es eine andere Option, die offen legt ImmutableArray<T>. Dies sind einige Punkte, warum ich denke, dass es besser ist als IEnumerable<T>oder ReadOnlyCollection<T>:

  • es gibt eindeutig an, dass es unveränderlich ist (expressive API)
  • Es wird klargestellt, dass die Sammlung bereits gebildet wurde (mit anderen Worten, sie ist nicht träge initialisiert und es ist in Ordnung, sie mehrmals zu durchlaufen).
  • wenn die Anzahl der Elemente bekannt ist, ist es nicht verbergen , dass WISSEN wie IEnumerable<T>tut
  • Da der Kunde nicht weiß, was die Implementierung ist, IEnumerableoder IReadOnlyCollectnicht davon ausgehen kann, dass sie sich nie ändert (mit anderen Worten, es gibt keine Garantie dafür, dass Elemente einer Sammlung nicht geändert wurden, während der Verweis auf diese Sammlung unverändert bleibt).

Auch wenn APi den Client über Änderungen in der Sammlung benachrichtigen muss, würde ich ein Ereignis als separates Mitglied verfügbar machen.

Beachten Sie, dass ich nicht sage, dass IEnumerable<T>das im Allgemeinen schlecht ist. Ich würde es immer noch verwenden, wenn ich die Sammlung als Argument übergebe oder eine träge initialisierte Sammlung zurückgebe (in diesem speziellen Fall ist es sinnvoll, die Anzahl der Elemente auszublenden, da dies noch nicht bekannt ist).

Pavels Ahmadulins
quelle