Kiedy używasz struktury zamiast klasy? [Zamknięte]

174

Jakie są Twoje ogólne zasady, kiedy używać struktur vs. klas? Zastanawiam się nad definicją tych terminów w języku C #, ale jeśli twój język ma podobne pojęcia, chciałbym również poznać Twoją opinię.

Zwykle używam klas do prawie wszystkiego i używam struktur tylko wtedy, gdy coś jest bardzo uproszczone i powinno być typem wartości, takim jak PhoneNumber lub coś takiego. Ale wydaje się to stosunkowo niewielkim zastosowaniem i mam nadzieję, że są bardziej interesujące przypadki użycia.

RationalGeek
źródło
4
Możliwy duplikat: stackoverflow.com/questions/6267957/struct-vs-class
FrustratedWithFormsDesigner
7
@Frustrated Nie można oznaczyć pytania jako duplikatu czegoś w innej witrynie, chociaż z pewnością jest ono powiązane.
Adam Lear
@Anna Lear: Wiem, głosowałam za migracją, a nie za duplikat. Po migracji można go poprawnie zamknąć jako duplikat .
FrustratedWithFormsDesigner
5
@ Sfrustrowany Nie sądzę, że trzeba go migrować.
Adam Lear
15
To wydaje się być pytaniem o wiele bardziej odpowiednim dla P.SE vs. SO. Dlatego o to zapytałem tutaj.
RationalGeek

Odpowiedzi:

169

Zgodnie z ogólną zasadą struktury powinny być małymi, prostymi (jednopoziomowymi) kolekcjami powiązanych właściwości, które po utworzeniu są niezmienne; do wszystkiego innego użyj klasy.

C # jest fajny, ponieważ struktury i klasy nie mają wyraźnych różnic w deklaracji innych niż słowo kluczowe definiujące; więc jeśli uważasz, że musisz „uaktualnić” strukturę do klasy lub odwrotnie „obniżyć” klasę do struktury, jest to głównie prosta kwestia zmiany słowa kluczowego (istnieje kilka innych problemów; struktury nie mogą wyprowadzić z jakiejkolwiek innej klasy lub typu strukturalnego i nie mogą jawnie zdefiniować domyślnego konstruktora bez parametrów).

Mówię „głównie”, ponieważ ważniejszą rzeczą, jaką należy wiedzieć o strukturach, jest to, że ponieważ są one typami wartości, traktowanie ich jak klasy (typy referencyjne) może skończyć się bólem i połową. W szczególności modyfikowanie właściwości struktury może powodować nieoczekiwane zachowanie.

Załóżmy na przykład, że masz klasę SimpleClass z dwiema właściwościami, A i B. Tworzysz kopię tej klasy, inicjujesz A i B, a następnie przekazujesz instancję do innej metody. Ta metoda dodatkowo modyfikuje A i B. Z powrotem w funkcji wywołującej (tej, która utworzyła instancję), A i B instancji będą miały wartości podane im przez wywoływaną metodę.

Teraz uczynisz to strukturą. Właściwości są nadal zmienne. Wykonujesz te same operacje przy użyciu tej samej składni, co wcześniej, ale teraz nowe wartości A i B nie występują w instancji po wywołaniu metody. Co się stało? Twoja klasa jest teraz strukturą, co oznacza, że ​​jest to typ wartości. Jeśli przekazujesz typ wartości do metody, domyślnym (bez słowa kluczowego out lub ref) jest przekazanie „według wartości”; płytka kopia instancji jest tworzona do użycia przez metodę, a następnie niszczona, gdy metoda jest wykonywana, pozostawiając pierwotną instancję nienaruszoną.

Staje się to jeszcze bardziej mylące, jeśli miałbyś mieć typ referencyjny jako członek swojej struktury (nie jest to zabronione, ale bardzo zła praktyka w praktycznie wszystkich przypadkach); klasa nie zostałaby sklonowana (tylko odwołanie do struktury), więc zmiany w strukturze nie wpłynęłyby na oryginalny obiekt, ale zmiany w podklasie struktury będą wpływać na instancję z kodu wywołującego. Może to bardzo łatwo wprowadzić struktury modyfikowalne w bardzo niespójne stany, które mogą powodować błędy daleko od miejsca, w którym znajduje się prawdziwy problem.

