W którym momencie niezmienne klasy stają się ciężarem?

16

Przy projektowaniu klas do przechowywania modelu danych przeczytałem, że przydatne może być tworzenie niezmiennych obiektów, ale w którym momencie ciężar listy parametrów konstruktora i głębokich kopii staje się zbyt duży i trzeba zrezygnować z niezmiennego ograniczenia?

Na przykład, tutaj jest niezmienna klasa reprezentująca nazwaną rzecz (używam składni C #, ale zasada dotyczy wszystkich języków OO)

class NamedThing
{
    private string _name;    
    public NamedThing(string name)
    {
        _name = name;
    }    
    public NamedThing(NamedThing other)
    {
         this._name = other._name;
    }
    public string Name
    {
        get { return _name; }
    }
}

Nazwane rzeczy można konstruować, wyszukiwać i kopiować do nowych nazwanych rzeczy, ale nazwy nie można zmienić.

Wszystko dobrze, ale co się stanie, gdy chcę dodać kolejny atrybut? Muszę dodać parametr do konstruktora i zaktualizować konstruktor kopiowania; co nie jest zbyt pracochłonne, ale problemy zaczynają się, o ile widzę, gdy chcę uczynić złożony obiekt niezmiennym.

Jeśli klasa zawiera atrybuty i kolekcje zawierające inne złożone klasy, wydaje mi się, że lista parametrów konstruktora stałaby się koszmarem.

Więc w którym momencie klasa staje się zbyt złożona, aby była niezmienna?

Tony
źródło
Zawsze staram się, aby zajęcia w moim modelu były niezmienne. Jeśli masz ogromne, długie listy parametrów konstruktora, to może twoja klasa jest zbyt duża i można ją podzielić? Jeśli twoje obiekty niższego poziomu są również niezmienne i mają ten sam wzór, to twoje obiekty na wyższym poziomie nie powinny cierpieć (zbytnio). Uważam, że O wiele trudniej jest zmienić istniejącą klasę, aby stała się niezmienna niż uczynić model danych niezmiennym, kiedy zaczynam od zera.
Nikt
1
Możesz spojrzeć na wzorzec Konstruktora sugerowany w tym pytaniu: stackoverflow.com/questions/1304154/...
Ant
Czy spojrzałeś na MemberwiseClone? Nie musisz aktualizować konstruktora kopiowania dla każdego nowego członka.
kevin cline
3
@ Tony Jeśli twoje kolekcje i wszystko, co zawierają, są również niezmienne, nie potrzebujesz głębokiej kopii, wystarczy płytka kopia.
mjcopple,
2
Nawiasem mówiąc, zwykle używam pól „set-once” w klasach, w których klasa musi być „dość” niezmienna, ale nie do końca. Uważam, że rozwiązuje to problem wielkich konstruktorów, ale zapewnia większość korzyści z niezmiennych klas. (mianowicie twój wewnętrzny kod klasy nie musi się martwić o zmianę wartości)
Earlz

Odpowiedzi:

23

Kiedy stają się ciężarem? Bardzo szybko (szczególnie jeśli wybrany język nie zapewnia wystarczającego wsparcia składniowego niezmienności).

Niezmienność jest sprzedawana jako srebrna kula dla wielordzeniowego dylematu i tak dalej. Ale niezmienność w większości języków OO zmusza cię do dodawania sztucznych artefaktów i praktyk do twojego modelu i procesu. Dla każdej złożonej niezmiennej klasy musisz mieć równie złożony (przynajmniej wewnętrznie) konstruktor. Bez względu na to, jak go zaprojektujesz, nadal wprowadza silne sprzężenie (dlatego lepiej mamy powód, aby je wprowadzić).

Nie zawsze jest możliwe modelowanie wszystkiego w małych, niezłożonych klasach. W przypadku dużych klas i struktur sztucznie dzielimy je na partycje - nie dlatego, że ma to sens w naszym modelu domen, ale dlatego, że musimy poradzić sobie z ich złożoną instancją i budowniczymi kodu.

Gorzej, gdy ludzie zbyt daleko idą na pojęcie niezmienności w języku ogólnego przeznaczenia, takim jak Java lub C #, czyniąc wszystko niezmiennym. W rezultacie widzisz ludzi wymuszających konstrukcje s-expression w językach, które nie obsługują takich rzeczy z łatwością.

Inżynieria to modelowanie poprzez kompromisy i kompromisy. Sprawianie, by wszystko było niezmienne przez edict, ponieważ ktoś przeczytał, że wszystko jest niezmienne w języku funkcjonalnym X lub Y (zupełnie inny model programowania), co jest niedopuszczalne. To nie jest dobra inżynieria.

Małe, ewentualnie jednolite rzeczy można uczynić niezmiennymi. Bardziej złożone rzeczy można uczynić niezmiennymi, gdy ma to sens . Ale niezmienność nie jest srebrną kulą. Zdolność do zmniejszania błędów, zwiększania skalowalności i wydajności, nie są jedyną funkcją niezmienności. Jest to funkcja właściwych praktyk inżynierskich . W końcu ludzie napisali dobre, skalowalne oprogramowanie bez niezmienności.

Niezmienność staje się naprawdę ciężkim obciążeniem (zwiększa przypadkową złożoność), jeśli jest wykonywana bez powodu, gdy jest wykonywana poza tym, co ma sens w kontekście modelu domeny.

Ja, na przykład, staram się tego unikać (chyba że pracuję w języku programowania z dobrą obsługą syntaktyczną).

luis.espinal
źródło
6
luis, czy zauważyłeś, że dobrze napisane, pragmatycznie poprawne odpowiedzi napisane z wyjaśnieniem prostych, ale solidnych zasad inżynierii zwykle nie zdobywają tylu głosów, co osoby stosujące najnowsze trendy kodowania? To świetna, świetna odpowiedź.
Huperniketes
3
Dzięki :) Sam zauważyłem trendy związane z reklamami, ale to w porządku. Fad fanboys rezygnują z kodu, który później naprawiamy w lepszych stawkach godzinowych, hahah :) jk ...
luis.espinal
4
Srebrna kula? Nie. Warto trochę niezręczności w C # / Java (nie jest tak źle)? Absolutnie. Również rola wielordzeniowości w niezmienności jest dość niewielka ... prawdziwą korzyścią jest łatwość rozumowania.
Mauricio Scheffer
@ Mauricio - jeśli tak mówisz (niezmienność w Javie nie jest taka zła). Pracując nad Javą w latach 1998-2011, chciałbym się różnić, nie jest to trywialne wyłączenie w prostych bazach kodu. Jednak ludzie mają różne doświadczenia i potwierdzam, że mój POV nie jest wolny od podmiotowości. Przepraszam, nie mogę się zgodzić. Zgadzam się jednak, że łatwość rozumowania jest najważniejsza, jeśli chodzi o niezmienność.
luis.espinal
13

