Dlaczego zmienne struktury są „złe”?

485

Po dyskusjach tutaj na temat SO przeczytałem już kilkakrotnie uwagę, że struktury modyfikowalne są „złe” (jak w odpowiedzi na to pytanie ).

Jaki jest rzeczywisty problem ze zmiennością i strukturami w C #?

Dirk Vollmar
źródło
12
Twierdzenie, że zmienne struktury są złe, jest jak twierdzenie, że zmienne ints, bools, a wszystkie inne typy wartości są złe. Istnieją przypadki mutacji i niezmienności. Przypadki te zależą od roli danych, a nie od rodzaju alokacji / udostępniania pamięci.
Slipp D. Thompson
41
@slipp inti niebool można ich
modyfikować
2
.-Syntax, dzięki czemu operacje z danymi o zmienionym typie i danymi o wartości zmienionej wyglądają tak samo, chociaż są wyraźnie różne. Jest to wina właściwości C #, a nie struktur - niektóre języki oferują alternatywną a[V][X] = 3.14składnię do mutowania w miejscu. W języku C # lepiej byłoby zaoferować metody mutatora typu struct-member, takie jak „MutateV (mutator <ref Vector2>)” i używać go w podobny sposób a.MutateV((v) => { v.X = 3; }) (przykład jest nadmiernie uproszczony ze względu na ograniczenia C # dotyczące refsłowa kluczowego, ale z pewnymi obejścia powinny być możliwe) .
Slipp D. Thompson
2
@Poślizg Cóż, myślę dokładnie przeciwnie o tego rodzaju strukturach. Jak myślisz, dlaczego struktury, które są już zaimplementowane w bibliotece .NET, takie jak DateTime lub TimeSpan (tak podobne), są niezmienne? Może przydałaby się zmiana tylko jednego członka var tej struktury, ale jest to po prostu zbyt niewygodne, prowadzi do zbyt wielu problemów. W rzeczywistości nie masz racji co do tego, co oblicza procesor, ponieważ C # nie kompiluje się do asemblera, kompiluje się do IL. W IL (pod warunkiem, że mamy już zmienną o nazwie x) ta pojedyncza operacja to 4 instrukcje: ldloc.0(ładuje zmienną 0-indeksową do ...
Sushi271
1
... rodzaj. Tjest typem. Ref jest tylko słowem kluczowym, które powoduje, że zmienna jest przekazywana do samej metody, a nie jej kopia. Ma to również sens dla typów referencji, ponieważ możemy zmienić zmienną , tj. Referencja poza metodą będzie wskazywała na inny obiekt po zmianie w ramach metody. Ponieważ ref Tnie jest typem, ale sposobem przekazywania parametru metody, nie można go wprowadzić <>, ponieważ można tam umieścić tylko typy. Więc to jest po prostu nieprawidłowe. Może byłoby to wygodne, może zespół C # mógłby to zrobić dla nowej wersji, ale teraz pracują nad niektórymi ...
Sushi271

Odpowiedzi:

290

Struktury są typami wartości, co oznacza, że ​​są kopiowane, gdy są przekazywane.

Więc jeśli zmienisz kopię, zmienisz tylko tę kopię, a nie oryginał, a nie inne, które mogą być w pobliżu.

Jeśli twoja struktura jest niezmienna, wszystkie automatyczne kopie wynikające z przekazania wartości będą takie same.

Jeśli chcesz to zmienić, musisz to zrobić świadomie, tworząc nową instancję struktury ze zmodyfikowanymi danymi. (nie kopia)

trampster
źródło
82
„Jeśli twoja struktura jest niezmienna, wszystkie kopie będą takie same”. Nie, oznacza to, że musisz świadomie wykonać kopię, jeśli chcesz innej wartości. Oznacza to, że nie zostaniesz przyłapany na modyfikowaniu kopii, myśląc, że modyfikujesz oryginał.
Lucas
19
@Lucas Myślę, że mówisz o innym rodzaju kopii Mówię o automatycznych kopiach wykonanych w wyniku przekazania wartości. Twoja „świadomie wykonana kopia” jest inna, ponieważ nie zrobiłeś tego przez pomyłkę, a jej tak naprawdę nie jest to kopia to celowe nowe instancje zawierające różne dane.
trampster
3
Twoja edycja (16 miesięcy później) sprawia, że ​​jest to trochę jaśniejsze. Nadal jednak „(niezmienna struktura) oznacza, że ​​nie zostaniesz przyłapany na modyfikowaniu kopii, myśląc, że modyfikujesz oryginał”.
Lucas
6
@Lucas: Niebezpieczeństwo robienia kopii struktury, modyfikowania jej i myślenia w jakiś sposób modyfikuje oryginał (kiedy fakt, że piszesz pole struktury, uwidacznia się w tym, że piszesz tylko kopię) dość małe w porównaniu z niebezpieczeństwem, że ktoś, kto trzyma obiekt klasy jako środek do przechowywania informacji w nim zawartych, zmutuje obiekt, aby zaktualizować własne informacje, a w trakcie procesu uszkodzi informacje przechowywane przez inny obiekt.
supercat
2
Trzeci akapit brzmi co najwyżej źle lub niejasnie. Jeśli twoja struktura jest niezmienna, po prostu nie będziesz mógł modyfikować jej pól ani pól wykonanych kopii. „Jeśli chcesz to zmienić trzeba ...” , który jest zbyt mylące, nie można zmienić go nigdy , ani świadomie, ani nieświadomie. Utworzenie nowej instancji, w której dane, które chcesz, nie ma nic wspólnego z oryginalną kopią poza tą samą strukturą danych.
Saeb Amini
167

Od czego zacząć ;-p

Blog Erica Lipperta jest zawsze dobry na cytat:

To kolejny powód, dla którego zmienne typy wartości są złe. Staraj się zawsze, aby typy wartości były niezmienne.

Po pierwsze, dość łatwo tracisz zmiany ... na przykład, wyciągając rzeczy z listy:

Foo foo = list[0];
foo.Name = "abc";

co to zmieniło? Nic użytecznego ...

To samo z właściwościami:

myObj.SomeProperty.Size = 22; // the compiler spots this one

zmuszając cię do zrobienia:

Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;