Z tego powodu praktycznie każdy autorytet w C # mówi, aby zawsze uczynić twoje struktury niezmiennymi; zezwalaj konsumentowi na określanie wartości właściwości tylko przy konstruowaniu obiektu i nigdy nie udostępniaj żadnych środków do zmiany wartości tego wystąpienia. Pola tylko do odczytu lub właściwości tylko do odczytu są regułą. Jeśli konsument chce zmienić wartość, może utworzyć nowy obiekt na podstawie wartości starego, z żądanymi zmianami, lub może wywołać metodę, która zrobi to samo. To zmusza ich do traktowania pojedynczego wystąpienia twojej struktury jako jednej konceptualnej „wartości”, niepodzielnej i odmiennej od (ale być może równej) wszystkich innych. Jeśli wykonają operację na „wartości” zapisanej przez Twój typ, otrzymają nową „wartość”, która różni się od ich wartości początkowej,

Dla dobrego przykładu spójrz na typ DateTime. Nie można bezpośrednio przypisać żadnego z pól instancji DateTime; musisz albo utworzyć nową, albo wywołać metodę na istniejącej, która utworzy nową instancję. Wynika to z faktu, że data i godzina są „wartością”, podobnie jak liczba 5, a zmiana liczby 5 powoduje powstanie nowej wartości, która nie jest 5. Tylko dlatego, że 5 + 1 = 6 nie oznacza, że ​​5 to teraz 6 ponieważ dodałeś do niego 1. DateTimes działają w ten sam sposób; 12:00 nie „staje się” 12:01, jeśli dodasz minutę, zamiast tego otrzymasz nową wartość 12:01, która różni się od 12:00. Jeśli jest to logiczny stan rzeczy dla twojego typu (dobre przykłady pojęciowe, które nie są wbudowane w .NET to Pieniądze, Dystans, Waga i inne ilości UOM, w których operacje muszą uwzględniać wszystkie części wartości), następnie użyj struktury i zaprojektuj go odpowiednio. W większości innych przypadków, w których podelementy obiektu powinny być niezależnie zmienne, użyj klasy.

KeithS
źródło
1
Dobra odpowiedź! Wskazałbym jedną z nich na metodologię i książkę „Domain Driven Development”, która wydaje się nieco powiązana z twoją opinią na temat tego, dlaczego warto korzystać z klas lub struktur opartych na koncepcji, którą faktycznie próbujesz wdrożyć
Maxim Popravko
1
Tylko mały szczegół, o którym warto wspomnieć: nie można zdefiniować własnego domyślnego konstruktora w strukturze. Musisz zadowolić się tym, który inicjuje wszystko do wartości domyślnej. Zwykle jest to dość niewielkie, szczególnie w klasie, która jest dobrym kandydatem na strukturę. Będzie to jednak oznaczać, że zmiana klasy na strukturę może oznaczać coś więcej niż tylko zmianę słowa kluczowego.
Laurent Bourgault-Roy,
1
@ LaurentBourgault-Roy - True. Są też inne gotcha; Na przykład struktury nie mogą mieć hierarchii dziedziczenia. Mogą implementować interfejsy, ale nie mogą wywodzić się z niczego innego niż System.ValueType (domyślnie przez to, że są strukturami).
KeithS,
Jako programista JavaScript muszę zadać dwie rzeczy. Czym literały obiektowe różnią się od typowych zastosowań struktur i dlaczego ważne jest, aby struktury były niezmienne?
Erik Reppen,
1
@ErikReppen: W odpowiedzi na oba pytania: Kiedy struktura jest przekazywana do funkcji, jest przekazywana przez wartość. Z klasą przekazujesz odwołanie do klasy (wciąż według wartości). Eric Lippert omawia dlaczego jest to problem: blogs.msdn.com/b/ericlippert/archive/2008/05/14/... . Ostatni akapit KeithS obejmuje również te pytania.
Brian
14

Odpowiedź: „Użyj struktury dla czystych konstrukcji danych i klasy dla obiektów z operacjami” jest zdecydowanie niepoprawna IMO. Jeśli struct posiada dużą liczbę właściwości, klasa jest prawie zawsze bardziej odpowiednia. Microsoft często mówi, z punktu widzenia wydajności, jeśli Twój typ jest większy niż 16 bajtów, powinien być klasą.

https://stackoverflow.com/questions/1082311/why-should-a-net-struct-be-less-than-16-bytes