Przeszedłem fazę nalegania, aby klasy były niezmienne, tam gdzie to możliwe. Czy konstruktorzy dla prawie wszystkiego, niezmiennych tablic itp. Itp. Znalazłem odpowiedź na twoje pytanie, jest prosta: w którym momencie niezmienne klasy stają się ciężarem? Bardzo szybko. Gdy tylko chcesz coś serializować, musisz być w stanie dokonać deserializacji, co oznacza, że ​​musi to być zmienne; gdy tylko chcesz użyć ORM, większość z nich nalega na możliwość zmiany właściwości. I tak dalej.

Ostatecznie zastąpiłem tę politykę niezmiennymi interfejsami do obiektów zmiennych.

class NamedThing : INamedThing
{
    private string _name;    
    public NamedThing(string name)
    {
        _name = name;
    }    

    public NamedThing(NamedThing other)
    {
        this._name = other._name;
    }

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

interface INamedThing
{
    string Name { get; }
}

Teraz obiekt ma elastyczność, ale nadal można powiedzieć kodowi wywołującemu, że nie powinien edytować tych właściwości.

pdr
źródło
8
Pomijając drobne sprzeczki, zgadzam się, że niezmienne obiekty mogą bardzo szybko stać się problemem w programowaniu w języku imperatywnym, ale nie jestem pewien, czy niezmienny interfejs naprawdę rozwiązuje ten sam problem, czy w ogóle jakiś problem. Głównym powodem korzystania z niezmiennego obiektu jest to, że możesz go rzucać w dowolnym miejscu i nigdy nie martwić się o to, że ktoś zepsuje ci stan. Jeśli obiekt leżący pod spodem jest zmienny, nie masz tej gwarancji, zwłaszcza jeśli powód, dla którego go utrzymałeś, był możliwy, ponieważ różne rzeczy muszą go mutować.
Aaronaught
1
@Aaronaught - Interesujący punkt. Wydaje mi się, że jest to bardziej kwestia psychologiczna niż faktyczna ochrona. Jednak twój ostatni wiersz ma fałszywą przesłankę. Powodem, dla którego jest zmienny, jest to, że różne rzeczy muszą go utworzyć i zapełnić poprzez odbicie, a nie mutować po utworzeniu instancji.
pdr
1
@Aaronaught: Ten sam sposób IComparable<T>gwarantuje, że jeśli X.CompareTo(Y)>0i Y.CompareTo(Z)>0wtedy X.CompareTo(Z)>0. Interfejsy mają umowy . Jeśli umowa IImmutableList<T>określa, że ​​wartości wszystkich przedmiotów i właściwości muszą zostać „ustalone w kamieniu”, zanim jakakolwiek instancja zostanie wystawiona na świat zewnętrzny, to zrobią to wszystkie uzasadnione wdrożenia. Nic nie stoi na przeszkodzie, aby IComparableimplementacja naruszała przechodniość, ale implementacje, które to robią, są nielegalne. W przypadku SortedDictionarynieprawidłowego działania, gdy zostanie wydany nielegalnie IComparable, ...
supercat
1
@Aaronaught: Dlaczego mam wierzyć, że implementacje IReadOnlyList<T>będą niezmienne, biorąc pod uwagę, że (1) nie ma takiego wymogu jest zawarta w dokumentacji interfejsu, oraz (2) najczęściej realizacja, List<T>, nie jest nawet tylko do odczytu ? Nie jestem do końca jasne, co jest niejednoznaczne w moich warunkach: kolekcja jest czytelna, jeśli dane w niej można odczytać. Jest tylko do odczytu, jeśli może obiecać, że zawarte w nim dane nie mogą zostać zmienione, chyba że kod zawiera jakieś zewnętrzne odniesienie, które by to zmieniło. Jest niezmienny, jeśli może zagwarantować, że nie można go zmienić, kropka.
supercat
1
@ superupat: Nawiasem mówiąc, Microsoft się ze mną zgadza. Wydali niezmienny pakiet kolekcji i zauważają, że wszystkie są konkretnymi typami, ponieważ nigdy nie można zagwarantować, że abstrakcyjna klasa lub interfejs jest naprawdę niezmienny.
Aaronaught
8

Nie sądzę, że istnieje ogólna odpowiedź na to pytanie. Im bardziej złożona jest klasa, tym trudniej jest jej uzasadnić zmiany stanu i tym droższe jest tworzenie jej nowych kopii. Zatem powyżej pewnego (osobistego) poziomu złożoności stanie się zbyt bolesne, aby uczynić klasę niezmienną.

Zauważ, że zbyt złożona klasa lub długa lista parametrów metody to same zapachy projektowe , niezależnie od niezmienności.

Tak więc zwykle preferowanym rozwiązaniem byłoby rozbicie takiej klasy na wiele odrębnych klas, z których każda może zostać samodzielnie zmieniona lub niezmienna. Jeśli nie jest to możliwe, można je zmienić.

Péter Török
źródło
5

Możesz uniknąć problemu z kopiowaniem, jeśli przechowujesz wszystkie swoje niezmienne pola w środku struct. Jest to w zasadzie odmiana wzoru pamiątkowego. Jeśli chcesz wykonać kopię, po prostu skopiuj pamiątkę:

class MyClass
{
    struct Memento
    {
        public int field1;
        public string field2;
    }

