Struktura (struct) to typ wartościowy, który definiujesz sam. Wygląda jak klasa, ma pola, właściwości, metody i konstruktory, ale zmienna typu strukturalnego zawiera dane bezpośrednio, a nie referencję do obiektu. Ta jedna różnica zmienia działanie przypisania, wywołań metod, równości i null.
Semantyka wartości: przypisanie kopiuje
Wbudowane typy liczbowe, bool, char, DateTime, TimeSpan i Guid to wszystko struktury. Twoje własne działają tak samo:
Wynik:
struct: s1.X = 1, s2.X = 50
class: c1.X = 50, c2.X = 50
after Move: s1.X = 1, c1.X = 150
W przypadku struktury s2 jest niezależną kopią, a Move też pracuje na własnej kopii. Żeby metoda mogła zmienić strukturę wywołującego, przekaż ją przez referencję z ref (zobacz ref i out). Żeby uniknąć kopiowania dużej struktury bez pozwalania na zmiany, C# 7.2 dodał parametry in.
Inne konsekwencje bycia typem wartościowym:
- Zmienna strukturalna nigdy nie może być
null.PointS p = null;się nie kompiluje. UżyjPointS?(typ wartościowy dopuszczający null), gdy brak wartości ma znaczenie. - Niezainicjalizowana struktura to same zera: pola liczbowe
0, polaboolfalse, pola referencyjnenull. - Struktura nie może dziedziczyć po innej strukturze ani klasie i nic nie może dziedziczyć po strukturze. Może implementować interfejsy.
- Lokalne zmienne strukturalne zwykle żyją na stosie albo wewnątrz obiektu, który je zawiera, więc utworzenie takiej zmiennej nie alokuje pamięci na stercie.
Pułapka listy struktur: CS1612
Ponieważ odczyt struktury tworzy kopię, ta niewinnie wyglądająca linia się nie kompiluje:
var points = new List<PointS> { new PointS(1, 2) };
points[0].X = 10;
// error CS1612: Cannot modify the return value of 'List<PointS>.this[int]' because it is not a variable
Indeksator List<T> to metoda zwracająca kopię elementu. Ustawienie X na tej tymczasowej kopii niczego by nie zmieniło, więc kompilator cię zatrzymuje. Ten sam błąd pojawia się, gdy właściwość zwraca strukturę: order.Location.X = 10. Skopiuj, zmień, zapisz z powrotem:
Wynik:
35
7
Wyjątkiem są tablice: array[0] to sam element, a nie kopia. Ten powracający problem jest powodem standardowej rady, żeby struktury były niemutowalne: jeśli struktury nie da się zmienić, zmiana kopii niczego nie gubi.
Konstruktory i wartości domyślne
W C# od 7 do 9 zasady dla konstruktorów struktur są ścisłe:
- Nie możesz zadeklarować konstruktora bezparametrowego.
new PointS()istnieje zawsze i zeruje każde pole. - Zadeklarowany konstruktor musi przypisać każde pole (i automatyczną właściwość), zanim zakończy działanie.
- Inicjalizatory pól (
public int X = 1;) nie są dozwolone dla pól instancji.
Wynik:
19.90 EUR
0.00 (none)
0.00 (none)
0.00 (none)
Money pokazuje też, dlaczego wartość domyślna ma znaczenie: wyzerowane Money ma walutę null i twój kod musi to obsłużyć, bo tablice, default(T) i niezainicjalizowane pola tworzą taką wartość.
Nowsze wersje złagodziły te zasady:
// C# 10: explicit parameterless constructors and field initializers
public struct Settings
{
public int Retries = 3;
public Settings() { }
}
// C# 11: fields you do not assign in a constructor are zeroed automatically,
// instead of being a compile error.
Konstruktor bezparametrowy z C# 10 uruchamia się dla new Settings(), ale nie dla default(Settings) ani elementów tablicy, które nadal są samymi zerami. Ten podział zaskakuje, więc używaj go ostrożnie.
readonly struct (C# 7.2)
Oznaczenie samej struktury jako readonly sprawia, że kompilator wymusza niemutowalność: każde pole musi być readonly, a każda automatyczna właściwość tylko z get.
public readonly struct Temperature
{
public double Celsius { get; }
public Temperature(double celsius) { Celsius = celsius; }
public Temperature WarmerBy(double delta) => new Temperature(Celsius + delta); // returns a new value
}
Poza dokumentowaniem intencji pomaga to wydajności: gdy struktura bez readonly jest przechowywana w polu readonly albo przekazywana jako parametr in, kompilator kopiuje ją przed każdym wywołaniem metody (nie może wiedzieć, że metoda jej nie zmieni). readonly struct nie potrzebuje takich kopii obronnych. W C# 7.0 nadal możesz zrobić każde pole readonly, tak jak Money robi to z właściwościami tylko z get: daje to niemutowalność, ale nie oszczędza kopii, bo kompilator ufa tylko strukturze zadeklarowanej jako readonly.
Równość
Equals na strukturze domyślnie porównuje pole po polu, czyli daje oczekiwane zachowanie wartości. Domyślna implementacja (ValueType.Equals) może jednak używać refleksji i jest wolna, a operator == nie jest w ogóle zdefiniowany: a == b na własnej strukturze to błąd CS0019. Zaimplementuj oba, gdy struktura będzie porównywana:
Wynik:
True
True
True
Implementacja IEquatable<T> ma znaczenie dla kolekcji: HashSet<T>, Dictionary<TKey, TValue> i List<T>.Contains wywołują Equals(GridCell) bezpośrednio, zamiast opakowywać każdą wartość, żeby wywołać Equals(object). Struktury rekordowe (poniżej) generują to wszystko za ciebie.
Boxing
Konwersja struktury na object albo typ interfejsu opakowuje ją (boxing): środowisko uruchomieniowe kopiuje wartość do nowego obiektu na stercie. Opakowanie i oryginał są potem niezależne:
Wynik:
2
0
Boxing za każdym razem kosztuje alokację, dlatego stare niegeneryczne kolekcje, takie jak ArrayList, były wolne z typami wartościowymi i dlatego zastąpiły je typy generyczne. Oznacza to też, że mutowalna struktura używana przez interfejs zmienia się w swoim opakowaniu, a nie w oryginale, co jest kolejnym powodem, żeby struktury były niemutowalne.
Struct a class
| struct | class | |
|---|---|---|
| Rodzaj | typ wartościowy | typ referencyjny |
| Przypisanie i parametry | kopiują dane | kopiują referencję |
Może być null | nie (T? może) | tak |
| Wartość domyślna | wszystkie pola wyzerowane | null |
| Dziedziczenie | brak; może implementować interfejsy | jedna klasa bazowa, interfejsy |
== | niezdefiniowany, jeśli go nie przeciążysz | równość referencji, jeśli nie przeciążony |
Domyślne Equals | porównuje pola | porównuje referencje |
| Alokacja | w miejscu (stos lub obiekt zawierający) | sterta, odśmiecanie |
| Dobre do | małych niemutowalnych wartości | encji, dużego lub współdzielonego stanu |
Kiedy używać struktury
Wybierz strukturę, gdy spełnione są wszystkie warunki: typ to jedna logiczna wartość (współrzędna, kwota pieniędzy, kolor, zakres dat), jest mały (wytyczna Microsoftu to około 16 bajtów, mniej więcej cztery int), jest niemutowalny i nie potrzebujesz dziedziczenia. Struktury się opłacają, gdy tworzysz ich bardzo wiele, na przykład miliony punktów w tablicy, bo unikają alokacji na stercie i wpisu dla odśmiecacza na każdy element.
Wybierz klasę dla wszystkiego, co ma tożsamość (klient, zamówienie), wszystkiego, co duże, wszystkiego, co jest zmieniane z kilku miejsc, i zawsze, gdy nie masz pewności. Duża mutowalna struktura daje ci koszty kopiowania plus zamieszanie ze zmienianiem kopii.
record struct (C# 10)
C# 10 dodał record struct, który w jednej linii generuje dla struktury równość wartości, ==, ToString i obsługę with:
public readonly record struct Coordinate(double Lat, double Lng);
var home = new Coordinate(38.72, -9.14);
var same = new Coordinate(38.72, -9.14);
Console.WriteLine(home == same); // True
Console.WriteLine(home); // Coordinate { Lat = 38.72, Lng = -9.14 }
var north = home with { Lat = 38.80 };
Zwykły record struct ma mutowalne właściwości pozycyjne; readonly record struct robi je tylko do inicjalizacji (init-only), co zwykle jest tym, czego chcesz. Strona o rekordach omawia wersję klasową i pokazuje składniki, które pisze kompilator.
Częste błędy
- Zmienianie struktury przez kopię.
list[0].X = 1(CS1612), zmiennaforeach(CS1654), wynik gettera właściwości. Zapisz zmienioną kopię z powrotem albo zrób strukturę niemutowalną. - Duże struktury. Każde przypisanie i wywołanie kopiuje całość. Powyżej kilku pól klasa jest zwykle szybsza.
- Zapominanie o wyzerowanej wartości domyślnej. Tablice i
default(T)tworzą wartości strukturalne bez uruchamiania twojego konstruktora, więc pola, które tam walidujesz, nadal mogą być zerem albonull. - Porównywanie przez
==bez jego zdefiniowania. CS0019. ZaimplementujIEquatable<T>i operatory albo użyj record struct. - Mutowalne struktury za interfejsami. Boxing oznacza, że zmiana trafia do kopii, na którą nie patrzysz.
Najczęściej zadawane pytania
Czym jest struktura (struct) w C#?
Struktura to typ wartościowy definiowany przez użytkownika: struct Point { public int X; public int Y; }. Zmienna typu strukturalnego przechowuje same dane, a nie referencję do obiektu, więc przypisanie i przekazanie do metody kopiują całą wartość. int, double, DateTime i Guid to wszystko struktury.
Jaka jest różnica między struct a class w C#?
Klasa to typ referencyjny: zmienne współdzielą jeden obiekt i dozwolone jest null. Struktura to typ wartościowy: każda zmienna ma własną kopię, nie może być null (chyba że użyjesz Point?), nie może dziedziczyć ani być dziedziczona, a jej wartość domyślna to wszystkie pola wyzerowane. Struktury pasują do małych, niemutowalnych wartości, a klasy do encji z tożsamością i zachowaniem.
Kiedy używać struktury zamiast klasy w C#?
Gdy typ reprezentuje jedną małą wartość (współrzędną, kwotę pieniędzy, zakres dat), najlepiej jest niemutowalny, jest porównywany po zawartości i tworzy się go w dużych ilościach, gdzie unikanie alokacji na stercie ma znaczenie. Wytyczne Microsoftu dodają rozmiar około 16 bajtów lub mniej. Jeśli typ ma tożsamość, wiele pól albo jest modyfikowany przez referencje, użyj klasy.
Skąd błąd "Cannot modify the return value because it is not a variable"?
To błąd CS1612, zwykle z list[0].X = 5 na List<Point> ze strukturami. Indeksator zwraca kopię struktury, więc zmiana kopii by przepadła, dlatego kompilator odmawia. Skopiuj element do zmiennej, zmień go i przypisz z powrotem: var p = list[0]; p.X = 5; list[0] = p;. Tablice nie mają tego problemu, bo array[0] odnosi się do samego elementu.
Czym jest readonly struct w C#?
readonly struct (C# 7.2) deklaruje, że żaden składnik struktury nie zmienia jej stanu: wszystkie pola muszą być readonly, a automatyczne właściwości tylko z get. Kompilator to wymusza i dzięki temu może unikać kopii obronnych, gdy struktura jest przekazywana przez in albo przechowywana w polu readonly. Większość struktur powinna być readonly.