C# jest typowany statycznie: każda zmienna, pole i wyrażenie ma typ znany w czasie kompilacji. Język ma zestaw typów wbudowanych z własnymi słowami kluczowymi (int, double, string...). Każde słowo kluczowe to alias typu z przestrzeni nazw System, więc int i System.Int32 to ten sam typ, a string i System.String to ten sam typ.
Typy wbudowane
| Typ | Typ .NET | Rozmiar | Zakres lub precyzja | Literał |
|---|---|---|---|---|
bool | Boolean | 1 bajt | true lub false | true |
byte | Byte | 1 bajt | od 0 do 255 | 200 |
sbyte | SByte | 1 bajt | od -128 do 127 | -5 |
short | Int16 | 2 bajty | od -32,768 do 32,767 | 1200 |
ushort | UInt16 | 2 bajty | od 0 do 65,535 | 60000 |
int | Int32 | 4 bajty | od -2,147,483,648 do 2,147,483,647 | 42 |
uint | UInt32 | 4 bajty | od 0 do 4,294,967,295 | 42u |
long | Int64 | 8 bajtów | około ±9.2 × 10^18 | 42L |
ulong | UInt64 | 8 bajtów | od 0 do około 1.8 × 10^19 | 42ul |
float | Single | 4 bajty | około ±3.4 × 10^38, ~6 do 9 cyfr | 2.5f |
double | Double | 8 bajtów | około ±1.8 × 10^308, ~15 do 17 cyfr | 2.5 lub 2.5d |
decimal | Decimal | 16 bajtów | około ±7.9 × 10^28, 28 do 29 cyfr | 2.5m |
char | Char | 2 bajty | jedna jednostka kodowa UTF-16, od U+0000 do U+FFFF | 'A' |
string | String | referencja | tekst dowolnej długości, niemodyfikowalny | "hello" |
object | Object | referencja | podstawa każdego typu | new object() |
Literał całkowity bez przyrostka ma typ int, a jeśli jest za duży na int, to pierwszy z typów uint, long i ulong, który go pomieści; literał z kropką dziesiętną ma typ double. Dlatego float f = 2.5; i decimal d = 2.5; się nie kompilują: literał jest typu double, a nie ma niejawnej konwersji z double na float ani decimal. Dodaj przyrostek: 2.5f, 2.5m.
Każdy typ liczbowy udostępnia swoje granice jako stałe:
Wynik:
byte: 0 to 255
short: -32768 to 32767
int: -2147483648 to 2147483647
long: -9223372036854775808 to 9223372036854775807
decimal max: 79228162514264337593543950335
double max: 1.798E+308
sizes: int 4, long 8, double 8, char 2
255 161 3000000000
W praktyce większość kodu używa czterech z nich: int do liczników i indeksów, long do identyfikatorów i dużych sum, double do pomiarów i obliczeń matematycznych oraz decimal do pieniędzy.
decimal a double
double (i float) przechowują liczby w systemie dwójkowym. Większość ułamków dziesiętnych, w tym 0.1, nie ma dokładnej postaci binarnej, więc są zapisywane jako najbliższa możliwa wartość, a drobne błędy się kumulują. decimal przechowuje liczbę całkowitą w systemie dziesiętnym plus skalę, więc 0.1 to dokładnie 0.1.
Wynik:
False
True
0.30000000000000004
0.3
double sum == 1.0: False
decimal sum == 1.0: True
Total: 59.97
Split three ways: 19.99
Używaj decimal do pieniędzy, cen, podatków i wszystkiego, co człowiek zsumuje ręcznie i porówna. Zachowuje końcowe zera z danych wejściowych (1.50m wypisuje się jako 1.50) i ma od 28 do 29 cyfr znaczących.
Używaj double do pomiarów fizycznych, statystyki, geometrii i gier. Jest kilka razy szybszy, zajmuje połowę pamięci, a jego zakres jest ogromny. Nigdy nie porównuj dwóch obliczonych wartości double przez ==; porównaj różnicę z tolerancją: Math.Abs(x - y) < 1e-9.
decimal nie jest magiczny: 1m / 3m to nadal 0.3333333333333333333333333333, a arytmetyka na double nie rzuca wyjątku przy dzieleniu przez zero (daje nieskończoność albo NaN dla 0.0 / 0.0), podczas gdy dzielenie przez zero na decimal i liczbach całkowitych rzuca DivideByZeroException.
Przepełnienie liczb całkowitych i checked
Arytmetyka całkowita, która wychodzi poza zakres typu, domyślnie zawija się, po cichu. Kompilator wyłapuje przepełnienie w wyrażeniach stałych, ale nie wtedy, gdy wartości pochodzą ze zmiennych:
Wynik:
-2147483648
OverflowException
-1294967296
2147483648
checked(...) albo blok checked { ... } zamienia przepełnienie w OverflowException. Można to też włączyć dla całego projektu przez <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>. Zapis int x = int.MaxValue + 1; ze stałymi to błąd kompilacji (CS0220), bo kompilator widzi przepełnienie.
Ostatnia linia pokazuje rozwiązanie większości błędów przepełnienia: konwertuj na szerszy typ przed obliczeniem. (long)(max + 1) byłoby błędne, bo dodawanie zawinęło się, zanim zadziałało rzutowanie.
Typy wartościowe i referencyjne
Każdy typ w C# jest albo wartościowy, albo referencyjny, a ta różnica decyduje o tym, co robi przypisanie.
- Typy wartościowe: wszystkie typy liczbowe,
bool,char,DateTime, enumy i każdastruct. Zmienna zawiera dane; przypisanie je kopiuje. - Typy referencyjne:
string, tablice,objecti każdaclass. Zmienna zawiera referencję do obiektu; przypisanie kopiuje referencję, więc obie zmienne widzą ten sam obiekt.
Wynik:
a=10 b=99
first[0]=99
s1=cat s2=cats
string to typ referencyjny, który w praktyce zachowuje się jak wartość: jest niemodyfikowalny, a == porównuje tekst, a nie referencje. Więcej o typach wartościowych definiowanych przez użytkownika w strukturach.
char i bool
char to jedna jednostka kodowa UTF-16, zapisywana w pojedynczych cudzysłowach. Pod spodem to typ liczbowy, więc konwertuje się na int i obsługuje arytmetykę:
Wynik:
66
C
True
True
7
b
True
bool ma dokładnie dwie wartości i nie konwertuje się na liczby ani z liczb: if (count) to błąd kompilacji, a nie test na wartość różną od zera. Napisz if (count != 0). Znaki spoza Basic Multilingual Plane, takie jak większość emotek, zajmują dwie wartości char (parę zastępczą), więc Length w string liczy jednostki UTF-16, a nie widoczne znaki.
Wartości domyślne
Pola, elementy tablic i default(T) dostają wartość domyślną typu: zero dla liczb, false dla bool, '\0' dla char i null dla typów referencyjnych. Zmienne lokalne nie dostają wartości domyślnej i trzeba je przypisać przed użyciem.
Wynik:
0
0
False
True
0001-01-01
0,0,0
Od C# 7.1 można pisać literał default bez typu, gdy kompilator potrafi go wywnioskować (int x = default;). Typ wartościowy nie może być null i do tego służy int? (typ wartościowy nullable); zobacz typy nullable.
Liczby całkowite o rozmiarze natywnym i duże liczby całkowite
Dla szczególnych przypadków istnieją jeszcze dwa typy liczbowe. nint i nuint (C# 9) to liczby całkowite o rozmiarze wskaźnika, 32 lub 64 bity w zależności od platformy, używane przy współpracy z kodem natywnym i w kodzie niskopoziomowym. System.Numerics.BigInteger przechowuje liczby całkowite dowolnej wielkości kosztem szybkości:
using System.Numerics;
BigInteger factorial = 1;
for (int i = 2; i <= 30; i++) factorial *= i;
// 265252859812191058636308480000000
Najczęściej zadawane pytania
Jakie są typy danych w C#?
Typy wbudowane to liczby całkowite (sbyte, byte, short, ushort, int, uint, long, ulong), typy zmiennoprzecinkowe float i double, decimal do dokładnej arytmetyki dziesiętnej, char dla znaku UTF-16, bool oraz typy referencyjne string i object. Każde słowo kluczowe to alias typu .NET: int to System.Int32, string to System.String.
decimal czy double do pieniędzy w C#?
Używaj decimal. double przechowuje ułamki binarne, więc wartości takich jak 0.1 nie da się zapisać dokładnie, a sumy się rozjeżdżają (0.1 + 0.2 == 0.3 daje false). decimal przechowuje cyfry dziesiętne, zachowuje od 28 do 29 cyfr znaczących i daje wyniki, jakich oczekują księgowi. double jest szybszy i ma znacznie większy zakres, więc używaj go w nauce, grafice i pomiarach.
Jaka jest maksymalna wartość int w C#?
int.MaxValue to 2,147,483,647, a int.MinValue to -2,147,483,648 (32-bitowa liczba całkowita ze znakiem). Dodanie 1 do int.MaxValue po cichu zawija się do int.MinValue, chyba że kod działa w kontekście checked, gdzie rzuca OverflowException. Dla większych wartości użyj long (do około 9,2 tryliona).
Czym różni się float od double w C#?
float ma 32 bity i około 6 do 9 cyfr znaczących; double ma 64 bity i około 15 do 17. Literały zmiennoprzecinkowe są domyślnie typu double, więc float wymaga przyrostka f: float speed = 2.5f;. Używaj double, chyba że pamięć albo API (na przykład Vector3 w Unity) wymaga float.
Czym różnią się typy wartościowe od referencyjnych w C#?
Zmienna typu wartościowego (int, double, bool, DateTime, dowolna struct) przechowuje same dane, a przypisanie je kopiuje. Zmienna typu referencyjnego (string, tablice, dowolna class) przechowuje referencję do obiektu na stercie, a przypisanie kopiuje referencję, więc dwie zmienne mogą wskazywać ten sam obiekt.