    private readonly Memento memento;

    public MyClass(int field1, string field2)
    {
        this.memento = new Memento()
            {
                field1 = field1,
                field2 = field2
            };
    }

    private MyClass(Memento memento) // for copying
    {
        this.memento = memento;
    }

    public int Field1 { get { return this.memento.field1; } }
    public string Field2 { get { return this.memento.field2; } }

    public MyClass WithNewField1(int newField1)
    {
        Memento newMemento = this.memento;
        newMemento.field1 = newField1;
        return new MyClass(newMemento);
    }
}
Scott Whitlock
źródło
Myślę, że wewnętrzna struktura nie jest konieczna. To po prostu kolejny sposób wykonywania MemberwiseClone.
Codism
@Codism - tak i nie. Są chwile, kiedy możesz potrzebować innych członków, których nie chcesz klonować. Co się stanie, jeśli użyjesz leniwej oceny w jednym z programów pobierających i buforujesz wynik w elemencie członkowskim? Jeśli wykonasz MemberwiseClone, sklonujesz buforowaną wartość, a następnie zmienisz jednego z członków, od którego zależy buforowana wartość. Czystsze jest oddzielenie stanu od pamięci podręcznej.
Scott Whitlock,
Warto wspomnieć o kolejnej przewadze wewnętrznej struktury: ułatwia obiektowi skopiowanie swojego stanu do obiektu, do którego istnieją inne odwołania . Częstym źródłem niejednoznaczności w OOP jest to, czy metoda zwracająca odwołanie do obiektu zwraca widok obiektu, który może ulec zmianie poza kontrolą odbiorcy. Jeśli zamiast zwracać odwołanie do obiektu, metoda akceptuje odwołanie do obiektu od obiektu wywołującego i kopiuje do niego stan, własność obiektu będzie znacznie wyraźniejsza. Takie podejście nie działa dobrze z typami swobodnie dziedziczonymi, ale ...
supercat
... może być bardzo przydatny w przypadku zmiennych danych. Podejście to sprawia również, że bardzo łatwo jest mieć „równoległe” zmienne i niezmienne klasy (pochodzące z abstrakcyjnej „czytelnej” bazy) i pozwolić ich konstruktorom na kopiowanie danych między sobą.
supercat
3

W pracy jest kilka rzeczy. Niezmienne zestawy danych doskonale nadają się do skalowania wielowątkowego. Zasadniczo możesz całkiem zoptymalizować swoją pamięć, aby jeden zestaw parametrów był jednym wystąpieniem klasy - wszędzie. Ponieważ obiekty nigdy się nie zmieniają, nie musisz się martwić synchronizacją dostępu do swoich członków. To dobra rzecz. Jednak, jak zauważyłeś, im bardziej złożony obiekt, tym bardziej potrzebujesz zmienności. Zacznę od rozumowania według następujących zasad:

  • Czy istnieje jakiś powód biznesowy, dla którego obiekt może to zrobić zmienić swój stan? Na przykład obiekt użytkownika przechowywany w bazie danych jest unikalny na podstawie swojego identyfikatora, ale z czasem musi mieć możliwość zmiany stanu. Z drugiej strony, gdy zmieniasz współrzędne na siatce, przestaje ona być pierwotną współrzędną, więc sensowne jest uczynienie współrzędnych niezmiennymi. To samo z łańcuchami.
  • Czy można obliczyć niektóre atrybuty? Krótko mówiąc, jeśli inne wartości w nowej kopii obiektu są funkcją pewnej wartości podstawowej, którą przekazujesz, możesz je obliczyć w konstruktorze lub na żądanie. Zmniejsza to nakłady na konserwację, ponieważ można zainicjować te wartości w taki sam sposób podczas kopiowania lub tworzenia.
  • Ile wartości składa się na nowy niezmienny obiekt? W pewnym momencie złożoność tworzenia obiektu staje się nietrywialna i w tym momencie posiadanie większej liczby instancji obiektu może stać się problemem. Przykłady obejmują niezmienne struktury drzew, obiekty z więcej niż trzema przekazanymi parametrami itp. Im więcej parametrów, tym większa możliwość zakłócenia kolejności parametrów lub wyzerowania niewłaściwego.

W językach, które obsługują tylko obiekty niezmienne (takie jak Erlang), jeśli istnieje jakakolwiek operacja, która wydaje się modyfikować stan obiektu niezmiennego, wynikiem końcowym jest nowa kopia obiektu o zaktualizowanej wartości. Na przykład po dodaniu elementu do wektora / listy:

myList = lists:append([[1,2,3], [4,5,6]])
% myList is now [1,2,3,4,5,6]

To może być rozsądny sposób pracy z bardziej skomplikowanymi obiektami. Na przykład podczas dodawania węzła drzewa powstaje nowe drzewo z dodanym węzłem. Metoda w powyższym przykładzie zwraca nową listę. W przykładzie w tym akapicie tree.add(newNode)zwróciłoby nowe drzewo z dodanym węzłem. Dla użytkowników praca z nim staje się łatwa. Dla autorów bibliotek staje się nudne, gdy język nie obsługuje niejawnego kopiowania. Ten próg zależy od twojej własnej cierpliwości. Dla użytkowników twojej biblioteki najbardziej rozsądnym limitem, jaki znalazłem, jest około trzech do czterech parametrów.

Berin Loritsch
źródło
Jeśli ktoś byłby skłonny użyć zmiennego odwołania do obiektu jako wartości [co oznacza, że ​​żadne odniesienia nie są zawarte w jego właścicielu i nigdy nie zostały ujawnione], skonstruowanie nowego niezmiennego obiektu, który zawiera pożądaną „zmienioną” zawartość, jest równoznaczne z bezpośrednią modyfikacją obiektu, choć jest prawdopodobnie wolniejszy. Obiekty zmienne można jednak również wykorzystywać jako byty . Jak stworzyć rzeczy, które zachowują się jak byty bez obiektów podlegających zmianom?
supercat
0

Jeśli masz wielu członków klasy końcowej i nie chcesz, aby byli narażeni na wszystkie obiekty, które muszą je utworzyć, możesz użyć wzorca konstruktora:

class NamedThing
{
    private string _name;    
    private string _value;
    private NamedThing(string name, string value)
    {
        _name = name;
        _value = value;
    }    
    public NamedThing(NamedThing other)
    {
        this._name = other._name;
        this._value = other._value;
    }
    public string Name
    {
        get { return _name; }
    }

