Kiedy powinienem używać struct zamiast klasy?

303

MSDN mówi, że powinieneś używać struktur, gdy potrzebujesz lekkich obiektów. Czy istnieją inne scenariusze, w których struktura jest lepsza niż klasa?

Niektórzy ludzie mogli zapomnieć, że:

  1. Struktury mogą mieć metody.
  2. Struktury nie mogą być dziedziczone.

Rozumiem techniczne różnice między strukturami i klasami, po prostu nie mam pojęcia, kiedy użyć struktury.

Esteban Araya
źródło
Przypomnienie - większość ludzi zapomina w tym kontekście, że w C # struktury mogą również mieć metody.
petr k.

Odpowiedzi:

296

MSDN ma odpowiedź: Wybieranie między klasami i strukturami .

Zasadniczo na tej stronie znajduje się 4-elementowa lista kontrolna i mówi, aby użyć klasy, chyba że Twój typ spełnia wszystkie kryteria.

Nie definiuj struktury, chyba że typ ma wszystkie następujące cechy:

  • Logicznie reprezentuje pojedynczą wartość, podobną do typów pierwotnych (liczba całkowita, podwójna itd.).
  • Ma rozmiar instancji mniejszy niż 16 bajtów.
  • Jest niezmienny.
  • Nie będzie trzeba go często zapakować.
OwenP
źródło
1
Może brakuje mi czegoś oczywistego, ale nie do końca rozumiem „niezmienną” część. Dlaczego to jest konieczne? Czy ktoś mógłby to wyjaśnić?
Tamas Czinege
3
Prawdopodobnie zalecili to, ponieważ jeśli struktura jest niezmienna, to nie będzie miało znaczenia, że ​​ma ona semantykę wartości, a nie semantykę odniesienia. To rozróżnienie ma znaczenie tylko wtedy, gdy mutujesz obiekt / strukturę po wykonaniu kopii.
Stephen C. Steel
@DrJokepu: W niektórych sytuacjach system utworzy tymczasową kopię struktury, a następnie zezwoli na przekazanie tej kopii przez odwołanie do kodu, który ją zmienia; ponieważ tymczasowa kopia zostanie odrzucona, zmiana zostanie utracona. Ten problem jest szczególnie poważny, jeśli struct ma metody, które go mutują. Zdecydowanie nie zgadzam się z poglądem, że zmienność jest powodem, aby uczynić coś klasą, ponieważ - pomimo pewnych braków w c # i vb.net, zmienne struktury zapewniają użyteczną semantykę, której nie można osiągnąć w żaden inny sposób; nie ma semantycznego powodu, aby preferować niezmienną strukturę od klasy.
supercat
4
@Chuu: Projektując kompilator JIT, Microsoft postanowił zoptymalizować kod do kopiowania struktur o wielkości 16 bajtów lub mniejszej; oznacza to, że kopiowanie 17-bajtowej struktury może być znacznie wolniejsze niż kopiowanie 16-bajtowej struktury. Nie widzę żadnego szczególnego powodu, aby oczekiwać, że Microsoft rozszerzy takie optymalizacje na większe struktury, ale ważne jest, aby pamiętać, że choć struktury 17-bajtowe mogą być wolniejsze niż struktury 16-bajtowe, w wielu przypadkach duże struktury mogą być bardziej wydajne niż duże obiekty klasy i gdzie względna przewaga struktur rośnie wraz z rozmiarem struktury.
supercat
3
@Chuu: Zastosowanie takich samych wzorców użytkowania do dużych struktur, jak w przypadku klas, może skutkować nieefektywnym kodem, ale właściwe rozwiązanie często nie zastępuje struktur klasami, ale zamiast tego używa ich bardziej wydajnie; przede wszystkim należy unikać przekazywania lub zwracania struktur według wartości. Przekaż je jako refparametry, ilekroć jest to uzasadnione. Przekaż struct z 4000 pól jako parametr ref do metody, która zmienia jedno będzie tańsze niż przekazanie struct z 4 polami pod względem wartości do metody, która zwraca zmodyfikowaną wersję.
supercat
54