mniej krytycznie pojawia się problem z rozmiarem; obiekty zmienne mają zwykle wiele właściwości; ale jeśli masz strukturę z dwoma ints, a string, a DateTimei a bool, możesz bardzo szybko wypalić dużo pamięci. Dzięki klasie wielu dzwoniących może udostępnić odwołanie do tego samego wystąpienia (odwołania są małe).

Marc Gravell
źródło
5
No tak, ale kompilator jest po prostu głupi. Niedopuszczenie do przypisania do elementów struktury własności było IMHO głupią decyzją projektową, ponieważ jest dozwolone dla ++operatora. W takim przypadku kompilator po prostu zapisuje jawne przypisanie zamiast krzątania się programisty.
Konrad Rudolph
12
@Konrad: myObj.SomeProperty.Size = 22 zmodyfikuje KOPIĘ myObj.SomeProperty. Kompilator ratuje Cię przed oczywistym błędem. I NIE jest dozwolone dla ++.
Lucas
@Lucas: Cóż, kompilator Mono C # na pewno na to pozwala - ponieważ nie mam systemu Windows, nie mogę sprawdzić kompilatora Microsoftu.
Konrad Rudolph,
6
@Konrad - z jedną mniejszą pośrednią powinna działać; jest to „mutowanie wartości czegoś, co istnieje tylko jako wartość przejściowa na stosie i która wkrótce wyparuje w nicość”, co jest przypadkiem, który jest blokowany.
Marc Gravell
2
@Marc Gravell: W poprzednim fragmencie kodu otrzymujesz „Foo”, którego nazwa to „abc” i którego inne atrybuty to te z Listy [0], bez zakłócania Listy [0]. Gdyby Foo była klasą, konieczne byłoby jej sklonowanie, a następnie zmiana kopii. Moim zdaniem dużym problemem związanym z rozróżnieniem między typem wartości a klasą jest użycie „”. operator do dwóch celów. Gdybym miał moje druty, klasy mogłyby obsługiwać oba „.” i „->” dla metod i właściwości, ale normalna semantyka dla „.” właściwością byłoby utworzenie nowej instancji ze zmodyfikowanym odpowiednim polem.
supercat,
71

Nie powiedziałbym, że zło, ale zmienność jest często oznaką nadgorliwości ze strony programisty, aby zapewnić maksimum funkcjonalności. W rzeczywistości często nie jest to potrzebne, a to z kolei sprawia, że ​​interfejs jest mniejszy, łatwiejszy w użyciu i trudniejszy w użyciu (= bardziej niezawodny).

Jednym z przykładów są konflikty odczytu / zapisu i zapisu / zapisu w warunkach wyścigu. Po prostu nie mogą wystąpić w niezmiennych strukturach, ponieważ zapis nie jest prawidłową operacją.

Ponadto twierdzę, że zmienność prawie nigdy nie jest potrzebna , programista po prostu myśli , że może to być w przyszłości. Na przykład zmiana daty nie ma sensu. Zamiast tego utwórz nową datę na podstawie starej. Jest to tania operacja, więc wydajność nie jest brana pod uwagę.

Konrad Rudolph
źródło
1
Eric Lippert mówi, że są ... zobacz moją odpowiedź.
Marc Gravell
42
Chociaż szanuję Erica Lipperta, nie jest on Bogiem (a przynajmniej jeszcze nie). Wpis na blogu, do którego linkujesz, oraz powyższy post są rozsądnymi argumentami przemawiającymi za tym, aby struktury były niezmienne, ale w rzeczywistości są bardzo słabe jako argumenty przemawiające za tym, aby nigdy nie używać zmiennych. Ten post ma jednak +1.
Stephen Martin
2
Opracowując w języku C #, od czasu do czasu potrzebujesz zmienności - szczególnie w przypadku modelu biznesowego, w którym chcesz, aby strumieniowanie itp. Działało płynnie z istniejącymi rozwiązaniami. Napisałem artykuł o tym, jak pracować z danymi zmiennymi ORAZ niezmiennymi, rozwiązując większość problemów związanych ze zmiennością (mam nadzieję): rickyhelgesson.wordpress.com/2012/07/17/…
Ricky Helgesson
3
@StephenMartin: Struktury enkapsulujące pojedynczą wartość często powinny być niezmienne, ale struktury są zdecydowanie najlepszym medium do enkapsulacji ustalonych zbiorów niezależnych, ale powiązanych zmiennych (takich jak współrzędne X i Y punktu), które nie mają „tożsamości” jako Grupa. Struktury, które są używane do tego celu, powinny zasadniczo ujawniać swoje zmienne jako pola publiczne. Uważałbym, że bardziej odpowiednie jest użycie klasy do struktury do takich celów, jest po prostu błędne. Niezmienne klasy są często mniej wydajne, a klasy zmienne często mają straszną semantykę.
supercat
2
@StephenMartin: Rozważmy na przykład metodę lub właściwość, która ma zwrócić sześć floatskładników transformacji graficznej. Jeśli taka metoda zwraca strukturę pola z odsłoniętymi sześcioma komponentami, oczywiste jest, że modyfikacja pól struktury nie zmodyfikuje obiektu graficznego, z którego została otrzymana. Jeśli taka metoda zwraca zmienny obiekt klasy, być może zmiana jego właściwości zmieni podstawowy obiekt graficzny, a może nie - nikt tak naprawdę nie wie.
supercat
48

Zmienne struktury nie są złe.

Są absolutnie niezbędne w warunkach wysokiej wydajności. Na przykład, gdy linie pamięci podręcznej lub zbieranie śmieci stają się wąskim gardłem.

Nie nazwałbym użycia niezmiennej struktury w tych doskonale uzasadnionych przypadkach użycia „złem”.

Widzę, że punkt składnia C # 's nie pomaga odróżnić dostęp członka typu wartości lub typu referencyjnego, więc jestem wszystko dla preferujących niezmienne konstrukcjom, które egzekwować niezmienność, przez zmienny konstrukcjom.

Jednak zamiast po prostu oznaczać niezmienne struktury jako „złe”, radziłbym objąć ten język i opowiadać się za bardziej pomocną i konstruktywną zasadą.