bytedev
źródło
1
Absolutnie. Głosowałbym, gdybym mógł. Nie rozumiem, skąd pomysł, że struktury są dla danych, a obiekty nie. Struktura w języku C # to tylko mechanizm alokacji na stosie. Kopie całej struktury są przekazywane zamiast tylko kopii wskaźnika obiektu.
mike30,
2
@mike - struktury C # niekoniecznie są przydzielane na stosie.
Lee,
1
@mike Pomysł „struktury są dla danych” prawdopodobnie pochodzi z nawyku nabytego przez wiele lat programowania C. C nie ma klas, a jego struktury to kontenery tylko danych.
Konamiman,
Oczywiście jest co najmniej 3 programistów C (otrzymało pozytywne recenzje) po przeczytaniu odpowiedzi Michaela K. ;-)
bytedev
10

Najważniejszą różnicą między a classa jest structto, co dzieje się w następującej sytuacji:

  Thing thing1 = new Thing ();
  thing1.somePropertyOrField = 5;
  Rzecz rzecz 2 = rzecz 1;
  thing2.somePropertyOrField = 9;

Jaki powinien być wpływ ostatniego stwierdzenia thing1.somePropertyOrField? Jeśli Thingstruct i somePropertyOrFieldjest odsłoniętym polem publicznym, obiekty thing1i thing2zostaną „oderwane” od siebie, więc to ostatnie zdanie nie będzie miało wpływu thing1. Jeśli Thingjest klasą, wtedy thing1i thing2będą do siebie przywiązane, a więc ta ostatnia instrukcja zapisze thing1.somePropertyOrField. Należy użyć struktury w przypadkach, w których poprzednia semantyka miałaby większy sens, i należy użyć klasy w przypadkach, w których druga semantyka miałaby większy sens.

Zauważ, że chociaż niektórzy ludzie radzą, że chęć zrobienia czegoś zmiennego jest argumentem przemawiającym za tym, że jest to klasa, sugerowałbym, że jest odwrotnie: jeśli coś, co istnieje w celu przechowywania niektórych danych, będzie podlegać zmianom, i jeśli nie będzie oczywiste, czy instancje są dołączone do czegokolwiek, rzecz powinna być strukturą (prawdopodobnie z odsłoniętymi polami), aby wyjaśnić, że instancje nie są dołączone do niczego innego.

Rozważmy na przykład stwierdzenia:

  Osoba somePerson = myPeople.GetPerson („123-45-6789”);
  somePerson.Name = "Mary Johnson"; // Był „Mary Smith”

Czy drugie zdanie zmieni informacje w nim przechowywane myPeople? Jeśli Personjest strukturą pola narażonego, nie będzie, a fakt, że nie będzie oczywistą konsekwencją bycia strukturą pola narażonego ; jeśli Personjest strukturą i chce się zaktualizować myPeople, to oczywiście trzeba coś zrobić myPeople.UpdatePerson("123-45-6789", somePerson). Jeśli Personjest to klasa, może być znacznie trudniej ustalić, czy powyższy kod nigdy nie zaktualizuje zawartości MyPeople, zawsze ją zaktualizuje, a czasem zaktualizuje.

Jeśli chodzi o pogląd, że struktury powinny być „niezmienne”, ogólnie nie zgadzam się. Istnieją uzasadnione przypadki użycia „niezmiennych” struktur (gdzie niezmienniki są wymuszane w konstruktorze), ale wymaganie, aby cała struktura musiała zostać przepisana za każdym razem, gdy jakakolwiek część jej zmian jest niewygodna, marnotrawna i bardziej skłonna do powodowania błędów niż po prostu ujawnianie pola bezpośrednio. Na przykład rozważmy PhoneNumberstrukturę, której pola obejmują między innymi, AreaCodei Exchange, i załóżmy, że ma List<PhoneNumber>. Następujący efekt powinien być dość wyraźny:

  for (int i = 0; i <myList.Count; i ++)
  {
    PhoneNumber theNumber = myList [i];
    if (theNumber.AreaCode == "312")
    {
      string newExchange = "";
      if (new312to708Exchanges.TryGetValue (theNumber.Exchange), out newExchange)
      {
        theNumber.AreaCode = "708";
        theNumber.Exchange = newExchange;
        myList [i] = theNumber;
      }
    }
  }