Dziwi mnie, że nie przeczytałem żadnej z poprzednich odpowiedzi na to pytanie, które uważam za najważniejszy aspekt:

Używam struktur, gdy chcę typ bez tożsamości. Na przykład punkt 3D:

public struct ThreeDimensionalPoint
{
    public readonly int X, Y, Z;
    public ThreeDimensionalPoint(int x, int y, int z)
    {
        this.X = x;
        this.Y = y;
        this.Z = z;
    }

    public override string ToString()
    {
        return "(X=" + this.X + ", Y=" + this.Y + ", Z=" + this.Z + ")";
    }

    public override int GetHashCode()
    {
        return (this.X + 2) ^ (this.Y + 2) ^ (this.Z + 2);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is ThreeDimensionalPoint))
            return false;
        ThreeDimensionalPoint other = (ThreeDimensionalPoint)obj;
        return this == other;
    }

    public static bool operator ==(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return p1.X == p2.X && p1.Y == p2.Y && p1.Z == p2.Z;
    }

    public static bool operator !=(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return !(p1 == p2);
    }
}

Jeśli masz dwa wystąpienia tej struktury, nie przejmujesz się, czy są to pojedyncze dane w pamięci, czy dwa. Troszczysz się tylko o wartości, które posiadają.

Andrei Rînea
źródło
4
Odetnij temat, ale dlaczego rzucasz ArgumentException, gdy obj nie jest ThreeDimensionalPoint? Czy w takim przypadku nie powinieneś po prostu zwrócić fałsz?
Svish
4
Zgadza się, byłem zbyt chętny. return falsejest to, co powinno być, poprawianie teraz.
Andrei Rînea
Ciekawy powód, aby użyć struktury. Stworzyłem klasy z GetHashCode i Equals zdefiniowane podobnie do tego, co tutaj pokazujesz, ale wtedy zawsze musiałem uważać, aby nie mutować tych instancji, jeśli użyłem ich jako kluczy do słownika. Prawdopodobnie byłoby bezpieczniej, gdybym zdefiniował je jako struktury. (Ponieważ wtedy klucz byłby kopią pól w chwili, gdy struct stał się kluczem słownika , więc klucz pozostałby niezmieniony, gdybym później zmienił oryginał.)
ToolmakerSteve
W twoim przykładzie jest ok, ponieważ masz tylko 12 bajtów, ale pamiętaj, że jeśli masz wiele pól w tej strukturze, które przekraczają 16 bajtów, musisz rozważyć użycie klasy i przesłonięcie metod GetHashCode i Equals.
Daniel Botero Correa
Typ wartości w DDD nie oznacza, że ​​należy koniecznie użyć typu wartości w C #
Darragh
27

Bill Wagner ma rozdział na ten temat w swojej książce „skuteczny c #” ( http://www.amazon.com/Effective-Specific-Ways-Improve-Your/dp/0321245660 ). Kończy, stosując następującą zasadę:

  1. Czy główna odpowiedzialność za przechowywanie danych typu?
  2. Czy jego interfejs publiczny jest zdefiniowany w całości przez właściwości, które uzyskują dostęp lub modyfikują jego członków danych?
  3. Czy na pewno Twój typ nigdy nie będzie miał podklas?
  4. Czy jesteś pewien, że Twój typ nigdy nie będzie traktowany polimorficznie?

Jeśli odpowiesz „tak” na wszystkie 4 pytania: użyj struktury. W przeciwnym razie użyj klasy.