Na przykład: „struktury są typami wartości, które są kopiowane domyślnie. Potrzebujesz odniesienia, jeśli nie chcesz ich kopiować” lub „najpierw spróbuj pracować z strukturami tylko do odczytu” .

JE42
źródło
8
Sądzę również, że jeśli ktoś chce przymocować stały zestaw zmiennych wraz z taśmą klejącą, aby ich wartości mogły być przetwarzane lub przechowywane osobno lub jako jednostka, o wiele bardziej sensowne jest zwrócenie się do kompilatora o przymocowanie stałego zestawu zmiennych zmienne razem (tj. deklarują pole structpubliczne), niż zdefiniować klasę, której można użyć niezdarnie, aby osiągnąć te same cele lub dodać wiązkę śmieci do struktury, aby emulować taką klasę (zamiast mieć ją zachowują się jak zestaw zmiennych sklejonych razem z taśmą klejącą, a tego naprawdę się chce)
supercat
41

Struktury z publicznymi zmiennymi polami lub właściwościami nie są złe.

Metody struktur (w odróżnieniu od ustawiaczy właściwości), które mutują „to”, są nieco złe, tylko dlatego, że .net nie zapewnia sposobu ich odróżnienia od metod, które tego nie robią. Metody struktur, które nie mutują „tego”, powinny być wywoływalne nawet na strukturach tylko do odczytu, bez potrzeby defensywnego kopiowania. Metody mutujące „to” nie powinny być w ogóle wywoływalne w strukturach tylko do odczytu. Ponieważ .net nie chce zabraniać wywoływania metod struktur, które nie modyfikują „tego” przed strukturami tylko do odczytu, ale nie chce zezwalać na mutowanie struktur tylko do odczytu, defensywnie kopiuje struktury w trybie do odczytu tylko konteksty, prawdopodobnie najgorsze z obu światów.

Jednak pomimo problemów z obsługą metod auto-mutacji w kontekstach tylko do odczytu, zmienne struktury często oferują semantykę znacznie przewyższającą modyfikowalne typy klas. Rozważ następujące trzy sygnatury metod:

struct PointyStruct {public int x, y, z;};
klasa PointyClass {public int x, y, z;};

void Method1 (PointyStruct foo);
void Method2 (ref PointyStruct foo);
void Method3 (PointyClass foo);

Dla każdej metody odpowiedz na następujące pytania:

  1. Zakładając, że metoda nie używa żadnego „niebezpiecznego” kodu, czy mogłaby modyfikować foo?
  2. Jeśli przed wywołaniem metody nie istnieją żadne zewnętrzne odniesienia do „foo”, czy może istnieć zewnętrzne odniesienie po?

Odpowiedzi:

Pytanie 1:
Method1()nie (jasne zamiary)
Method2() : tak (jasne zamiary)
Method3() : tak (niepewne zamiary)
Pytanie 2
Method1():: nie
Method2(): nie (chyba że niebezpieczne)
Method3() : tak

Metoda 1 nie może modyfikować foo i nigdy nie otrzymuje referencji. Metoda 2 pobiera krótkotrwałe odwołanie do foo, którego może użyć modyfikować pola foo dowolną liczbę razy, w dowolnej kolejności, dopóki nie zwróci, ale nie może zachować tego odwołania. Przed powrotem metody Method2, chyba że użyje niebezpiecznego kodu, wszelkie kopie, które mogły zostać wykonane z jej odwołania „foo”, znikną. Method3, w przeciwieństwie do Method2, otrzymuje obiecująco współdzielone odniesienie do foo i nie wiadomo, co może z tym zrobić. Może w ogóle nie zmienić foo, może zmienić foo, a następnie powrócić lub może odwoływać się do foo do innego wątku, który może mutować go w dowolny arbitralny sposób w dowolnym przyszłym czasie.

Tablice struktur oferują cudowną semantykę. Biorąc pod uwagę RectArray [500] typu Rectangle, jest jasne i oczywiste, jak np. Skopiować element 123 do elementu 456, a następnie jakiś czas później ustawić szerokość elementu 123 do 555, bez zakłócania elementu 456. "RectArray [432] = RectArray [321 ]; ...; RectArray [123] .Width = 555; ". Wiedząc, że Prostokąt jest strukturą z polem całkowitym o nazwie Szerokość, powiesz wszystko, co musisz wiedzieć o powyższych instrukcjach.

Załóżmy teraz, że RectClass była klasą z tymi samymi polami co Rectangle, a jedna chciała wykonać te same operacje na RectClassArray [500] typu RectClass. Być może tablica ma pomieścić 500 wstępnie zainicjowanych niezmiennych odwołań do zmiennych obiektów RectClass. w takim przypadku właściwym kodem byłoby coś w rodzaju „RectClassArray [321] .SetBounds (RectClassArray [456]); ...; RectClassArray [321] .X = 555;”. Być może zakłada się, że tablica przechowuje instancje, które się nie zmienią, więc właściwy kod będzie bardziej podobny do: „RectClassArray [321] = RectClassArray [456]; ...; RectClassArray [321] = New RectClass (RectClassArray [321 ]); RectClassArray [321] .X = 555; " Aby wiedzieć, co należy zrobić, trzeba by dowiedzieć się znacznie więcej o RectClass (np. Czy obsługuje on konstruktor kopii, metodę kopiowania z itp.) ) i zamierzone użycie tablicy. Nigdzie nie jest tak czysty, jak użycie struktury.

Dla pewności nie istnieje żaden przyjemny sposób, by żadna klasa kontenerów inna niż tablica oferowała czystą semantykę tablicy struct. Najlepszą rzeczą, jaką można zrobić, jeśli chce się zaindeksować kolekcję za pomocą np. Łańcucha, byłoby prawdopodobnie zaoferowanie ogólnej metody „ActOnItem”, która zaakceptowałaby ciąg dla indeksu, ogólny parametr i delegata, który zostałby przekazany przez odniesienie zarówno do parametru ogólnego, jak i elementu kolekcji. Pozwoliłoby to na prawie taką samą semantykę jak tablice struktur, ale jeśli nie da się przekonać ludzi vb.net i C # do zaoferowania ładnej składni, kod będzie wyglądał niezgrabnie, nawet jeśli jest to dość wydajne (przekazanie parametru ogólnego pozwalają na użycie statycznego delegata i unikną potrzeby tworzenia tymczasowych instancji klasy).

