Znajomość różnić między klasą a strukturą w c# jest bardzo ważna ze względu na kwestie wydajnościowe. Sposób wykorzystania może czasami wydawać się bardzo podobny, jednak to co dzieje się w tle jest kompletnie różne. W przypadku C++, struktury i klasy były bardzo podobne i reprezentowane były w pamięci w taki sam sposób. Różnica polegała na tym, że pola klas domyślnie były prywatne, a struktur publiczne.
Przede wszystkim struktura jest wartością (value type) a klasa typem referencyjnym. Value Type jest alokowany na stosie, z kolei typ referencyjny na stercie (heap). Każda klasa dziedziczy po System.Object . Struktura dziedziczy po System.ValueType, który z kolei również pochodzi od System.Object.
Należy pamiętać, że parametry które są Valuetype podczas przekazywania do metod są w całości kopiowane. W przypadku klas, kopiowany jest wyłącznie adres komórki w którym wartości danej klasy znajdują się. Podobnie jest z wszelkimi operacjami przypisania:
int a,b; a=5; b=a; // a i b to dwie różne komórki w pamięci //(bez optymalizacji kompilatora) SampleClass a = new SampleClass(); SampleClass b; b=a; // b wskazuje na a i obie // referencje wskazują na ten sam fragment pamięci
Szczególnie niebezpiecznie jest połączenie właściwości (getter) z strukturami – za każdym razem tworzona będzie nowa struktura.
Każdy obiekt klasy zajmuje więcej pamięci ponieważ alokowany jest na stercie i oprócz właściwych danych, miejsce przeznaczane jest również na nagłówek. Zatem jeśli typ składa się z dwóch Int32 dla struktury będzie zajmować 64 bity a dla klasy 64 + header.
Oprócz względów pamięciowych, głównym problemem klas jest zwalnianie pamięci. Garbage Collector nie jest najszybszym rozwiązaniem zatem faza “marking” dla GC zajmuje trochę czasu, zwłaszcza dla dużych obiektów, z wysoką liczbą zagnieżdżonych referencji.
Podsumowujmy więc zasadnicze różńcie:
1. Z jakiego typu dziedziczy?
Klasa: System.Object
Struktura: System.ValueType, który z kolei pochodzi od System.Object.
2. Jak są wykonywane przypisania?
Klasa jest typem referencyjnym (tylko adres jest kopiowany). W przypadku struktury, wszystkie pola są kopiowane.
3. Czy wspiera typy zagnieżdżone?
Zarówno klasy jak i struktury wspierają typy zagnieżdżone:
struct SampleStructA { public int a, b, c; public SampleStructB structB; public struct SampleStructB { public int a, b, c; } }
4. Czy dozwolone są pola oraz ich modyfikatory
Jak widać na powyższym przykładzie struktury również mogą zawierać pola. Ze względu na sposób inicjalizacji struktur (o tym w następnym poście), nie można definiować wartości domyślnych:
struct SampleStructA { public int a = 9; // ŹLE }
5. Czy dozwolone są stałe?
Zwykłe pola nie mogą zawierać wartości domyślnych, jednak stałe są dozwolone:
struct SampleStructA { public const int a = 9; }
W przypadku klas, pola jak i stałe mogą być deklarowane bez żadnych ograniczeń.
6. Czy dozwolone są właściwości, zdarzenia, indexer’y oraz metody?
Tak – deklaracja niczym się nie różni się od klas. Jednak należy być świadomym kilku pułapek. Struktury są zawsze kopiowane, zatem poniższa konstrukcja jest niebezpieczna:
internal struct SampleStructB { private SampleStructA _structA; public SampleStructA SampleStructA { get { return _structA; } } }
Za każdym razem gdy chcemy uzyskać dostęp przez SampleStructA, _structA jest kopiowany. Ze zdarzeniami również należy być ostrożnym. W praktyce jednak nie znam przypadku w którym musiałem użyć zdarzeń w strukturze.
7. Czy dozwolone są pola statyczne?
Tak
8. Wspiera dziedziczenie?
Wszystkie struktury są sealed(nie można dziedziczyć). Można jednak implementować interfejs.
9. Wsparcie dla typów generycznych, przeładowania operatorów oraz słowa kluczowego partial
Zarówno klasy jak i struktury wspierają powyższą funkcjonalność.
11. Czy można definiować konstruktor?
Konstruktory są dozwolone dla struktur, jednak nie ma możliwości stworzenia domyślnego konstruktora(bez parametrów).
12. Czy można przypisać NULL?
NULL to wskaźnik na pusty adres. Struktury są wartościami, więc nie ma takiej możliwości. Można jednak skorzystać z Nullable i cieszyć się podobnym zachowaniem:
SampleStructB? sampleStruct=null; // LUB Nullable<SampleStructB> sampleStruct=null;
10. Co z operatorem new?
W przypadku klas sprawa jest jasna – należy używać new aby zainicjalizować obiekt. Operator new jest jednak również dozwolony dla struktur:
SampleStructB sampleStruct=new SampleStructB(); sampleStruct.a = 4; MessageBox.Show(sampleStruct.a.ToString()); MessageBox.Show(sampleStruct.b.ToString());
Różnicą jednak jest fakt, że new nie jest jedynym operatorem inicjalizacji. Można również obyć się bez niego:
SampleStructB sampleStruct; sampleStruct.a = 4; MessageBox.Show(sampleStruct.a.ToString());
Nie spowoduje to wyjątku NullReference. Należy jednak pamiętać, aby ręcznie zainicjalizować WSZYSTKIE publiczne pola, z których chcemy skorzystać. Poniższy kod nie skompiluje się ponieważ pole b nie zostało ręcznie zainicjalizowane:
SampleStructB sampleStruct; sampleStruct.a = 4; MessageBox.Show(sampleStruct.a.ToString()); MessageBox.Show(sampleStruct.b.ToString());
Pozostało jeszcze kilka kwestii do omówienia: boxing oraz wyjaśnienie dlaczego nie można definiować domyślnych konstruktorów – o tym w następnych postach.
Ze względów wydajnościowych warto zatem rozważyć zapomniane struktury, szczególnie dla małych, niezmiennych fragmentów danych (Point, Vector itp.).
Nie rozumiem tylko dlaczego np.: DateTime’a nie można tak zadeklarować jak i innych struktur wbudowanych w .NET.
Chodzi mi o coś takiego:
Int32 x;
return x.ToString();
Dziwne, ale na taką konstrukcję nie pozwala konstruktor.
Poprawka: Dziwne, ale na taką konstrukcję nie pozwala kompilator.