Bart Gijssens
źródło
1
więc ... obiekty do przesyłania danych (DTO) powinny być strukturami?
cruizer
1
Powiedziałbym tak, jeśli spełnia 4 opisane powyżej kryteria. Dlaczego obiekt przesyłania danych powinien być traktowany w określony sposób?
Bart Gijssens,
2
@cruizer zależy od twojej sytuacji. W ramach jednego projektu mieliśmy wspólne pola kontroli w naszych DTO i dlatego napisaliśmy podstawowe DTO, z którego inni odziedziczyli.
Andrew Grothe
1
Wszystkie oprócz (2) wydają się być doskonałymi zasadami. Musiałby zobaczyć jego rozumowanie, aby wiedzieć, co dokładnie rozumie przez (2) i dlaczego.
ToolmakerSteve,
1
@ToolmakerSteve: Musisz za to przeczytać książkę. Nie sądzę, że kopiowanie / wklejanie dużych części książki jest sprawiedliwe.
Bart Gijssens,
11

Używałbym struktur, gdy:

  1. obiekt ma być tylko do odczytu (za każdym razem, gdy przekazujesz / przypisujesz strukturę, jest on kopiowany). Obiekty tylko do odczytu są świetne, jeśli chodzi o przetwarzanie wielowątkowe, ponieważ w większości przypadków nie wymagają blokowania.

  2. przedmiot jest mały i krótkotrwały. W takim przypadku istnieje duża szansa, że ​​obiekt zostanie przydzielony na stosie, co jest znacznie bardziej wydajne niż umieszczenie go na zarządzanej stercie. Co więcej, pamięć przydzielona przez obiekt zostanie zwolniona, gdy tylko wyjdzie poza jego zakres. Innymi słowy, jest to mniej pracy dla Garbage Collectora, a pamięć jest wykorzystywana bardziej wydajnie.

Paweł Pabich
źródło
10

Użyj klasy, jeśli:

  • Jego tożsamość jest ważna. Struktury są kopiowane niejawnie podczas przekazywania wartości do metody.
  • Będzie miał duży wpływ na pamięć.
  • Jego pola wymagają inicjalizacji.
  • Musisz dziedziczyć po klasie podstawowej.
  • Potrzebujesz zachowania polimorficznego;

Użyj struktury, jeśli:

  • Będzie działał jak typ pierwotny (int, long, bajt itp.).
  • Musi mieć niewielką pamięć.
  • Wywołujesz metodę P / Invoke, która wymaga przekazania struktury przez wartość.
  • Musisz zmniejszyć wpływ wyrzucania elementów bezużytecznych na wydajność aplikacji.
  • Jego pola muszą być inicjowane tylko do wartości domyślnych. Wartość ta wynosiłaby zero dla typów numerycznych, false dla typów boolowskich, a null dla typów referencyjnych.
    • Zauważ, że w C # 6.0 struktury mogą mieć domyślny konstruktor, którego można użyć do zainicjowania pól struktury na wartości domyślne.
  • Nie musisz dziedziczyć po klasie podstawowej (innej niż ValueType, z której dziedziczą wszystkie struktury).
  • Nie potrzebujesz zachowania polimorficznego.
Yashwanth Chowdary Kata
źródło
5

Zawsze używałem struktury, gdy chciałem zgrupować kilka wartości, aby przekazać rzeczy z powrotem z wywołania metody, ale nie będę musiał używać go do niczego po przeczytaniu tych wartości. Tylko jako sposób na utrzymanie porządku w czystości. Zwykle postrzegam rzeczy w strukturze jako „wyrzucane”, a rzeczy w klasie jako bardziej przydatne i „funkcjonalne”

Ryan Skarin
źródło
4