Osobiście jestem zirytowany nienawiścią Erica Lipperta i in. wyrzut dotyczący zmiennych typów wartości. Oferują znacznie czystszą semantykę niż rozrzucone typy referencyjne, które są używane wszędzie. Pomimo niektórych ograniczeń związanych ze wsparciem .net dla typów wartości, istnieje wiele przypadków, w których zmienne typy wartości lepiej pasują niż jakikolwiek inny rodzaj bytu.

supercat
źródło
1
@Ron Warholic: nie jest oczywiste, że SomeRect jest prostokątem. Może to być jakiś inny typ, który może być domyślnie rzutem czcionki od Rectangle. Chociaż jedynym typem zdefiniowanym przez system, który może być domyślnie rzutowaniem typu z Rectangle, jest RectangleF, a kompilator skurczyłby się, gdyby ktoś próbował przekazać pola RectangleF do konstruktora Rectangle (ponieważ te pierwsze są Pojedyncze, a druga Liczba całkowita) , mogą istnieć struktury zdefiniowane przez użytkownika, które pozwalają na takie niejawne typowania. BTW, pierwsza instrukcja działałaby równie dobrze, niezależnie od tego, czy SomeRect był Rectangle czy RectangleF.
supercat
Wszystko, co pokazałeś, to to, że w wymyślonym przykładzie uważasz, że jedna metoda jest bardziej przejrzysta. Jeśli weźmiemy twój przykład Rectangle, mógłbym z łatwością wymyślić wspólną sytuację, w której masz bardzo niejasne zachowanie. Weź pod uwagę, że WinForms implementuje zmienny Rectangletyp używany we właściwości formularza Bounds. Jeśli chcę zmienić granice, chciałbym użyć twojej ładnej składni: form.Bounds.X = 10;to jednak nie zmienia dokładnie nic w formularzu (i generuje piękny błąd informujący o tym). Niespójność jest zmorą programowania i dlatego potrzebna jest niezmienność.
Ron Warholic
3
@Ron Warholic: BTW, ja lubię być w stanie powiedzieć "form.Bounds.X = 10;" i po prostu działa, ale system nie zapewnia żadnego czystego sposobu na zrobienie tego. Konwencja dotycząca ujawniania właściwości typu wartości jako metod akceptujących oddzwanianie może oferować znacznie czystszy, wydajny i możliwy do poprawnego poprawienia kod niż jakiekolwiek podejście wykorzystujące klasy.
supercat
4
Ta odpowiedź jest o wiele bardziej wnikliwa niż kilka najczęściej głosowanych odpowiedzi. To absurdalne, że argument przeciwko zmiennym typom wartości opiera się na pojęciu „tego, czego się spodziewasz” po połączeniu aliasingu i mutacji. To straszna rzecz zrobić byle jak !
Eamon Nerbonne
2
@ supercat: Kto wie, może ta funkcja zwrotu zwrotów, o której mówią w C # 7, może obejmować tę bazę (właściwie nie przyjrzałem się jej szczegółowo, ale na pozór brzmi podobnie).
Eamon Nerbonne
24

Typy wartości zasadniczo reprezentują niezmienne pojęcia. Fx, nie ma sensu mieć wartości matematycznych, takich jak liczba całkowita, wektor itp., A następnie móc ją modyfikować. To by było jak przedefiniowanie znaczenia wartości. Zamiast zmieniać typ wartości, bardziej sensowne jest przypisanie innej unikalnej wartości. Pomyśl o tym, że typy wartości są porównywane poprzez porównanie wszystkich wartości jego właściwości. Chodzi o to, że jeśli właściwości są takie same, to jest to ta sama uniwersalna reprezentacja tej wartości.

Jak wspomina Konrad, zmiana daty nie ma sensu, ponieważ wartość reprezentuje ten unikalny punkt w czasie, a nie instancję obiektu czasu, który ma zależność od stanu lub kontekstu.

Mam nadzieję, że ma to dla ciebie jakiś sens. Z pewnością chodzi bardziej o koncepcję, którą próbujesz uchwycić za pomocą typów wartości, niż o praktyczne szczegóły.

Morten Christiansen
źródło
Cóż, oni powinni reprezentować niezmienne koncepcje, przynajmniej ;-P
Marc Gravell
3
Cóż, przypuszczam, że mogliby unieruchomić System.Drawing.Point, ale byłby to poważny błąd projektowy IMHO. Myślę, że punkty są w rzeczywistości typem wartości archetypowej i są zmienne. I nie powodują żadnych problemów dla nikogo poza naprawdę wczesnym programowaniem 101 początkujących.
Stephen Martin
3
Zasadniczo uważam, że punkty powinny być niezmienne, ale jeśli sprawia, że ​​ten typ jest trudniejszy lub mniej elegancki w użyciu, to oczywiście należy to również wziąć pod uwagę. Nie ma sensu mieć konstrukcji kodu, które wspierają najlepsze zasady, jeśli nikt nie chce ich używać;)
Morten Christiansen
3
Typy wartości są przydatne do reprezentowania prostych niezmiennych koncepcji, ale struktury pola narażonego są najlepszymi typami do przechowywania lub przekazywania małych stałych zestawów powiązanych, ale niezależnych wartości (takich jak współrzędne punktu). Miejsce przechowywania takiego typu wartości zawiera wartości jego pól i nic więcej. Natomiast miejsce przechowywania typu zmiennego odniesienia może być użyte w celu utrzymania stanu przedmiotu zmiennego, ale także zawiera tożsamość wszystkich innych odniesień w całym wszechświecie, które istnieją dla tego samego obiektu.
supercat
4
„Typy wartości zasadniczo reprezentują niezmienne pojęcia”. Nie, nie robią tego. Jedną z najstarszych i najbardziej przydatnych aplikacji zmiennej typu wartości jest intiterator, który byłby całkowicie bezużyteczny, gdyby był niezmienny. Myślę, że łączysz kompilatory / implementacje typów wykonawczych „wartości” ze „zmiennymi wpisanymi do typu wartości” - ta ostatnia z pewnością może zostać zmieniona na dowolną z możliwych wartości.
Slipp D. Thompson,
19

