Wie gehe ich mit Setzern auf unveränderlichen Feldern um?

25

Ich habe eine Klasse mit zwei readonly intFeldern. Sie sind als Eigenschaften ausgesetzt:

public class Thing
{
    private readonly int _foo, _bar;

    /// <summary> I AM IMMUTABLE. </summary>
    public Thing(int foo, int bar)
    {
        _foo = foo;
        _bar = bar;
    }

    public int Foo { get { return _foo; } set { } }

    public int Bar { get { return _bar; } set { } }
}

Dies bedeutet jedoch, dass das Folgende vollkommen legal ist:

Thing iThoughtThisWasMutable = new Thing(1, 42);

iThoughtThisWasMutable.Foo = 99;  // <-- Poor, mistaken developer.
                                  //     He surely has bugs now. :-(

Die Fehler, die von der Annahme ausgehen, dass dies funktionieren würde, werden mit Sicherheit heimtückisch sein. Sicher, der falsche Entwickler sollte die Dokumentation gelesen haben. Das ändert aber nichts an der Tatsache, dass ihn kein Kompilierungs- oder Laufzeitfehler vor dem Problem gewarnt hat.

Wie sollte die ThingKlasse geändert werden, damit Entwickler mit geringerer Wahrscheinlichkeit den oben genannten Fehler machen?

Eine Ausnahme auslösen? Verwenden Sie eine Get-Methode anstelle einer Eigenschaft?

kdbanman
quelle
6
gnat
1
Danke, @gnat. Das Post (sowohl die Frage als auch die Antwort) scheint über Schnittstellen zu sprechen, wie es in Kapital I Interfaces. Ich bin nicht sicher, ob ich das tue.
kdbanman
6
@gnat, das ist ein schrecklicher Rat. Schnittstellen bieten eine großartige Möglichkeit, öffentlich unveränderliche VOs / DTOs zu bedienen, die noch leicht zu testen sind.
David Arno
4
@gnat, ich habe und die Frage macht keinen Sinn, da das OP zu denken scheint, dass Schnittstellen Unveränderlichkeit zerstören, was Unsinn ist.
David Arno
1
@gnat, bei dieser Frage ging es darum, Schnittstellen für DTOs zu deklarieren. Die am häufigsten gewählte Antwort war, dass Schnittstellen nicht schädlich, aber wahrscheinlich unnötig wären.
Paul Draper

Antworten:

107

Warum diesen Code legal machen?
Nehmen Sie das raus, set { }wenn es nichts tut.
So definieren Sie eine schreibgeschützte öffentliche Eigenschaft:

public int Foo { get { return _foo; } }
Paparazzo
quelle
3
Ich fand das aus irgendeinem Grund nicht legal, aber es scheint perfekt zu funktionieren! Perfekt, danke.
kdbanman
1
@kdbanman Es hat wahrscheinlich etwas mit dem zu tun, was die IDE an einem Punkt gemacht hat - wahrscheinlich mit der automatischen Vervollständigung.
Panzercrisis
2
@kdbanman; Was nicht legal ist (bis C # 5) ist public int Foo { get; }(Auto-Property mit nur einem Getter), aber public int Foo { get; private set; }ist.
Laurent LA RIZZA
@LaurentLARIZZA, du hast mein Gedächtnis durcheinander gebracht. Genau hierher kam meine Verwirrung.
kdbanman
45

In C # 5 und früheren Versionen standen uns zwei Optionen für unveränderliche Felder gegenüber, die über einen Getter verfügbar gemacht wurden:

  1. Erstellen Sie eine schreibgeschützte Sicherungsvariable und geben Sie diese über einen manuellen Getter zurück. Diese Option ist sicher (man muss die Option explizit entfernen readonly, um die Unveränderlichkeit zu zerstören. Es wurde jedoch viel Code für die Kesselplatte erstellt.
  2. Verwenden Sie eine Auto-Eigenschaft mit einem privaten Setter. Dies erzeugt einfacheren Code, es ist jedoch einfacher, die Unveränderlichkeit aus Versehen zu brechen.

Mit C # 6 (verfügbar in VS2015, das gestern veröffentlicht wurde) erhalten wir jetzt das Beste aus beiden Welten: schreibgeschützte automatische Eigenschaften. Dies ermöglicht es uns, die Klasse des OP zu vereinfachen, um:

public class Thing
{
    /// <summary> I AM IMMUTABLE. </summary>
    public Thing(int foo, int bar)
    {
        Foo = foo;
        Bar = bar;
    }

    public int Foo { get; }
    public int Bar { get; }
}

Die Zeilen Foo = foound Bar = barsind nur im Konstruktor gültig (der die robuste Nur-Lese-Anforderung erfüllt) und das Hintergrundfeld ist impliziert, anstatt explizit definiert zu werden (was den einfacheren Code ermöglicht).

David Arno
quelle
2
Und primäre Konstruktoren können dies noch weiter vereinfachen, indem sie den syntaktischen Aufwand des Konstruktors beseitigen.
Sebastian Redl
3
@SebastianRedl Leider wurden primäre Konstruktoren aus der endgültigen C # 6-Version entfernt .
Dan Lyons
1
@DanLyons Verdammt, ich freute mich darauf, dies in unserer gesamten Codebasis zu nutzen.
Sebastian Redl
12

Man könnte die Setter einfach loswerden. Sie tun nichts, sie werden die Benutzer verwirren und zu Fehlern führen. Sie können sie jedoch stattdessen als privat kennzeichnen und so die Sicherungsvariablen entfernen, was Ihren Code vereinfacht:

public class Thing
{
    public Thing(int foo, int bar)
    {
        Foo = foo;
        Bar = bar;
    }

    public int Foo { get; private set; }

    public int Bar { get; private set; }
}
David Arno
quelle
1
" public int Foo { get; private set; }Diese Klasse ist nicht länger unveränderlich, weil sie sich selbst ändern kann." Trotzdem danke!
kdbanman
4
Die beschriebene Klasse ist unveränderlich, da sie sich nicht ändert.
David Arno
1
Meine eigentliche Klasse ist etwas komplexer als die MWE, die ich gegeben habe. Ich bin nach echter Unveränderlichkeit , komplett mit Thread-Sicherheit und klasseninternen Garantien.
kdbanman
4
@ kdbanman, und Ihr Punkt ist? Wenn Ihre Klasse während der Erstellung nur an die privaten Setter schreibt, hat sie "echte Unveränderlichkeit" implementiert. Das readonlySchlüsselwort ist nur ein Poka-Joch, dh es schützt vor der Klasse, die in Zukunft die Unveränderlichkeit bricht. es wird jedoch nicht benötigt, um Unveränderlichkeit zu erreichen.
David Arno
3
Interessanter Begriff, ich musste ihn googeln - Poka-Yoke ist ein japanischer Begriff, der "Fehlerschutz" bedeutet. Du hast recht! Genau das mache ich.
kdbanman
6

Zwei Lösungen.

Einfach:

Schließen Sie keine Setter ein, wie von David für unveränderliche schreibgeschützte Objekte angegeben.

Alternative:

Setter können ein neues unveränderliches Objekt zurückgeben, das im Vergleich zum vorherigen ausführlich ist, jedoch für jedes initialisierte Objekt den Status über die Zeit angibt. Dieses Design ist ein sehr nützliches Werkzeug für die Thread-Sicherheit und Unveränderlichkeit, das sich über alle wichtigen OOP-Sprachen erstreckt.

Pseudocode

public class Thing
{

{readonly vars}

    public Thing(int foo, int bar)
    {
        _foo = foo;
        _bar = bar;
    }

    public Thing withFoo(int foo) {
        return new Thing(foo, getBar());
    }

    public Thing withBar(int bar) {
        return new Thing(getFoo(), bar)
    }

    etc...
}

öffentliche statische Fabrik

public class Thing
{

{readonly vars}

    private Thing(int foo, int bar)
    {
        _foo = foo;
        _bar = bar;
    }

    public static with(int foo, int bar) {
        return new Thing(foo, bar)
    }

    public Thing withFoo(int foo) {
        return new Thing(foo, getBar());
    }

    public Thing withBar(int bar) {
        return new Thing(getFoo(), bar)
    }

    etc...
}
Matt Smithies
quelle
2
setXYZ sind schreckliche Namen für Factory-Methoden, überlegen Sie sich withXYZ oder ähnliches.
Ben Voigt
Vielen Dank, meine Antwort wurde aktualisiert, um Ihren hilfreichen Kommentar wiederzugeben. :-)
Matt Smithies
Die Frage bezog sich speziell auf Eigenschaftssetzer und nicht auf Setter-Methoden.
user253751
Stimmt, aber das OP ist besorgt über den veränderlichen Zustand eines möglichen zukünftigen Entwicklers. Variablen, die schreibgeschützt private oder private final-Schlüsselwörter verwenden, sollten selbstdokumentierend sein. Dies ist nicht der ursprüngliche Entwicklerfehler, wenn dies nicht verstanden wird. Das Verstehen verschiedener Methoden zum Ändern des Status ist der Schlüssel. Ich habe einen anderen Ansatz hinzugefügt, der sehr hilfreich ist. Es gibt eine Menge Dinge in der Code-Welt, die zu übermäßiger Paranoia führen. Private Readonly-Vars sollten nicht dazu gehören.
Matt Smithies