Klasy i struktury w C#

W C# nie ma słowa kluczowego typedef, które służy w C++ do tworzenia aliasów typów. Termin ''definiowanie typów'' ma wobec tego jednoznaczny sens. Oznacza wyłącznie definiowanie nowych klas lub struktur. Celem artykułu jest przybliżenie wybranych zagadnień związanych z definiowaniem typów w C#, a w szczególności wskazanie różnic między klasami i strukturami oraz pomoc w wybraniu jednego z tych ''metatypów'', gdy trzeba implementować nowy typ w C#.

W C# nie ma słowa kluczowego typedef, które służy w C++ do tworzenia aliasów typów. Termin 'definiowanie typów' ma wobec tego jednoznaczny sens. Oznacza wyłącznie definiowanie nowych klas lub struktur. Celem artykułu jest przybliżenie wybranych zagadnień związanych z definiowaniem typów w C#, a w szczególności wskazanie różnic między klasami i strukturami oraz pomoc w wybraniu jednego z tych 'metatypów', gdy trzeba implementować nowy typ w C#.

Język C# jest w pełni obiektowy. Nie ma w nim typów prostych, a zatem wszystkie zadeklarowane przez nas zmienne są obiektami. Nawet liczby mają metody, o czym łatwo się przekonać, kompilując kod zawierający choćby wyrażenie 1.ToString(). Ale czym są obiekty i jak odnoszą się do klas i struktur? Formalnie rzecz ujmując, relację klasy i obiektu można wyrazić następująco: klasa jest typem obiektu, a obiekt jest instancją klasy. Jeżeli na przykład zaprojektujemy klasę Drzewo z trzema polami, czyli zmiennymi zadeklarowanymi wewnątrz klasy, opisującymi gatunek, wysokość i wiek drzewa, to możemy utworzyć wiele różnych obiektów opisujących konkretne drzewa, a różniących się właśnie tymi trzema własnościami. Instancją klasy Drzewo, a więc obiektem, będzie stuletni dąb mierzący dziesięć metrów oraz dwumetrowy trzyletni świerk. Klasy mogą podlegać dziedziczeniu, czyli można wzbogacać ich zawartość i uszczegółowić przy tym ich przeznaczenie. Możemy zatem dodać kolejne pole będące tablicą, które przechowa położenie i rodzaj ozdób zawieszonych na drzewie. Tak utworzoną klasę potomną moglibyśmy nazwać Choinka. Zachowa ona, oczywiście, wszystkie własności klasy bazowej Drzewo i w tym sensie Choinka jest nadal Drzewem.

Wszystkie obiekty będące instancjami klas powstają dzięki użyciu operatora new i podobnie, jak dynamicznie tworzone obiekty w C++, tworzone są na stercie (heap). Jednak w odróżnieniu od C++, a podobnie jak w Javie, programista nie musi, co więcej - nie może kontrolować usuwania obiektów ze sterty. Obiekt usuwany jest automatycznie wtedy, gdy odśmiecacz (garbage collector) stwierdzi, że adresu obiektu nie przechowuje już żadna referencja. W ten sposób nie ma groźby wycieku pamięci, który był zmorą programistów C++.

Struktury

Rysunek 1. Najbardziej wiarygodnym źródłem wiedzy o klasach i strukturach jest dokumentacja MSDN, niestety, dostępna tylko w języku angielskim.

Rysunek 1. Najbardziej wiarygodnym źródłem wiedzy o klasach i strukturach jest dokumentacja MSDN, niestety, dostępna tylko w języku angielskim.

A jednak z faktu, że wszystko w C# jest obiektem nie wynika wcale, że wszystkie typy obiektów są klasami. Innymi słowy, nie wszystkie obiekty są w C# instancjami klas. Oprócz klas są jeszcze struktury. W C# struktura, podobnie jak klasa, może mieć nie tylko pola, ale także metody czy konstruktory. Ograniczeniem w stosunku do klas jest zakaz dziedziczenia. I to zarówno dziedziczenia ze zdefiniowanych struktur, jak i dziedziczenia przez nie. Mogą natomiast implementować interfejsy. Jednak najważniejsza różnica między klasami i strukturami pojawia się podczas próby utworzenia obiektu będącego instancją klasy lub struktury. Jak już wiemy, obiekt będący instancją klasy musi być tworzony za pomocą operatora new i powstaje na stercie. W wypadku struktur jest inaczej. Porównajmy następujące polecenia:

(1) Int32 i; //lub int i;

(2) Button b;

oraz

(3) Int32 i=new Int32(); //lub int i=new int();

(4) Button b=new Button();