Istnieje kilka innych przypadków narożnych, które mogą prowadzić do nieprzewidzianych zachowań z punktu widzenia programisty.

Niezmienne typy wartości i pola tylko do odczytu

    // Simple mutable structure. 
    // Method IncrementI mutates current state.
    struct Mutable
    {
        public Mutable(int i) : this() 
        {
            I = i;
        }

        public void IncrementI() { I++; }

        public int I { get; private set; }
    }

    // Simple class that contains Mutable structure
    // as readonly field
    class SomeClass 
    {
        public readonly Mutable mutable = new Mutable(5);
    }

    // Simple class that contains Mutable structure
    // as ordinary (non-readonly) field
    class AnotherClass 
    {
        public Mutable mutable = new Mutable(5);
    }

    class Program
    {
        void Main()
        {
            // Case 1. Mutable readonly field
            var someClass = new SomeClass();
            someClass.mutable.IncrementI();
            // still 5, not 6, because SomeClass.mutable field is readonly
            // and compiler creates temporary copy every time when you trying to
            // access this field
            Console.WriteLine(someClass.mutable.I);

            // Case 2. Mutable ordinary field
            var anotherClass = new AnotherClass();
            anotherClass.mutable.IncrementI();

            // Prints 6, because AnotherClass.mutable field is not readonly
            Console.WriteLine(anotherClass.mutable.I);
        }
    }

Zmienne typy wartości i tablica

Załóżmy, że mamy tablicę naszej Mutablestruktury i wywołujemy IncrementImetodę dla pierwszego elementu tej tablicy. Jakiego zachowania oczekujesz od tej rozmowy? Czy powinno to zmienić wartość tablicy, czy tylko kopię?

    Mutable[] arrayOfMutables = new Mutable[1];
    arrayOfMutables[0] = new Mutable(5);

    // Now we actually accessing reference to the first element
    // without making any additional copy
    arrayOfMutables[0].IncrementI();

    // Prints 6!!
    Console.WriteLine(arrayOfMutables[0].I);

    // Every array implements IList<T> interface
    IList<Mutable> listOfMutables = arrayOfMutables;

    // But accessing values through this interface lead
    // to different behavior: IList indexer returns a copy
    // instead of an managed reference
    listOfMutables[0].IncrementI(); // Should change I to 7

    // Nope! we still have 6, because previous line of code
    // mutate a copy instead of a list value
    Console.WriteLine(listOfMutables[0].I);

Zmienne struktury nie są złe, o ile ty i reszta zespołu dobrze rozumiecie, co robicie. Ale jest zbyt wiele przypadków narożnych, w których zachowanie programu byłoby inne niż oczekiwano, co może prowadzić do subtelnych, trudnych do wyprodukowania i trudnych do zrozumienia błędów.

Siergiej Cieplakow
źródło
5
Co by się stało, gdyby języki .net miały nieco lepszą obsługę typów wartości, byłoby zabronione metodom struct mutowanie „tego”, chyba że są one jawnie zadeklarowane jako takie, a metody, które są tak zadeklarowane, powinny być zabronione tylko do odczytu konteksty. Tablice zmiennych struktur dają użyteczną semantykę, której nie można skutecznie osiągnąć innymi sposobami.
supercat
2
są to dobre przykłady bardzo subtelnych problemów, które powstałyby ze zmiennych struktur. Nie spodziewałbym się takiego zachowania. Dlaczego tablica daje ci odniesienie, a interfejs daje ci wartość? Pomyślałbym, że poza wartościami przez cały czas (czego tak naprawdę się spodziewałbym), byłoby przynajmniej na odwrót: interfejs z referencjami; tablice podające wartości ...
Dave Cousineau
@ Sahuagin: Niestety nie ma standardowego mechanizmu, za pomocą którego interfejs może ujawniać referencje. Istnieją sposoby .net może pozwolić na bezpieczne wykonanie takich czynności (np. Poprzez zdefiniowanie specjalnej struktury „ArrayRef <T>” zawierającej T[]indeks i liczbę całkowitą oraz zapewnienie, że dostęp do właściwości typu ArrayRef<T>będzie interpretowany jako dostęp do odpowiedniego elementu tablicy) [jeśli klasa chciała ujawnić ArrayRef<T>dla dowolnego innego celu, mogłaby zapewnić metodę - w przeciwieństwie do właściwości - do jej odzyskania]. Niestety nie ma takich przepisów.
supercat
2
O mój ... to sprawia, że ​​zmienne struktury są cholernie złe!
nawfal
1
Podoba mi się ta odpowiedź, ponieważ zawiera bardzo cenne informacje, które nie są oczywiste. Ale tak naprawdę, jak twierdzą niektórzy, nie jest to argument przeciwko zmiennym strukturom. Tak, tutaj widzimy „rozpacz”, jak ująłby to Eric, ale źródłem tej rozpaczy nie jest zmienność. Źródłem rozpaczy są metody mutacji struktur . (Jeśli chodzi o to, dlaczego tablice i listy zachowują się inaczej, to dlatego, że jeden jest w zasadzie operatorem, który oblicza adres pamięci, a drugi jest właściwością. Zasadniczo wszystko staje się jasne, gdy zrozumiesz, że „odwołanie” jest wartością adresu .)
AnorZaken
18

Jeśli kiedykolwiek programowałeś w języku takim jak C / C ++, struktury mogą być używane jako zmienne. Przekaż je z ref, wokół i nic nie może pójść źle. Jedyny problem, jaki znalazłem, to ograniczenia kompilatora C # i, w niektórych przypadkach, nie jestem w stanie zmusić głupiej rzeczy do użycia odwołania do struktury zamiast do kopii (na przykład, gdy struct jest częścią klasy C # ).

Zmienne struktury nie są złe, C # uczynił je złymi. Cały czas używam zmiennych struktur w C ++ i są one bardzo wygodne i intuicyjne. W przeciwieństwie do C # zmusił mnie do całkowitego porzucenia struktur jako członków klas ze względu na sposób, w jaki obsługują one obiekty. Ich wygoda kosztowała nas naszą.