    public static class Builder {
        string _name;
        string _value;

        public void setValue(string value) {
            _value = value;
        }
        public void setName(string name) {
            _name = name;
        }
        public NamedThing newObject() {
            return new NamedThing(_name, _value);
        }
    }
}

zaletą jest to, że można łatwo utworzyć nowy obiekt z inną wartością o innej nazwie.

Salandur
źródło
Myślę, że twój konstruktor będący statyczny jest niepoprawny. Inny wątek może zmienić nazwę statyczną lub wartość statyczną po ich ustawieniu, ale przed wywołaniem newObject.
ErikE
Tylko klasa konstruktora jest statyczna, ale jej członkowie nie są. Oznacza to, że dla każdego konstruktora, który utworzysz, ma on własny zestaw elementów o odpowiednich wartościach. Klasa musi być statyczna, aby można było jej używać i tworzyć jej instancję poza klasą zawierającą ( NamedThingw tym przypadku)
Salandur
Rozumiem, co mówisz, po prostu wyobrażam sobie z tym problem, ponieważ nie prowadzi to dewelopera do „wpadnięcia w pułapkę sukcesu”. Fakt, że używa zmiennych statycznych oznacza, że ​​jeśli a Builderzostanie ponownie użyte, istnieje realne ryzyko, że coś się wydarzy, o czym wspomniałem. Ktoś może budować wiele obiektów i zdecydować, że skoro większość właściwości jest taka sama, po prostu ponownie użyj Builder, a tak naprawdę zróbmy z tego globalny singleton wstrzykiwany w zależności! Ups Wprowadzono poważne błędy. Myślę więc, że ten wzór mieszanej instancji vs. statyczności jest zły.
ErikE
1
@Salandur Klasy wewnętrzne w języku C # są zawsze „statyczne” w sensie klasy wewnętrznej Java.
Sebastian Redl,
0

Więc w którym momencie klasa staje się zbyt złożona, aby była niezmienna?

Moim zdaniem nie warto zadawać sobie trudu, aby małe klasy były niezmienne w językach takich jak ten, który pokazujesz. Używam tutaj małych i nie skomplikowanych , ponieważ nawet jeśli dodasz dziesięć pól do tej klasy i to naprawdę wymyślne operacje na nich, wątpię, że zajmie to kilobajty, a co dopiero megabajty, a co dopiero gigabajty, więc każda funkcja wykorzystująca instancje twojego klasa może po prostu wykonać tanią kopię całego obiektu, aby uniknąć modyfikacji oryginału, jeśli chce uniknąć wywoływania zewnętrznych efektów ubocznych.

Trwałe struktury danych

Uważam, że osobistym zastosowaniem niezmienności jest duże, centralne struktury danych, które agregują garść drobnych danych, takich jak instancje klasy, którą pokazujesz, jak ta, która przechowuje milion NamedThings. Przynależność do trwałej struktury danych, która jest niezmienna i znajduje się za interfejsem, który pozwala tylko na dostęp tylko do odczytu, elementy należące do kontenera stają się niezmienne bez NamedThingkonieczności radzenia sobie z klasą elementów ( ).

Tanie kopie

Trwała struktura danych umożliwia przekształcenie jej regionów i uczynienie ich unikalnymi, unikając modyfikacji oryginału bez konieczności kopiowania struktury danych w całości. To jest prawdziwe piękno tego. Jeśli chcesz naiwnie pisać funkcje, które unikają skutków ubocznych, które wprowadzają strukturę danych, która zajmuje gigabajty pamięci i tylko modyfikuje pamięć o wartości megabajta, musisz skopiować całą dziwaczną rzecz, aby uniknąć dotykania danych wejściowych i zwrócić nową wynik. Albo kopiuj gigabajty, aby uniknąć efektów ubocznych, albo powoduj skutki uboczne w tym scenariuszu, co oznacza, że ​​musisz wybierać między dwoma nieprzyjemnymi wyborami.

Dzięki trwałej strukturze danych pozwala napisać taką funkcję i uniknąć kopiowania całej struktury danych, wymagając jedynie około megabajta dodatkowej pamięci na dane wyjściowe, jeśli tylko funkcja przekształciła pamięć o wielkości megabajta.

Ciężar

Jeśli chodzi o ciężar, przynajmniej w moim przypadku jest to natychmiastowe. Potrzebuję tych konstruktorów, o których mówią ludzie lub „przejściowych”, jak je nazywam, aby móc skutecznie wyrażać przekształcenia w tę ogromną strukturę danych bez dotykania jej. Kod taki jak ten:

void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
     // Transform stuff in the range, [first, last).
     for (; first != last; ++first)
          transform(stuff[first]);
}

