Poniższy artykuł pochodzi z serii Przygotowań do egzaminu 70-536.
Framework Microsoft .NET posiada dwie podstawowe grupy zmiennych – wartościowe, przekazywane przez wartość – np. Decimal, oraz referencyjne - przekazywane przez referencję, np. String. Postaram się przedstawić pierwszą grupę zmiennych.
Typy wbudowane
Typy przekazywane przez wartość stanowią dużą cześć wbudowanych typów frameworka .NET. Przykładem są Value Types (np. System.Byte, System.Int32, System.Double) – wszystkie są przekazywane przez wartość. Oto przykład pokazujący różnicę zachowania typu wartościowego i referencyjnego:
1: // przykład 1 - Value Type
2: System.Int32 licznik = 7; // int
3: System.Int32 licznik2 = licznik;
4: licznik2 += 1;
5:
6: Console.WriteLine(licznik + ", " + licznik2); // 7, 8
7:
8: // przykład 2 - Reference Type
9: ArrayList tablica = new ArrayList {1, 2, 3};
10: ArrayList tablica2 = tablica;
11: tablica2.Add(7);
12:
13: Console.WriteLine(tablica.Count+", "+tablica2.Count); // 4, 4
W pierwszym wypadku zmiana jednej z wartości nie powoduje zmiany drugiej. Instancje zmiennych typów wartościowych bezpośrednio przechowują wartość na stosie. Stos jest strukturą, gdzie dane mogą być dodawane na wierzch stosu i z wierzchołka stosu pobierane. Jest to szybkie rozwiązanie, które gwarantuje dużą wydajność. Przypisanie powoduje skopiowanie danych, obie zmienne przechowywane są w innym miejscu na stosie (w przypadku przekazania zmiennej wartościowej jako parametru wywoływanej metody także operujemy na jej kopii!).
W drugim przypadku do zmiennej tablica2 nie zostały przekazane wartości, a referencja - wskaźnik, odnośnik do miejsca w pamięci, gdzie przechowywane są wartości. Dwie nazwy wskazują teraz na ten sam obszar pamięci.
Inne, często spotykane zmienne wartościowe, to np. System.Char, System.Boolean, System.IntPtr, System.DateTime.
Więszkość typów podstawowych ma zdefiniowane aliasy, które są równoważne wykorzystaniu pełnych nazw typów. Przykładowo zamiast System.Uint32 możemy stosować alias uint.
Warto zauważyć także, że tworząc zmienne nie wykorzystujemy słowa kluczowego new – konstruktor takich typów jest ukryty, deklaracja zmiennej tworzy instancję zmiennej, przypisując jej przy tym domyślną wartość.
Jeżeli chcemy mieć możliwość stwierdzenia, czy zmiennej została przypisana wartość, możemy określić zmienną jako Nullable. Pozwala to przypisać jej wartość null. Poniższy kod pozwala utworzyć zmienną typu Boolean, która może przyjmować wartości true, false lub null.
1: Nullable<bool> b = null;
2: Nullable<int> i = null;
3: // lub krócej…
4: bool? b = null;
5: int? i = null;
Zmienną, która w ten sposób „awansuje”, możemy sprawdzić i stwierdzić, czy ma ona ustawioną wartość, czy nie. Wykorzystujemy w tym celu pola HasValue i Value.

Wyliczenia
Wyliczenie dostarcza listę wyborów, które są zbiorem określonych wartości powiązanych ze sobą. Zwiększają one czytelność i bezpieczeństwo kodu. Jako przykład możemy sobie wyobrazić pole typu numerycznego, które przechowuje liczbę reprezentującą poziom uprawnień użytkownika. Jest to rozwiązanie niebezpieczne, gdyż zbiór wartości jest ograniczony jedynie typem zmiennej. Zastosowanie wyliczenia pozwala wypisać zbiór poziomów, ograniczyć wartości pola tylko do nich. Inny przykład:
1: enum Genders : int { Male, Female };
Definiujemy typ wyliczeniowy Genders (płcie), który może przyjąć dwie wartości: Male (mężczyzna) i Female (kobieta).
1: class Program
2: {
3: enum Genders { Male, Female}; // enum Genders : int { Male = 0, Female = 1}
4: static void Main(string[] args)
5: {
6: Genders gender = Genders.Male;
7: gender = (Genders)1; // Female
8: Console.WriteLine(gender.ToString()); // Female
9: Console.WriteLine((int)Genders.Male + ", " + (int)Genders.Female); // 0, 1
10: Console.ReadKey();
11: }
12: }
Przy okazji ujawnia się ciekawe zachowanie:
1: gender = 0; // Male,
2: gender = (Genders) 0; // Type cast is redundant
3: //gender = 1; // błąd!
4: gender = (Genders) 1; // działa, rzutowanie wymagane!
Inna ciekawosta:
1: enum Genders { Male = 3, Female};
2: gender = (Genders)1;
3: Console.WriteLine(gender.ToString()); // 1
Typy zdefiniowane przez użytkownika
Zakładamy, że chcemy utworzyć byt, który ma składać się wyłącznie z typów wartościowych. Ma przy tym być tak wydajny, jak to tylko możliwe. Także przekazując go jako parametr metody, chcemy aby nie ulegał zmianie.
Co będzie najlepszym rozwiązaniem? Struktura – definiowana przez użytkownika, przechowywana na stosie, niemal identyczna do klasy. Grupuje ona powiązane dane, co sprawia, że łatwiej się na nich pracuje. Takim przykładem jest System.Drawing.Point.
1: System.Drawing.Point p = new System.Drawing.Point(20, 30);
2: Console.WriteLine("Point X {0}, Y {1}", p.X, p.Y);
Podczas inicjowania struktur korzystamy ze słowa kluczowego new.
Przykład struktury z wykorzystaniem poprzednio utworzonego typu wyliczeniowego Genders:
1: struct Person
2: {
3: public string firstName;
4: public string lastName;
5: public int age;
6: public Genders gender;
7:
8: public Person(string firstName, string lastName, int age, Genders gender)
9: {
10: this.firstName = firstName;
11: this.lastName = lastName;
12: this.age = age;
13: this.gender = gender;
14: }
15:
16: public override string ToString()
17: {
18: return firstName + " " + lastName + "("+ gender +"), age " + age;
19: }
20: }
Musimy pamiętać, aby struktura spełniała pewne kryteria:
- Logicznie reprezentuje pojedynczą wartość (np. punkt)
- Rozmiar instancji nie zajmuje więcej niż 16 bajtów
- Nie będzie zmieniana po utworzeniu (refleksja)
- Nie będzie rzutowana na typ referencyjny
Jeśli chcemy zamienić strukturę w klasę wystarczy zmiana słowa kluczowego ze struct na class.
Kolejny artykuł z serii to 70-536:Using Common Reference Types