Jeśli jednostka będzie niezmienna, pytanie, czy użyć struktury czy klasy, będzie na ogół dotyczyło wydajności, a nie semantyki. W systemie 32/64-bitowym odwołania do klas wymagają przechowywania 4/8 bajtów, niezależnie od ilości informacji w klasie; skopiowanie odwołania do klasy będzie wymagało skopiowania 4/8 bajtów. Z drugiej strony każdy wyraźnyinstancja klasy będzie miała 8/16 bajtów narzutu oprócz informacji, które posiada i kosztu pamięci odniesień do niej. Załóżmy, że ktoś chce tablicy 500 jednostek, z których każda zawiera cztery 32-bitowe liczby całkowite. Jeśli jednostka jest typem struktury, tablica będzie wymagać 8 000 bajtów niezależnie od tego, czy wszystkie 500 jednostek jest identycznych, różnych lub pomiędzy nimi. Jeśli jednostka jest typem klasy, tablica 500 referencji zajmie 4000 bajtów. Gdyby wszystkie te odniesienia wskazywały na różne obiekty, obiekty wymagałyby dodatkowych 24 bajtów każdy (12 000 bajtów na wszystkie 500), łącznie 16 000 bajtów - dwa razy więcej niż koszt przechowywania typu strukturalnego. Z drugiej strony kod utworzył jedną instancję obiektu, a następnie skopiował odwołanie do wszystkich 500 gniazd tablic, całkowity koszt wyniósłby 24 bajty dla tej instancji i 4, 000 dla tablicy - łącznie 4024 bajtów. Duże oszczędności. Działałoby niewiele sytuacji, tak jak i ostatnia, ale w niektórych przypadkach może być możliwe skopiowanie niektórych odniesień do wystarczającej liczby gniazd tablic, aby takie współdzielenie było opłacalne.

Jeśli jednostka ma być zmienna, pytanie, czy użyć klasy czy struktury, jest pod pewnymi względami łatwiejsze. Załóżmy, że „Rzecz” jest strukturą lub klasą, która ma pole liczby całkowitej o nazwie x i wykonuje następujący kod:

  Rzecz t1, t2;
  ...
  t2 = t1;
  t2.x = 5;

Czy ktoś chce, aby ta ostatnia instrukcja wpływała na t1.x?

Jeśli Thing jest typem klasy, t1 i t2 będą równoważne, co oznacza, że ​​t1.x i t2.x również będą równoważne. Zatem druga instrukcja wpłynie na t1.x. Jeśli Thing jest typem struktury, t1 i t2 będą różnymi instancjami, co oznacza, że ​​t1.x i t2.x będą odnosić się do różnych liczb całkowitych. Zatem drugie zdanie nie wpłynie na t1.x.

Zmienne struktury i zmienne klasy mają zasadniczo różne zachowania, chociaż .net ma pewne dziwactwa w obsłudze mutacji struktur. Jeśli ktoś chce zachowania typu wartości (co oznacza, że ​​„t2 = t1” skopiuje dane z t1 do t2, pozostawiając t1 i t2 jako odrębne instancje) i jeśli można żyć z dziwactwami w obsłudze typów wartości w .net, użyj struktura. Jeśli ktoś chce semantyki typu wartości, ale dziwactwa .net spowodowałyby popsutą semantykę typu wartości w swojej aplikacji, należy użyć klasy i mamrotać.

supercat
źródło
3

Ponadto doskonałe odpowiedzi powyżej:

Struktury są typami wartości.

Nigdy nie można ich ustawić na Nic .

Ustawienie struktury = Nic, spowoduje ustawienie wszystkich typów wartości na wartości domyślne.

George Filippakos
źródło
2

kiedy tak naprawdę nie potrzebujesz zachowania, ale potrzebujesz więcej struktury niż prosta tablica lub słownik.

Kontynuacja Tak myślę o strukturach w ogóle. Wiem, że mogą mieć metody, ale lubię zachować to ogólne rozróżnienie mentalne.

Jim Deville
źródło
Dlaczego to mówisz? Struktury mogą mieć metody.
Esteban Araya
2

Jak powiedział @ Simon, struktury zapewniają semantykę typu „wartość”, więc jeśli potrzebujesz zachowania podobnego do wbudowanego typu danych, użyj struktury. Ponieważ struktury są przekazywane przez kopię, musisz upewnić się, że są małe, około 16 bajtów.

Scott Dorman
źródło
2

Jest to stary temat, ale chciałem zapewnić prosty test porównawczy.

Utworzyłem dwa pliki .cs:

public class TestClass
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

i