... wtedy należy napisać w ten sposób:

ImmList<Stuff> transform_stuff(ImmList<Stuff> stuff, int first, int last)
{
     // Grab a "transient" (builder) list we can modify:
     TransientList<Stuff> transient(stuff);

     // Transform stuff in the range, [first, last)
     // for the transient list.
     for (; first != last; ++first)
          transform(transient[first]);

     // Commit the modifications to get and return a new
     // immutable list.
     return stuff.commit(transient);
}

Ale w zamian za te dwa dodatkowe wiersze kodu funkcja jest teraz bezpiecznie wywoływać w wątkach z tą samą oryginalną listą, nie powoduje żadnych skutków ubocznych itp. Ułatwia to również, aby ta operacja stała się niemożliwą do wykonania operacją użytkownika, ponieważ Cofnij może po prostu przechowywać tanią płytką kopię starej listy.

Wyjątek - bezpieczeństwo lub odzyskiwanie po błędzie

Nie każdy może czerpać tyle samo korzyści, co ja, z trwałych struktur danych w takich kontekstach (znalazłem dla nich tak wiele zastosowania w systemach cofania i nieniszczącej edycji, które są głównymi koncepcjami w mojej domenie VFX), ale jedna rzecz dotyczy tylko wszyscy powinni wziąć pod uwagę bezpieczeństwo wyjątków lub odzyskiwanie po błędzie .

