Spis treści Poprzednia strona: Zmienne Następna strona: Wyliczenia
Typy to podstawa. Jeśli ktoś nie zrozumie typów, to może zapomnieć o programowaniu. Musisz wiedzieć, że:
- Każda wartość (literał) ma swój typ.
- Każda zmienna i stała ma swój typ.
- Typ zmiennej mówi nam o tym jakich typów wartości można w tej zmiennej zapisać i z niej odczytać. Jeśli spróbujemy zapisać lub odczytać wartość innego typu, to nastąpi konwersja typu jeśli jest możliwa, a jeśli nie – wystąpi błąd.
- Każdy typ ma swoją nazwę (identyfikator). Nazwy większości typów podstawowych są tak ważne, że są słowami kluczowymi i edytor VS wyróżnia je na niebiesko (SD: trochę z przekory na czerwono).
- Nawet brak typu oznaczany jest przez specjalny typ void. Będzie o tym w rozdziale o funkcjach, czyli metodach.
Oto jest tabelka podstawowych typów. Programista obudzony w środku nocy powinien móc ją całą odtworzyć z pamięci:
Kategoria | Nazwy | Opis | |
Typy wartościowe | Typy proste | sbyte, short, int, long | Liczby całkowite ze znakiem |
byte, ushort, uint, ulong | Liczby całkowite bez znaku | ||
float, double | Liczby zmiennoprzecinkowe | ||
decimal | Liczba stałoprzecinkowa | ||
char | Znak Unicode | ||
bool | Typ logiczny | ||
Wyliczenia | enum | Grupy stałych całkowitych | |
Struktury | struct | Grupy typów wartościowych | |
Typy z wartością null | T?, Nullable<T> | Typy dopuszczające wartość null | |
Typy referencyjne | Klasy | object | Podstawowy typ wszystkich obiektów |
string | Łańcuchy znaków Unicode, teksty | ||
class | Klasy zdefiniowane w bibliotekach lub przez użytkownika | ||
Interfejsy | interface | Deklaracje składowych klas bez ich implementacji | |
Tablice | T[], T[,], T[,,] | Uporządkowane zbiory skończonej ilości zmiennych tego samego typu T (wartościowego lub referencyjnego), dostęp do poszczególnych zmiennych przez całkowity nieujemny indeks w nawiasach kwadratowych bezpośrednio przylegających z prawej strony do nazwy tablicy | |
Delegaty | delegate, event | Sygnatury metod |
Literały liczb całkowitych to na przykład: 0, 255, 65535, 2147483647, -2147483648, 1L, 1U, 1UL, 1LU, 0123, 0xABCDEF. Literki L i U to tzw. sufiksy, inaczej przyrostki, oznaczające odpowiednio long (użyj 64 bitów) i unsigned (bez znaku). Literki 0x to prefiks, inaczej przedrostek, oznaczający zapis liczby w systemie szesnastkowym [hexadecimal] w skrócie [hex]. Cyfry szesnastkowe to 0123456789ABCDEF. W języku C# nie ma zapisu ósemkowego, czyli liczba 01232 to 1232, a nie 666, więc początkowe zera z lewej strony liczby nie mają specjalnego znaczenia i są ignorowane. Specjalne znaczenie ma tylko prefiks 0x. Literały oznaczające liczby zmiennoprzecinkowe (a to nie jest to samo, co liczby rzeczywiste) to na przykład: 1.5, -0.67, 2e12, 5.3E-8, 0.0, 3F, 4.123D. Literki F i D oznaczają odpowiednio float (liczba tzw. pojedynczej precyzji, około 7 cyfr znaczących) i double (liczba tzw. podwójnej precyzji, około 15 cyfr znaczących). Jeśli robimy jakieś większe obliczenia, to float zwykle jest „za słaby” i najczęściej zamiast niego używamy double. Dlatego też jeśli literał zmiennoprzecinkowy nie ma sufiksu, to domyślnie jest typu double. Notacja z literką e lub E to tzw. notacja naukowa i oznacza pomnożenie liczby z lewej przed E przez dziesięć do potęgi takiej, jaka liczba całkowita znajduje się z prawej za E. Sufiks dla liczb typu decimal to literka M (typ decimal przechowuje około 28 cyfr znaczących). Cyfry znaczące określają maksymalną precyzję [precision] danej liczby, a nie jej dokładność [accuracy] i nie należy mylić tych pojęć.
Literały typu char to tzw. stałe znakowe. Przykłady: 'a’, 'A’, '\n’, '\x20′, '\u00a9′. Błędem jest umieszczenie w apostrofach dwóch znaków, tak 'ab’, albo nie umieszczenie żadnego tak ”. Spotkałem kiedyś taki żart, że w pliku źródłowym C# była stała znakowa bez żadnego znaku i nie powodowała błędu. Szybko się okazało, że to tylko złudzenie. Tam był znak o kodzie 127, który w edytorze VS ma zerową szerokość. To taki mały, zupełnie nieszkodliwy dowcip Microsoft-u. Powyżej jednego znaku to już musi być łańcuch znaków typu string, czyli na przykład „ab”. Każdy znak ma swój numer (16-bitowy kod) w międzynarodowym standardzie Unicode. Unicode to system 32-bitowy, więc niektóre znaki mogą być tylko w postaci string, bo pojedynczy char nie wystarczy – muszą być dwa (2 * 16 = 32). Numer ten podaje się w postaci szesnastkowej i tak, na przykład '\u00a9′ to znak © o kodzie dziesiętnym 169. Znak '\x20′ to spacja. Symbol \x różni się od \u tym, że interpretuje największą ilość cyfr szesnastkowych za nim, a \u dokładnie cztery (stąd przed a9 potrzebne były dwa zera). Kod znaku uzyskujemy dzięki rzutowaniu znaku na typ int tak (int)znak, a znak o określonym kodzie uzyskujemy odwrotnie rzutując kod typu int na typ char tak (char)kod. Kod n-tego znaku łańcucha znaków s to (int)s[n]. Łańcuchy znaków typu string, tzw. stringi albo teksty to na przykład: „A kuku”, @”Fajny tekst”, „Słowo w \”cudzysłowach\””. Zmienne typu logicznego bool mogą mieć tylko dwie wartości: true i false, obie będące słowami kluczowymi.
Jaka jest różnica między typami wartościowymi, a referencyjnymi? Typy wartościowe:
- są strukturą struct (która pochodzi od klasy abstrakcyjnej System.ValueType, a ta z kolei pochodzi od System.Object)
- nie mogą być null, chyba że „opakujemy” je przez klasę generyczną Nullable<T> albo za nazwą typu umieścimy znak zapytania tak T? (nie można tego robić dwukrotnie, tzn. T?? jest błędem)
- zmienne takiego typu zawierają swoje dane bezpośrednio tam, gdzie istnieją, czyli na tzw. stosie
- nie podlegają dziedziczeniu (niejawnie dziedziczą po typie System.ValueType oraz po object)
Typy referencyjne:
- pochodzą od klasy class
- mogą być null
- zmienne takiego typu zawierają referencje do danych przechowywanych gdzie indziej, na tzw. stercie
- podlegają pojedynczemu dziedziczeniu (chyba że je zabronimy przy pomocy modyfikatora sealed)
Struktury zostaną omówione w rozdziale o plikach, bo tam jest ich najczęstsze zastosowanie. Należy pamiętać, że typ string nie jest typem wartościowym, ale często zachowuje się podobnie jak typy wartościowe (np.: operacja porównania porównuje wartości, a nie referencje). Czym się różni stos od sterty? Po to odsyłam do rozdziału o algorytmach. Wyliczeniom, łańcuchom znaków (tzw. stringom) i tablicom poświęcone są osobne rozdziały. Struktury Nullable<T> są omówione przy okazji baz danych.
Powyższe nazwy typów są tylko skrótami „prawdziwych” nazw. Oto tabelka pełnych nazw typów z podstawowej biblioteki klas .NET:
byte | System.Byte |
sbyte | System.SByte |
ushort | System.UInt16 |
short | System.Int16 |
uint | System.UInt32 |
int | System.Int32 |
ulong | System.UInt64 |
long | System.Int64 |
float | System.Single |
double | System.Double |
decimal | System.Decimal |
bool | System.Boolean |
char | System.Char |
string | System.String |
object | System.Object |
Możliwe jest pisanie w języku C# w stylu nieco podobnym do tego znanego z języka JavaScript, czyli bez jawnego deklarowania typów. Typ zmiennej określa wtedy przypisana jej wartość. Typ zmiennej zostaje potem na stałe (zmienne var), albo aż do przypisania jej wartości innego typu (zmienne object lub dynamic). Popatrzmy na przykład:
int i = 1; var v = 2; // typ określony niejawnie (przez wartość) object o = 3; // zapakowanie [boxing] int w object dynamic d = 4; // zmienna dynamiczna z typem int Console.WriteLine(i.GetType()); // wypisze System.Int32 Console.WriteLine(v.GetType()); // wypisze System.Int32 Console.WriteLine(o.GetType()); // wypisze System.Int32 Console.WriteLine(d.GetType()); // wypisze System.Int32 v = "halo string"; // błąd kompilacji, niezgodność typów, v to int o = o + 10; // błąd kompilacji o = (int)o + 10; // ok, rozpakowanie [unboxing] d = "cztery"; // dynamiczna zmiana typu zmiennej d o = d + 2; // brak błędu, o == "cztery2" o = d * 2; // błąd wykonania (mnożenie string-ów nie ma sensu) o = 2.0; // brak błędu (kolejny boxing) d = 3.0F; // brak błędu Console.WriteLine(o.GetType()); // System.Double Console.WriteLine(d.GetType()); // System.Single var x = new { }; // nowy obiekt typu anonimowego Console.WriteLine(x.GetType()); x = new { Akuku = 3 }; // błąd kompilacji, niezgodność typów var x2 = new { Akuku = 3 }; x2 = new { Akuku = 9 }; Console.WriteLine(x2.GetType()); Console.WriteLine(x2.Akuku); Console.WriteLine(x2.GetType().GetProperty("Akuku").GetValue(x2, null)); o = x; // bez błędu o = x2; // bez błędu
No to po kolei:
- Definicja zmiennej „i” typu int. Normalna zainicjalizowana zmienna.
- Zmienna „typu var” to sygnał dla kompilatora, że typ ma określić sam, po pierwszej przypisanej tej zmiennej wartości. To jest typ określony niejawnie [implicitly]. Po co to? Jeśli nazwa typu jest długa, to piszemy var i mamy tę nazwę z głowy. Czasem mamy coś i nie wiemy jakiego jest typu, no to odruchowo przypisujemy to coś do zmiennej var, a potem sprawdzamy jaki jest typ tej zmiennej.
- Zmienna typu object może przechowywać dosłownie wszystko, zarówno typy wartościowe, jak i referencyjne, a tutaj typ int. Zapakowanie [boxing] polega tu na zapisaniu wartości int na stercie i na umieszczeniu w zmiennej typu object referencji do tej wartości int. Sam int nie jest typem referencyjnym, ale zapakowany w object – jest.
- Zmienna dynamiczna d nie ma określonego typu na etapie kompilacji. Zostanie on określony w trakcie działania programu [at runtime]. Kontrola typów nastąpi, ale dopiero w trakcie działania programu, dlatego trzeba uważać, bo kompilator nam nie pomaga pisać poprawne (tzn. istniejące i mające sens) operacje.
- Typ zmiennej to int, czyli System.Int32
- Jak wyżej. Metoda GetType() pochodzi z typu object. W jaki sposób typ int ją posiada? w Języku C# wszystko jest obiektem, zarówno class (typ referencyjny) jak i struct (typ wartościowy).
- To nie jest System.Object, tylko System.Int32. Tak działa boxing.
- Dostajemy System.Int32. To, co zwraca metoda GetType() to zawsze jest wewnętrzny [internal] System.RuntimeType, a nie abstrakcyjny [abstract] System.Type (ale to prawie nigdy nie jest istotna różnica).
- No właśnie, typ zmiennej v został już określony w linii 2 jako int. To jest błąd kompilacji. Jeśli próbujesz uruchomić ten przykład, to aby przejść dalej po prostu wykomentuj tę linię, czyli dodaj // przed pierwszym nie białym znakiem.
- Nie można dodać 10 do… nie wiadomo czego. Zmienna typu object może być czymkolwiek.
- Jeśli wiemy, że zmienna o to zapudełkowany int, to możemy go wyjąć z pudełka. Ta operacja to tzw. unboxing.
- Zmienna dynamiczna może przechowywać wszystko, podobnie jak object. W rzeczywistości to wewnętrznie jest object, tylko trochę inaczej kompilowany. W .NET Framework nie ma typu dynamic w sensie stricte (np.: czegoś w rodzaju System.Dynamic), to tylko odrobinę magii wykonywanej przez kompilator.
- Tak, można dodać liczbę do tekstu i wtedy liczba zostanie zawsze zamieniona na tekst (tylko, że nie mamy kontroli nad tym jak zostanie zamieniona, ale uwaga, bo ten brak kontroli bywa kłopotliwy).
- Kolejna błędna linia, ale tym razem z błędem wykonania [runtime error]. Kompilator to przepuszcza właśnie ze względu na typ dynamic. Nie posiada on kontroli w czasie kompilacji i tym się różni od typu object.
- Zmieniamy typ zmiennej o na domyślny zmiennoprzecinkowy, czyli double.
- Zmieniamy typ zmiennej d na zmiennoprzecinkowy, ale nie domyślny. Przyrostek [suffix] „F” oznacza, że jest to literał typu float, a nie double.
- Zmienna o to teraz rzeczywiście double.
- Zmienna d to teraz rzeczywiście float, a dla .NET Framework to System.Single, czyli pojedyncza precyzja (4 bajty).
- Czary mary. Słowo kluczowe new (lub jak kto woli operator new) służy do tworzenia nowych obiektów. Co jest tym obiektem? Pusty nawias, czyli nic, a tak naprawdę, to klasa, która nie posiada ani nazwy, ani żadnych pól, metod i właściwości. Ta nicość ma jednak sens. To tzw. typ anonimowy (bo nie ma nazwy). Jest on po prostu identyfikowany poprzez swoją własną wewnętrzną strukturę (akurat tutaj pustą), a nie przez nazwę. Coś takiego można przypisać tylko pod zmienną var, object albo dynamic, bo nie wiadomo co to takiego (nie ma nazwy).
- Dowód na to, że x ma typ i jest to typ anonimowy. Na konsoli wypisane zostaje coś dziwnego: „<>f__AnonymousType0”. Acha, czyli jednak jest nazwa, tylko automatycznie przydzielana. Tu się kryje ta anonimowość. Jest to anonimowość pozorna. Dlatego nie skłamałem na początku tego rozdziału, gdzie napisałem, że każdy typ ma swoją nazwę.
- To nie prawda, że nie wiadomo co to x. Wiadomo, że x nie powinien mieć Akuku, więc mamy błąd. Co to jest to Akuku? To tzw. właściwość [property], ale to w tej chwili nie jest istotne.
- Nowa zmienna jak najbardziej może mieć Akuku.
- A nawet Akuku o innej wartości.
- Zmienna x2 też jest anonimowa, ale anonimowa inaczej: posiada jedną właściwość typu int, co widać w wyniku wypisania typu na konsoli: „<>f__AnonymousType1`1[System.Int32]”.
- Skoro x2 posiada Akuku, to można to wyświetlić.
- Do Akuku można się też dobrać przy pomocy tzw. refleksji. Straszna sprawa. Tutaj tylko po to, żeby pokazać, że w języku C# wszystko (no, prawie wszystko) jest możliwe.
- Zmienna typu object może zawierać wszystko, więc także obiekt typu anonimowego.
- Jak wyżej.
A zatem. Rozumiesz już typy? Tylko spokojnie, powoli i po kolei. To dopiero początek…
Spis treści Poprzednia strona: Zmienne Następna strona: Wyliczenia