Jak obsługiwać seterów na niezmiennych polach?

25

Mam klasę z dwoma readonly intpolami. Są eksponowane jako właściwości:

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 { } }
}

Oznacza to jednak, że następujący kod jest całkowicie zgodny z prawem:

Thing iThoughtThisWasMutable = new Thing(1, 42);

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

Błędy wynikające z założenia, że to zadziała, z pewnością będą podstępne. Jasne, błędny programista powinien przeczytać dokumenty. Ale to nie zmienia faktu, że żaden błąd kompilacji lub wykonania nie ostrzegł go o problemie.

Jak Thingzmienić klasę, aby deweloperzy nie popełnili powyższego błędu?

Rzuć wyjątek? Używasz metody gettera zamiast właściwości?

kdbanman
źródło
1
Dzięki, @gnat. Że post (zarówno pytanie i odpowiedź) wydaje się mówić o interfejsach, jak w Capital I Interfaces. Nie jestem pewien, czy to właśnie robię.
kdbanman
6
@gnat, to okropna rada. Interfejsy oferują świetny sposób obsługi publicznie niezmiennych VO / DTO, które wciąż są łatwe do przetestowania.
David Arno,
4
@gnat, zrobiłem, a pytanie nie ma sensu, ponieważ OP wydaje się sądzić, że interfejsy zniszczą niezmienność, co jest nonsensem.
David Arno
1
@gnat, to pytanie dotyczyło zadeklarowania interfejsów dla DTO. Najczęściej głosowaną odpowiedzią było to, że interfejsy nie będą szkodliwe, ale prawdopodobnie niepotrzebne.
Paul Draper,

Odpowiedzi:

107

Dlaczego ten kodeks jest legalny?
Wyjmij, set { }jeśli nic nie robi.
Oto jak definiujesz własność publiczną tylko do odczytu:

public int Foo { get { return _foo; } }
paparazzo
źródło
3
Z jakiegoś powodu nie sądziłem, że to legalne, ale wydaje się, że działa idealnie! Perfekcyjnie, dziękuję.
kdbanman
1
@kdbanman Prawdopodobnie ma to coś wspólnego z czymś, co IDE robiło w pewnym momencie - prawdopodobnie z autouzupełnianiem.
Panzercrisis,
2
@kdbanman; to, co nie jest legalne (do C # 5), to public int Foo { get; }(auto-własność tylko z geterem) Ale public int Foo { get; private set; }jest.
Laurent LA RIZZA
@LaurentLARIZZA, przebiegłeś moją pamięć. Właśnie stąd pochodzi moje zamieszanie.
kdbanman
45

W wersji C # 5 i wcześniejszych mieliśmy do czynienia z dwiema opcjami dla niezmiennych pól eksponowanych przez getter:

  1. Utwórz zmienną bazową tylko do odczytu i zwróć ją za pomocą metody pobierającej ręcznie. Ta opcja jest bezpieczna (należy jawnie usunąć, readonlyaby zniszczyć niezmienność. Stworzyło to jednak mnóstwo kodu płyty kotłowej.
  2. Użyj auto-właściwości z prywatnym ustawiaczem. Tworzy to prostszy kod, ale łatwiej jest przypadkowo złamać niezmienność.

Jednak w wersji C # 6 (dostępnej w wersji VS2015, która została wydana wczoraj), otrzymujemy to, co najlepsze z obu światów: właściwości automatyczne tylko do odczytu. To pozwala nam uprościć klasę OP w celu:

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

Foo = fooI Bar = barlinie są ważne tylko w konstruktor (co osiąga się niezawodne wymagania tylko do odczytu), i pole podłoża zakłada się, zamiast być wyraźnie określone (który uzyskuje się prostszą kodu).

David Arno
źródło
2
A pierwsi konstruktorzy mogą to jeszcze bardziej uprościć, pozbywając się składniowego narzutu konstruktora.
Sebastian Redl,
1
@ DanLyons Cholera, nie mogłem się doczekać, aby użyć tego w całej naszej bazie kodu.
Sebastian Redl,
12

Możesz po prostu pozbyć się seterów. Nic nie robią, wprowadzą użytkowników w błąd i doprowadzą do błędów. Możesz jednak zamiast tego ustawić je jako prywatne i pozbyć się zmiennych pomocniczych, upraszczając kod:

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
źródło
1
public int Foo { get; private set; }” Ta klasa nie jest już niezmienna, ponieważ może się zmienić. W każdym razie dzięki!
kdbanman
4
Opisana klasa jest niezmienna, ponieważ sama się nie zmienia.
David Arno,
1
Moja faktyczna klasa jest nieco bardziej złożona niż MWE, które podałem. Jestem po rzeczywistym niezmienności , wraz z gwarancjami bezpieczeństwa gwintu i klasowo wewnętrzny.
kdbanman
4
@kdbanman, a masz na myśli? Jeśli twoja klasa pisze tylko do prywatnych seterów podczas budowy, to zaimplementowała „prawdziwą niezmienność”. readonlyKluczowe jest tylko Poka-Yoke, czyli chroni przed klasą zerwania immutablity w przyszłości; nie jest to jednak konieczne do osiągnięcia niezmienności.
David Arno,
3
Interesujący termin, musiałem google go - poka-yoke to japoński termin oznaczający „zabezpieczenie przed błędami”. Masz rację! Właśnie to robię.
kdbanman
6

Dwa rozwiązania.

Prosty:

Nie dołączaj seterów, jak zauważył David dla niezmiennych obiektów tylko do odczytu.

Alternatywnie:

Zezwól ustawiającym na zwrócenie nowego niezmiennego obiektu, szczegółowego w porównaniu z poprzednim, ale zapewnia stan w czasie dla każdego zainicjowanego obiektu. Ta konstrukcja jest bardzo przydatnym narzędziem do zapewnienia bezpieczeństwa i niezmienności wątków, które obejmuje wszystkie niezbędne języki OOP.

Pseudo kod

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...
}

publiczna fabryka statyczna

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
źródło
2
setXYZ to okropne nazwy metod fabrycznych, rozważ z XYZ lub podobnymi.
Ben Voigt,
Dzięki, zaktualizowałem moją odpowiedź, aby odzwierciedlić twój pomocny komentarz. :-)
Matt Smithies,
Pytanie dotyczyło w szczególności ustawiaczy właściwości , a nie metod ustawiania.
user253751
To prawda, ale OP martwi się o stan zmienny od potencjalnego przyszłego programisty. Zmienne używające prywatnych tylko do odczytu lub prywatnych końcowych słów kluczowych powinny być samodokumentujące, co nie jest winą oryginalnego dewelopera, jeśli nie jest to zrozumiane. Kluczem jest zrozumienie różnych metod zmiany stanu. Dodałem inne podejście, które jest bardzo pomocne. W świecie kodów jest wiele rzeczy, które powodują nadmierną paranoję, prywatne zmienne tylko nie powinny być jedną z nich.
Matt Smithies,