Jeśli chcesz, aby oryginalna funkcja mutacji była bezpieczna dla wyjątków, potrzebuje logiki cofania, dla której najprostsza implementacja wymaga skopiowania całej listy:

void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
    // Make a copy of the whole massive gigabyte-sized list 
    // in case we encounter an exception and need to rollback
    // changes.
    MutList<Stuff> old_stuff = stuff;

    try
    {
         // Transform stuff in the range, [first, last).
         for (; first != last; ++first)
             transform(stuff[first]);
    }
    catch (...)
    {
         // If the operation failed and ran into an exception,
         // swap the original list with the one we modified
         // to "undo" our changes.
         stuff.swap(old_stuff);
         throw;
    }
}

W tym momencie bezpieczna dla wyjątków wersja mutowalna jest jeszcze bardziej obliczeniowa i prawdopodobnie trudniejsza do napisania niż wersja niezmienna przy użyciu „konstruktora”. I wielu programistów C ++ po prostu zaniedbuje bezpieczeństwo wyjątków i być może jest to w porządku dla ich domeny, ale w moim przypadku chcę upewnić się, że mój kod działa poprawnie nawet w przypadku wyjątku (nawet pisanie testów, które celowo zgłaszają wyjątki w celu przetestowania wyjątku bezpieczeństwo), a to sprawia, że ​​muszę być w stanie cofnąć wszelkie skutki uboczne, które funkcja powoduje w połowie funkcji, jeśli coś rzuci.

Jeśli chcesz być bezpieczny w wyjątkach i wdzięcznie odzyskać po błędach bez awarii i nagrywania aplikacji, musisz cofnąć / cofnąć wszelkie skutki uboczne, które funkcja może wywołać w przypadku błędu / wyjątku. I tam konstruktor może faktycznie zaoszczędzić więcej czasu programisty niż to kosztuje wraz z czasem obliczeniowym, ponieważ: ...

Nie musisz się martwić o wycofywanie efektów ubocznych w funkcji, która nie powoduje żadnych!

Wróćmy do podstawowego pytania:

W którym momencie niezmienne klasy stają się ciężarem?

Zawsze są one obciążeniem dla języków, które obracają się bardziej wokół zmienności niż niezmienności, dlatego uważam, że powinieneś ich używać, gdy korzyści znacznie przewyższają koszty. Ale na wystarczająco szerokim poziomie dla wystarczająco dużych struktur danych, wierzę, że istnieje wiele przypadków, w których jest to godziwy kompromis.

Również w moim mam tylko kilka niezmiennych typów danych, a wszystkie są ogromnymi strukturami danych przeznaczonymi do przechowywania ogromnej liczby elementów (piksele obrazu / tekstury, byty i komponenty ECS oraz wierzchołki / krawędzie / wielokąty siatka).


źródło