Zauważ, że nic w powyższym kodzie nie wie ani nie troszczy się o żadne pola PhoneNumberinne niż AreaCodei Exchange. Gdyby PhoneNumberbył tak zwaną „niezmienną” strukturą, konieczne byłoby albo dostarczenie withXXmetody dla każdego pola, która zwróciłaby nową instancję struktury, która posiadałaby przekazaną wartość we wskazanym polu, albo byłaby konieczna dla kodu takiego jak wyżej, aby wiedzieć o każdym polu w strukturze. Niezupełnie atrakcyjne.

BTW, istnieją co najmniej dwa przypadki, w których struktury mogą zasadnie zawierać odniesienia do typów zmiennych.

  1. Semantyka struktury wskazuje, że przechowuje ona tożsamość przedmiotowego obiektu, a nie skrót do przechowywania właściwości obiektu. Na przykład `KeyValuePair` przechowuje tożsamości niektórych przycisków, ale nie oczekuje się, że będą przechowywać trwałe informacje o pozycjach tych przycisków, stanach podświetlenia itp.
  2. Struktura wie, że posiada jedyne odniesienie do tego obiektu, nikt inny nie otrzyma odniesienia, a wszelkie mutacje, które kiedykolwiek zostaną wykonane na tym obiekcie, zostaną wykonane, zanim odniesienie do niego zostanie zapisane w dowolnym miejscu.

W poprzednim scenariuszu TOŻSAMOŚĆ obiektu będzie niezmienna; w drugim przypadku klasa zagnieżdżonego obiektu może nie wymuszać niezmienności, ale struktura przechowująca odwołanie to zrobi.

supercat
źródło
7

Zwykle używam struktur tylko wtedy, gdy potrzebna jest jedna metoda, przez co klasa wydaje się zbyt „ciężka”. Dlatego czasami traktuję konstrukcje jako lekkie przedmioty. UWAGA: to nie zawsze działa i nie zawsze jest najlepszą rzeczą, więc naprawdę zależy od sytuacji!

DODATKOWE ZASOBY

Aby uzyskać bardziej formalne wyjaśnienia, MSDN mówi: http://msdn.microsoft.com/en-us/library/ms229017.aspx Ten link weryfikuje to, o czym wspomniałem powyżej, struktury powinny być używane tylko do przechowywania niewielkiej ilości danych.

zrozpaczony
źródło
5
Pytania w innej witrynie (nawet jeśli ta witryna jest przepełniona stosem ) nie liczą się jako duplikaty. Rozwiń swoją odpowiedź, aby zawierała rzeczywistą odpowiedź, a nie tylko linki do innych miejsc. Jeśli linki kiedykolwiek się zmienią, twoja odpowiedź w formie, w jakiej jest teraz napisana, będzie bezużyteczna, a my chcemy zachować informacje i uczynić programistów tak samodzielnymi, jak to możliwe. Dzięki!
Adam Lear
2
@Anna Lear Dzięki za poinformowanie mnie, odtąd będę o tym pamiętać!
rrazd
2
-1 „Zwykle używam struktur tylko wtedy, gdy potrzebna jest jedna metoda, przez co klasa wydaje się zbyt„ ciężka ”.” ... ciężki? Co to tak naprawdę oznacza? ... i czy naprawdę mówisz, że tylko dlatego, że istnieje jedna metoda, to prawdopodobnie powinna to być struktura?
bytedev,
2

Użyj struktury dla czystych konstrukcji danych i klasy dla obiektów z operacjami.

Jeśli struktura danych nie wymaga kontroli dostępu i nie ma żadnych specjalnych operacji innych niż get / set, użyj struktury. To sprawia, że ​​oczywiste jest, że cała ta struktura jest pojemnikiem na dane.

Jeśli twoja struktura ma operacje, które modyfikują strukturę w bardziej złożony sposób, użyj klasy. Klasa może także wchodzić w interakcje z innymi klasami poprzez argumenty do metod.

Pomyśl o tym jak o różnicy między cegłą a samolotem. Cegła jest strukturą; ma długość, szerokość i wysokość. Nie może wiele zdziałać. Samolot może mieć wiele operacji, które go w jakiś sposób mutują, takie jak poruszanie powierzchniami sterującymi i zmiana ciągu silnika. Byłoby lepiej jako klasa.

Michael K.
źródło
2
„Użyj struktury dla czystych konstrukcji danych i klasy dla obiektów z operacjami” w języku C # to nonsens. Zobacz moją odpowiedź poniżej.
bytedev
1
„Użyj struktury dla czystych konstrukcji danych i klasy dla obiektów z operacjami”. Dotyczy to języka C / C ++, a nie C #
taktak004