public struct TestStruct
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Uruchom test:

  • Utwórz 1 TestClass
  • Utwórz 1 TestStruct
  • Utwórz 100 TestClass
  • Utwórz 100 TestStruct
  • Utwórz 10000 TestClass
  • Utwórz 10000 TestStruct

Wyniki:

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
[Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT  [AttachedDebugger]
DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT


|         Method |           Mean |         Error |        StdDev |     Ratio | RatioSD | Rank |    Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |---------------:|--------------:|--------------:|----------:|--------:|-----:|---------:|------:|------:|----------:|

|      UseStruct |      0.0000 ns |     0.0000 ns |     0.0000 ns |     0.000 |    0.00 |    1 |        - |     - |     - |         - |
|       UseClass |      8.1425 ns |     0.1873 ns |     0.1839 ns |     1.000 |    0.00 |    2 |   0.0127 |     - |     - |      40 B |
|   Use100Struct |     36.9359 ns |     0.4026 ns |     0.3569 ns |     4.548 |    0.12 |    3 |        - |     - |     - |         - |
|    Use100Class |    759.3495 ns |    14.8029 ns |    17.0471 ns |    93.144 |    3.24 |    4 |   1.2751 |     - |     - |    4000 B |
| Use10000Struct |  3,002.1976 ns |    25.4853 ns |    22.5920 ns |   369.664 |    8.91 |    5 |        - |     - |     - |         - |
|  Use10000Class | 76,529.2751 ns | 1,570.9425 ns | 2,667.5795 ns | 9,440.182 |  346.76 |    6 | 127.4414 |     - |     - |  400000 B |
Eduardas Šlutas
źródło
1

Hmm ...

Nie użyłbym śmiecia jako argumentu za / przeciw użyciu struktur vs klas. Zarządzana sterta działa podobnie do stosu - utworzenie obiektu umieszcza ją na szczycie stosu, co jest prawie tak szybkie, jak przydzielanie na stosie. Ponadto, jeśli obiekt jest krótkotrwały i nie przetrwa cyklu GC, dezalokacja jest bezpłatna, ponieważ GC działa tylko z pamięcią, która jest nadal dostępna. (Przeszukaj MSDN, jest seria artykułów na temat zarządzania pamięcią .NET, jestem po prostu zbyt leniwa, by poszukać ich).

Przez większość czasu używam struktury, skończę kopać się za to, ponieważ później odkrywam, że posiadanie referencyjnej semantyki uprościłoby sprawę.

W każdym razie te cztery punkty w opublikowanym powyżej artykule MSDN wydają się dobrą wskazówką.


źródło
1
Jeśli czasem potrzebujesz semantyki referencyjnej ze strukturą, po prostu zadeklaruj class MutableHolder<T> { public T Value; MutableHolder(T value) {Value = value;} }, a wtedy a MutableHolder<T>będzie obiektem z modyfikowalną semantyką klas (działa to równie dobrze, jeśli Tjest strukturą lub niezmiennym typem klasy).
supercat
1

Struktury znajdują się na stosie, a nie na stosie, dlatego są bezpieczne dla wątków i powinny być używane podczas implementacji wzorca obiektu przenoszenia, nigdy nie chcesz używać obiektów na stosie, są one niestabilne, w tym przypadku chcesz użyć stosu wywołań, jest to podstawowy przypadek użycia struktury Jestem zaskoczony wszystkimi odpowiedziami tutaj,

Jacek
źródło
-3

Myślę, że najlepszą odpowiedzią jest po prostu użycie struct, gdy potrzebujesz kolekcji właściwości, klasa, gdy jest to zbiór właściwości ORAZ zachowań.

Lucian Gabriel Popescu
źródło
Struktury również mogą mieć metody
Nithin Chandran
oczywiście, ale jeśli potrzebujesz metod, szanse wynoszą 99%, niewłaściwie używasz struct zamiast klasy. Jedynymi wyjątkami, które znalazłem, gdy można mieć metody w struct, są wywołania zwrotne
Lucian Gabriel Popescu,