ThunderGr
źródło
Posiadanie pól klas typów struktur może być często bardzo użytecznym wzorcem, choć wprawdzie istnieją pewne ograniczenia. Wydajność ulegnie pogorszeniu, jeśli ktoś użyje właściwości zamiast pól lub readonlyzastosuje, ale jeśli uniknie się tych rzeczy, pola klas typów struktur są w porządku. Jedynym naprawdę fundamentalnym ograniczeniem struktur jest to, że pole struktury typu klasy mutable int[]może zawierać w sobie tożsamość lub niezmienny zestaw wartości, ale nie może być użyte do enkapsulacji wartości zmiennych bez zakapsułkowania niepożądanej tożsamości.
supercat,
13

Jeśli trzymasz się struktur, dla których są przeznaczone (w C #, Visual Basic 6, Pascal / Delphi, C ++ typu struktur (lub klas), gdy nie są one używane jako wskaźniki), przekonasz się, że struktura nie jest niczym więcej niż zmienną złożoną . Oznacza to: potraktujesz je jako spakowany zestaw zmiennych pod wspólną nazwą (zmienna rekordowa, z której odwołujesz się do członków).

Wiem, że wprowadzałoby to w błąd wielu ludzi głęboko przyzwyczajonych do OOP, ale to nie wystarczający powód, by twierdzić, że takie rzeczy są z natury złe, jeśli są właściwie stosowane. Niektóre struktury są niezmienne, jak zamierzają (tak jest w przypadku Pythonanamedtuple ), ale jest to kolejny paradygmat do rozważenia.

Tak: struktury wymagają dużej ilości pamięci, ale nie będzie to więcej pamięci, wykonując:

point.x = point.x + 1

w porównaniu do:

point = Point(point.x + 1, point.y)

Zużycie pamięci będzie co najmniej takie samo lub nawet większe w przypadku niewymiennym (chociaż przypadek ten byłby tymczasowy, dla bieżącego stosu, w zależności od języka).

Wreszcie struktury to struktury , a nie obiekty. W POO główną właściwością obiektu jest jego tożsamość , która przez większość czasu jest nie większa niż adres pamięci. Struct oznacza strukturę danych (niepoprawny obiekt, więc i tak nie mają tożsamości), a dane można modyfikować. W innych językach record (zamiast struct , jak ma to miejsce w przypadku Pascala) jest słowem i ma ten sam cel: po prostu zmienna rekordu danych, przeznaczona do odczytu z plików, modyfikowana i zrzucana do plików (to jest główny używać, aw wielu językach można nawet zdefiniować wyrównanie danych w rekordzie, choć niekoniecznie tak jest w przypadku poprawnie nazywanych Obiektów).

Chcesz dobry przykład? Struktury służą do łatwego odczytu plików. Python ma tę bibliotekę, ponieważ ponieważ jest zorientowana obiektowo i nie obsługuje struktur, musiał zaimplementować ją w inny sposób, co jest nieco brzydkie. Języki implementujące struktury mają tę funkcję ... wbudowaną. Spróbuj odczytać nagłówek mapy bitowej z odpowiednią strukturą w językach takich jak Pascal lub C. Będzie to łatwe (jeśli struktura jest poprawnie zbudowana i wyrównana; w Pascal nie używałbyś dostępu opartego na rekordach, ale funkcje do odczytu dowolnych danych binarnych). Zatem w przypadku plików i bezpośredniego (lokalnego) dostępu do pamięci struktury są lepsze niż obiekty. Na dzień dzisiejszy jesteśmy przyzwyczajeni do JSON i XML, dlatego też zapominamy o użyciu plików binarnych (a jako efekt uboczny użycie struktur). Ale tak: istnieją i mają cel.

Nie są źli. Po prostu używaj ich we właściwym celu.

Jeśli myślisz w kategoriach młotów, będziesz chciał traktować śruby jak gwoździe, aby znaleźć śruby, które są trudniejsze do wbicia się w ścianę, i to będzie wina śrub, a one będą złymi.

Luis Masuelli
źródło
12

Wyobraź sobie, że masz tablicę 1 000 000 struktur. Każda struktura reprezentuje kapitał z takimi rzeczami, jak bid_price, offer_price (być może dziesiętne) i tak dalej, to jest tworzone przez C # / VB.

Wyobraź sobie, że tablica jest tworzona w bloku pamięci przydzielonym w niezarządzanej stercie, aby jakiś inny natywny wątek kodu mógł jednocześnie uzyskiwać dostęp do tablicy (być może jakiś matematyczny kod o wysokiej wydajności).

Wyobraź sobie, że kod C # / VB nasłuchuje informacji rynkowej o zmianach cen, kod ten może mieć dostęp do jakiegoś elementu tablicy (dla dowolnego bezpieczeństwa), a następnie zmodyfikować niektóre pola ceny.

Wyobraź sobie, że robi się to dziesiątki, a nawet setki tysięcy razy na sekundę.

Cóż, spójrzmy prawdzie w oczy, w tym przypadku naprawdę chcemy, aby te struktury były zmienne, muszą być, ponieważ są udostępniane przez inny natywny kod, więc tworzenie kopii nie pomoże; muszą być, ponieważ tworzenie kopii około 120-bajtowej struktury przy tych szybkościach jest szaleństwem, szczególnie gdy aktualizacja może faktycznie wpłynąć tylko na jeden bajt lub dwa.

Hugo

Hugo
źródło
3
To prawda, ale w tym przypadku powodem użycia struktury jest to, że jest to narzucone projektowi aplikacji przez zewnętrzne ograniczenia (wynikające z użycia kodu natywnego). Wszystko, co opisujesz na temat tych obiektów, sugeruje, że powinny to być klasy w języku C # lub VB.NET.
Jon Hanna,
6
Nie jestem pewien, dlaczego niektórzy uważają, że powinny to być przedmioty klasowe. Jeśli wszystkie gniazda tablic są wypełnione referencjami, różne instancje, użycie typu klasy doda dodatkowe dwanaście lub dwadzieścia cztery bajty do wymagań pamięci, a sekwencyjny dostęp do tablicy referencji do obiektów klasy może być znacznie wolniejszy niż sekwencyjny dostęp tablica struktur.
supercat
9

Kiedy coś można zmutować, zyskuje poczucie tożsamości.

struct Person {
    public string name; // mutable
    public Point position = new Point(0, 0); // mutable

    public Person(string name, Point position) { ... }
}

Person eric = new Person("Eric Lippert", new Point(4, 2));

Ponieważ Personjest zmienna, bardziej naturalne jest myślenie o zmianie pozycji Erica niż klonowanie Erica, przenoszenie klonu i niszczenie oryginału . Obie operacje zmieniłyby zawartość eric.position, ale jedna jest bardziej intuicyjna niż druga. Podobnie, bardziej intuicyjne jest przekazywanie Ericowi (jako odniesienie) metod jego modyfikacji. Podanie metody klona Erica prawie zawsze będzie zaskakujące. Każdy, kto chce mutować, Personmusi pamiętać, aby poprosić o odniesienie doPerson przeciwnym razie zrobi coś złego.

Jeśli sprawisz, że typ będzie niezmienny, problem zniknie; jeśli nie mogę zmodyfikować eric, nie ma dla mnie znaczenia, czy otrzymam, ericczy klon eric. Mówiąc bardziej ogólnie, typ jest bezpieczny do przekazania przez wartość, jeśli cały jego obserwowalny stan jest utrzymywany w elementach, które są:

  • niezmienny
  • typy referencyjne
  • bezpieczny dla wartości

Jeśli te warunki są spełnione, zmienna wartość typu zachowuje się jak typ odniesienia, ponieważ płytka kopia nadal pozwoli odbiornikowi modyfikować oryginalne dane.

Intuicyjność niezmiennego Personzależy jednak od tego, co próbujesz zrobić. Jeśli Persontylko reprezentuje zestaw danych o osobie, nie ma w tym nic nieintuicyjnego; Personzmienne naprawdę reprezentują wartości abstrakcyjne , a nie obiekty. (W takim przypadku prawdopodobnie lepiej byłoby zmienić nazwę na PersonData.) JeśliPerson faktycznie modeluje się samą osobę, pomysł ciągłego tworzenia i przenoszenia klonów jest głupi, nawet jeśli unikniesz pułapki myślenia, że ​​modyfikujesz oryginalny. W takim przypadku prawdopodobnie bardziej naturalne byłoby po prostu utworzenie Persontypu referencyjnego (to znaczy klasy).

To prawda, że ​​jak nauczyło nas programowanie funkcjonalne, korzyści z uczynienia wszystkiego niezmiennym (nikt nie może potajemnie trzymać się odniesienia do niego erici mutować go), ale ponieważ nie jest to idiomatyczne w OOP, nadal będzie to nieintuicyjne dla każdego, kto pracuje z twoim kod.

Doval
źródło
3
Twoje zdanie na temat tożsamości jest dobre; warto zauważyć, że tożsamość jest istotna tylko wtedy, gdy istnieje wiele odniesień do czegoś. Jeśli fooposiada jedyne odniesienie do swojego celu w dowolnym miejscu we wszechświecie i nic nie uchwyciło wartości skrótu tożsamości tego obiektu, wówczas pole mutacji foo.Xjest semantycznie równoważne z foowskazywaniem na nowy obiekt, który jest podobny do tego, o którym wcześniej wspominał, ale z Xutrzymywanie pożądanej wartości. W przypadku typów klas generalnie trudno jest ustalić, czy istnieje wiele odwołań do czegoś, ale w przypadku struktur jest to łatwe: nie istnieją.
supercat
1
Jeśli Thingjest typem klasy możliwej do zmodyfikowania, a Thing[] będzie hermetyzować tożsamość obiektu - czy tego się chce, czy nie - chyba że można zapewnić, że żadne Thingz elementów tablicy, do której istnieją odniesienia zewnętrzne, nigdy nie zostanie zmutowane. Jeśli nie chcemy, aby elementy tablicy zawierały tożsamość, należy ogólnie upewnić się, że żadne elementy, do których należą odniesienia, nigdy nie zostaną zmutowane, lub że żadne odniesienia zewnętrzne nigdy nie będą istnieć dla elementów, które posiada [podejścia hybrydowe mogą również działać ]. Żadne z tych podejść nie jest zbyt wygodne. Jeśli Thingjest strukturą, Thing[]hermetyzuje tylko wartości.
supercat
W przypadku obiektów ich tożsamość pochodzi z ich położenia. Instancje typów referencyjnych mają swoją tożsamość dzięki ich lokalizacji w pamięci, a Ty przekazujesz tylko ich tożsamość (referencję), a nie ich dane, podczas gdy typy wartości mają swoją tożsamość w zewnętrznym miejscu, w którym są przechowywane. Tożsamość typu wartości Erica pochodzi tylko od zmiennej, w której jest przechowywany. Jeśli go przepuścisz, straci swoją tożsamość.
IllidanS4 chce, aby Monica wróciła
6

Nie ma to nic wspólnego ze strukturami (i nie z C #), ale w Javie możesz mieć problemy ze zmiennymi obiektami, gdy są one np. Kluczami na mapie mieszającej. Jeśli zmienisz je po dodaniu ich do mapy i zmieni to kod skrótu , mogą się zdarzyć złe rzeczy.

Bombe
źródło
3
To prawda, jeśli używasz klasy jako klucza na mapie.
Marc Gravell
6

Osobiście, kiedy patrzę na kod, wygląda mi to dość niezręcznie:

data.value.set (data.value.get () + 1);

zamiast po prostu

data.value ++; lub data.value = data.value + 1;

Hermetyzacja danych jest przydatna podczas przekazywania klasy i chcesz mieć pewność, że wartość zostanie zmodyfikowana w kontrolowany sposób. Jeśli jednak masz zestaw publiczny i dostajesz funkcje, które niewiele więcej niż ustawiają wartość tego, co kiedykolwiek zostało przekazane, jak to jest ulepszenie w stosunku do zwykłego przekazywania struktury danych publicznych?

Kiedy tworzę prywatną strukturę wewnątrz klasy, stworzyłem tę strukturę, aby zorganizować zestaw zmiennych w jedną grupę. Chcę móc modyfikować tę strukturę w zakresie klasy, nie otrzymywać kopii tej struktury i tworzyć nowych instancji.

Dla mnie to uniemożliwia prawidłowe użycie struktur używanych do organizowania zmiennych publicznych, gdybym chciał kontroli dostępu, użyłbym klasy.

Mikrofon
źródło
1
Prosto do celu! Struktury są jednostkami organizacyjnymi bez ograniczeń kontroli dostępu! Niestety, C # uczynił je bezużytecznymi do tego celu!
ThunderGr
to całkowicie nie ma sensu, ponieważ oba twoje przykłady pokazują modyfikowalne struktury.
vidstige
C # uczynił je bezużytecznymi do tego celu, ponieważ nie jest to celem struktur
Luiz Felipe
6

Istnieje kilka problemów z przykładem Erica Lipperta. Pomyślono o tym, aby zilustrować punkt, w którym struktury są kopiowane i jak może to stanowić problem, jeśli nie będziesz ostrożny. Patrząc na ten przykład, widzę go jako wynik złego nawyku programowania, a nie tak naprawdę problemu z strukturą lub klasą.

  1. Struktura ma mieć tylko członków publicznych i nie powinna wymagać żadnej enkapsulacji. Jeśli tak, to naprawdę powinien to być typ / klasa. Naprawdę nie potrzebujesz dwóch konstrukcji, aby powiedzieć to samo.

  2. Jeśli masz klasę otaczającą strukturę, wywołaj metodę w klasie, aby zmutować strukturę członka. To właśnie zrobiłbym jako dobry nawyk programowania.

Prawidłowe wdrożenie byłoby następujące.

struct Mutable {
public int x;
}

class Test {
    private Mutable m = new Mutable();
    public int mutate()
    { 
        m.x = m.x + 1;
        return m.x;
    }
  }
  static void Main(string[] args) {
        Test t = new Test();
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
    }

Wygląda na to, że jest to problem z nawykami programistycznymi, a nie problem z samą strukturą. Struktury powinny być zmienne, to jest idea i intencja.

Rezultat zmian voila zachowuje się zgodnie z oczekiwaniami:

1 2 3 Naciśnij dowolny klawisz, aby kontynuować. . .

Ramesh Kadambi
źródło
4
Nie ma nic złego w projektowaniu małych nieprzezroczystych struktur, które będą zachowywać się jak niezmienne obiekty klasy; wytyczne MSDN są rozsądne, gdy próbuje się stworzyć coś, co zachowuje się jak obiekt . Struktury są odpowiednie w niektórych przypadkach, w których potrzebne są lekkie rzeczy, które zachowują się jak przedmioty, oraz w przypadkach, gdy potrzebna jest wiązka zmiennych sklejonych razem z taśmą klejącą. Z jakiegoś powodu jednak wiele osób nie zdaje sobie sprawy, że struktury mają dwa różne zastosowania, a wytyczne odpowiednie dla jednej są nieodpowiednie dla drugiej.
supercat
5

Istnieje wiele zalet i wad zmiennych danych. Wadą za milion dolarów jest aliasing. Jeśli ta sama wartość jest używana w wielu miejscach, a jedna z nich ją zmienia, to wygląda na to, że magicznie zmieniła się na inne miejsca, które jej używają. Jest to związane, ale nie identyczne z warunkami wyścigu.

Zaletą miliona dolarów jest czasami modułowość. Zmienny stan pozwala ukryć zmieniające się informacje w kodzie, które nie muszą o tym wiedzieć.

Art of the Tłumacz ustny szczegółowo omawia te kompromisy i podaje kilka przykładów.

Andru Luvisi
źródło
struktury nie są aliasowane w języku c #. Każde zadanie struktury jest kopią.
rekursywny
@recursive: W niektórych przypadkach jest to główna zaleta modyfikowalnych struktur, co sprawia, że ​​wątpię w to, że struktury nie powinny być modyfikowalne. Fakt, że kompilatory czasami niejawnie kopiują struktury, nie zmniejsza użyteczności struktur zmiennych.
supercat,
1

Nie wierzę, że są złe, jeśli są właściwie stosowane. Nie wstawiłbym tego do mojego kodu produkcyjnego, ale zrobiłbym coś w rodzaju próbnych testów strukturalnych, gdzie żywotność struktury jest stosunkowo niewielka.

Korzystając z przykładu Erica, być może chcesz utworzyć drugą instancję tego Erica, ale dokonaj korekt, ponieważ taka jest natura twojego testu (tj. Powielanie, a następnie modyfikowanie). Nie ma znaczenia, co stanie się z pierwszą instancją Erica, jeśli używamy Eric2 do pozostałej części skryptu testowego, chyba że planujesz użyć go jako porównania testowego.

Byłoby to szczególnie przydatne do testowania lub modyfikowania starszego kodu, który płytko definiuje określony obiekt (punkt struktur), ale dzięki niezmiennej strukturze zapobiega to irytującemu użyciu.

ShaunnyBwoy
źródło
Jak widzę, struct ma w sercu wiele zmiennych sklejonych razem z taśmą klejącą. W .NET możliwe jest, że struktura udaje, że jest czymś innym niż wiązka zmiennych sklejonych razem z taśmą klejącą, i sugerowałbym, że gdy jest to praktyczne, typ, który udaje, że jest czymś innym niż wiązka zmiennych sklejonych razem z taśmą klejącą powinien zachowywać się jak zunifikowany obiekt (co dla struktury oznaczałoby niezmienność), ale czasem przydatne jest sklejenie wielu zmiennych razem z taśmą klejącą. Nawet w kodzie produkcyjnym
uważałbym,
... który wyraźnie nie ma semantyki poza „każde pole zawiera ostatnią zapisaną do niego rzecz”, wypychając całą semantykę do kodu, który korzysta ze struktury, niż po to, aby struktura robiła więcej. Biorąc pod uwagę, na przykład, Range<T>typ z elementami Minimumi Maximumpolami typu Toraz kodem Range<double> myRange = foo.getRange();, powinny pochodzić wszelkie gwarancje dotyczące tego, co Minimumi co Maximumzawiera foo.GetRange();. Będąc Rangestrukturą pola narażonego wyjaśniłby, że nie doda żadnego własnego zachowania.
supercat,