Ponieważ System.Windows.Forms.Button jest klasą, wyrażenie (2) jest jedynie deklaracją zmiennej b, która jest referencją do obiektu typu Button, i to niezainicjowaną. Aby powstał obiekt-instancja klasy, musimy użyć operatora new, czyli umieścić w kodzie polecenie typu (4). Całkiem inaczej rzecz ma się ze strukturami. Obiekty-instancje struktur możemy tworzyć, zwyczajnie je deklarując. Pisząc polecenie (1), tworzymy obiekt na stosie (stack), który utożsamiany jest ze zmienną i. Jeżeli jednak nie wykorzystamy operatora new do wywołania konstruktora struktury, to pola tak utworzonego obiektu pozostaną nieprzypisane i obiekt nie może być użyty, tj. nie może być argumentem metody, poza metodami wymuszającymi przypisanie modyfikatorem out, ani argumentem operatorów, poza operatorem przypisania z lewej strony. Utworzenie takiego obiektu bez inicjowania pozwala na uniknięcie nadmiarowych instrukcji, jeżeli chcemy na przykład ustalić wartość liczby w instrukcji warunkowej lub zadeklarować indeks pętli o zakresie większym niż sama pętla. W wypadku struktur implementujących typy liczbowe inicjowanie możliwe jest bądź przez stałą liczbową, np. Int32 i=1; (pamiętajmy, że 1 jest też formalnie obiektem), lub za pomocą operatora new. W drugim przypadku możemy użyć, jak w poleceniu (3), konstruktora domyślnego inicjującego pola obiektu zerami, wartościami false lub null odpowiednio do ich typu lub użyć konstruktora pozwalającego na podanie wartości, np. Int32 i=new Int32(1);. Operatory działające na strukturach zostały zdefiniowane w taki sposób, że operują na stanie obiektu (wartości liczby), a nie jak w wypadku klas na adresach przechowywanych przez referencje. Aby jeszcze bardziej upodobnić operacje na strukturach do analogicznych poleceń na zmiennych prostych, znanych z C i C++, twórcy C# zdefiniowali aliasy niektórych struktur odpowiadających klasycznym typom zmiennych. Wobec tego obiekt typu Int32 możemy również utworzyć, pisząc int i;.

Obiekty struktury wykazują pewne podobieństwa do statycznie tworzonych obiektów w C++ - również tworzone są na stercie. I również mają żywotność ograniczoną do zakresu, w którym zostały zdefiniowane, czyli są usuwane automatycznie z pamięci, gdy wątek wychodzi poza obręb nawiasów {}, co bardzo ułatwia zarządzanie pamięcią. W ten sposób zmienna lokalna zadeklarowana w metodzie będzie tworzona podczas wywołania tej metody i natychmiast usuwana w momencie jej opuszczenia. W analogiczny sposób po opuszczeniu pętli przez wątek zmienna indeksująca zostanie natychmiast usunięta z pamięci. Ten prosty mechanizm kontroli pamięci nie ma zastosowania do obiektów będących instancjami klas. Jak pamiętamy, w ich przypadku za gospodarkę pamięcią odpowiedzialny jest odśmiecacz, który usuwa obiekt ze sterty wówczas, gdy w pamięci nie ma żadnej wskazującej na niego referencji. Można powiedzieć, że aby obiekt taki istniał poza metodą, w której został zdefiniowany, musi być referencja będąca np. polem klasy, czyli o zakresie większym niż metoda, która będzie przechowywała jego adres - esse est percipi. Należy przy tym pamiętać, że adres instancji klasy może być przekazywany z referencji do referencji, a instancji struktury - nie.

Wśród typów mających aliasy, a więc bool, char, byte, int, float, double, decimal i innych, tylko string i object odpowiadają klasom. Wszystkie pozostałe, zwłaszcza wszystkie typy implementujące liczby, to struktury.

Może być mylące dla osób, które wcześniej programowały w C++, że typ, a nie sposób utworzenia obiektu determinuje miejsce jego utworzenia. W C++ obiekt każdej klasy mógł być zdefiniowany statycznie (Klasa obiekt;) lub dynamicznie (Klasa* obiekt=new Klasa()). W C# programista nie ma takiej dowolności. Jego wpływ na wybór obszaru pamięci zajmowanego przez obiekt ogranicza się do wyboru między klasą a strukturą podczas implementacji typu.

Zmienne wartościowe i referencyjne

Z podziałem na struktury i klasy wiąże się jeszcze jedno rozróżnienie, a mianowicie podział na zmienne wartościowe oraz zmienne referencyjne. Na boku pozostawiamy trzeci typ zmiennych - wskaźniki, których użycie w C# graniczy ze złym gustem i wymaga specjalnego oznaczenia całej aplikacji jako niebezpiecznej. Skoro w C# nie ma tzw. typów prostych, to oba interesujące nas typy zmiennych muszą odnosić się do obiektów. Zmienne typów wartościowych utożsamiane są z instancjami struktur znajdującymi się na stosie, natomiast zmienne typów referencyjnych to referencje do instancji klas, które powstają na stercie. Utożsamienie zmiennej wartościowej z obiektem, który reprezentuje, jest całkowite. Widać to najlepiej podczas kopiowania ich wartości, co dokładniej omówimy niżej. Natomiast w wypadku zmiennych referencyjnych łatwo odseparować sam obiekt i wskazującą na niego referencję. Kopiowanie referencji powoduje jedynie utworzenie nowej zmiennej wskazującej na ten sam obiekt.


Zobacz również