W poprzednim wpisie napisaliśmy nasz pierwszy program w C# 14. Używaliśmy Console.WriteLine() i zmiennych typu string. Ale co to właściwie znaczy "typ"? Dlaczego musimy pisać int wiek zamiast tylko wiek?
W tym wpisie zagłębimy się w system typów C#. Poznamy typy wartościowe i referencyjne, nauczymy się jak działa nullable reference types (rewolucja z C# 8!), zobaczymy target-typed new, init-only properties i required members.
🔍 Czym jest "typ" w programowaniu?
Typ mówi kompilatorowi:
Jaki rodzaj danych przechowuje zmienna (liczba? tekst? prawda/fałsz?)
Ile pamięci zarezerwować (int = 4 bajty, long = 8 bajtów)
Jakie operacje są dozwolone (na liczbach możesz +, -, *, / ale nie na tekście!)
C# jest statically-typed (statycznie typowany) – musisz zadeklarować typ zmiennej i NIE możesz go zmienić. To zapobiega MNÓSTWU błędów! 🛡️
Typy wartościowe vs typy referencyjne
Fundamentalna różnica
W C# wszystkie typy dzielą się na dwie kategorie:
Cecha
Typy wartościowe Value Types
Typy referencyjne Reference Types
Przechowywanie
Wartość bezpośrednio w zmiennej
Referencja (adres) do obiektu w pamięci
Lokalizacja w pamięci
Stack (stos) - szybki dostęp
Heap (sterta) - wolniejszy dostęp
Kopiowanie
Kopia wartości (niezależne kopie)
Kopia referencji (obie zmienne wskazują na ten sam obiekt)
Może być null?
Nie (chyba że nullable: int?)
Tak (domyślnie)
Przykłady
int, double, bool, struct, enum
string, class, array, delegate
🔍 Stack vs Heap - co to znaczy?
Stack (stos) – jak stos talerzy. Szybki dostęp, automatyczne czyszczenie (gdy metoda kończy działanie), ograniczony rozmiar.
Heap (sterta) – jak magazyn. Wolniejszy dostęp, wymaga Garbage Collectora do czyszczenia, duży rozmiar.
Nie musisz o tym myśleć na co dzień! C# zarządza pamięcią automatycznie. Ale warto wiedzieć że istnieje różnica. 👍
Przykład - różnica w kopiowaniu
// TYPY WARTOŚCIOWE - kopia wartości
int a = 5;
int b = a; // b dostaje KOPIĘ wartości (5)
b = 10; // zmieniamy b
Console.WriteLine($"a = {a}"); // a = 5 (nie zmienione!)
Console.WriteLine($"b = {b}"); // b = 10
// TYPY REFERENCYJNE - kopia referencji
var osoba1 = new Osoba { Imie = "Jan" };
var osoba2 = osoba1; // osoba2 dostaje KOPIĘ REFERENCJI (wskaźnik do tego samego obiektu)
osoba2.Imie = "Anna"; // zmieniamy przez osoba2
Console.WriteLine($"osoba1.Imie = {osoba1.Imie}"); // Anna (zmienione!)
Console.WriteLine($"osoba2.Imie = {osoba2.Imie}"); // Anna
// Obie zmienne wskazują na TEN SAM obiekt w pamięci!
⚠️ Częsty błąd początkujących!
Myślisz że kopiujesz obiekt, a tak naprawdę kopiujesz tylko referencję (wskaźnik). Obie zmienne NADAL wskazują na ten sam obiekt!
var lista1 = new List<int> { 1, 2, 3 };
var lista2 = lista1; // ❌ To NIE jest kopia listy!
lista2.Add(4);
Console.WriteLine(lista1.Count); // 4 (lista1 też się zmieniła!)
Jeśli chcesz prawdziwą kopię, musisz użyć metody .ToList() lub podobnej.
Podstawowe typy wartościowe
Liczby całkowite
Typ
Rozmiar
Zakres
Kiedy używać?
byte
1 bajt
0 do 255
Małe liczby nieujemne (kolory RGB, bajty)
sbyte
1 bajt
-128 do 127
Małe liczby ze znakiem (rzadko używane)
short
2 bajty
-32,768 do 32,767
Średnie liczby (rzadko używane)
int
4 bajty
-2.1 miliarda do 2.1 miliarda
⭐ DOMYŚLNY dla liczb całkowitych
long
8 bajtów
-9.2 tryliony do 9.2 tryliony
Bardzo duże liczby (timestamp, ID w bazach)
uint
4 bajty
0 do 4.2 miliarda
Liczby nieujemne (rzadko używane)
ulong
8 bajtów
0 do 18.4 trylionów
Bardzo duże liczby nieujemne
💡 Kiedy używać którego typu liczb?
99% czasu używaj int! Nawet dla małych liczb jak wiek (0-120).
int wiek = 25; // ✅ Standardowo
int liczbaUzytkownikow = 1000; // ✅
int punkty = -50; // ✅ (może być ujemne)
long userId = 123456789012345; // ✅ Dla bardzo dużych liczb (ID z bazy)
byte czerwony = 255; // ✅ Dla RGB (0-255)
Liczby zmiennoprzecinkowe
Typ
Rozmiar
Precyzja
Kiedy używać?
float
4 bajty
~6-9 cyfr
Grafika, gry (wymaga suffiksu f)
double
8 bajtów
~15-17 cyfr
⭐ DOMYŚLNY dla liczb zmiennoprzecinkowych
decimal
16 bajtów
28-29 cyfr
Finanse, pieniądze (wymaga suffiksu m)
float pi1 = 3.14f; // f na końcu - WYMAGANE dla float
double pi2 = 3.14; // domyślnie double (bez suffiksu)
decimal cena = 19.99m; // m na końcu - WYMAGANE dla decimal
// Dlaczego decimal dla pieniędzy?
double zle = 0.1 + 0.2;
Console.WriteLine(zle); // 0.30000000000000004 😱
decimal dobrze = 0.1m + 0.2m;
Console.WriteLine(dobrze); // 0.3 ✅
⚠️ NIGDY nie używaj float/double do pieniędzy!
Liczby zmiennoprzecinkowe (float, double) są niedokładne przez sposób reprezentacji w pamięci. Dla pieniędzy ZAWSZE używaj decimal!
// bool - prawda/fałsz
bool czyJestPelnoletni = true;
bool czyJestZalogowany = false;
// char - pojedynczy znak (Unicode)
char litera = 'A';
char emotka = '😊'; // C# wspiera Unicode!
// DateTime - data i czas (struct, czyli value type!)
DateTime teraz = DateTime.Now;
DateTime jutro = DateTime.Now.AddDays(1);
Console.WriteLine(teraz); // 2026-02-05 14:30:00
🔍 Czy DateTime to value type czy reference type?
DateTime to struct, więc value type! Mimo że wygląda jak obiekt (ma metody jak .AddDays()), jest przechowywany na stosie i kopiuje się przez wartość.
To częste pytanie na rozmowach kwalifikacyjnych! 😉
Nullable Value Types - typ? z pytajnikiem
Problem: typy wartościowe nie mogą być null
int wiek = null; // ❌ BŁĄD! int nie może być null
bool czyPotwierdzony = null; // ❌ BŁĄD!
Ale czasami POTRZEBUJESZ null! Na przykład:
Pole "Data urodzenia" w formularzu - użytkownik może nie wypełnić
Wynik z bazy danych - kolumna może być NULL
Opcjonalne parametry
Rozwiązanie: Nullable<T> lub T?
// Dwa równoważne sposoby zapisu:
Nullable<int> wiek1 = null; // verbose
int? wiek2 = null; // ✅ Krótka składnia (używaj tej!)
// Nullable może przechowywać wartość LUB null
int? wynik = null;
wynik = 42;
wynik = null; // znowu null
// Sprawdzanie czy ma wartość
if (wynik.HasValue)
{
Console.WriteLine($"Wartość: {wynik.Value}");
}
else
{
Console.WriteLine("Brak wartości (null)");
}
// Lub krócej z null-conditional operator
Console.WriteLine($"Wartość: {wynik?.ToString() ?? "brak"}");
💡 Praktyczny przykład - formularz użytkownika
class Uzytkownik
{
public string Imie { get; set; } // WYMAGANE
public string Nazwisko { get; set; } // WYMAGANE
public int? Wiek { get; set; } // OPCJONALNE (może być null)
public DateTime? DataUrodzenia { get; set; } // OPCJONALNE
}
var user = new Uzytkownik
{
Imie = "Jan",
Nazwisko = "Kowalski"
// Wiek i DataUrodzenia są null (nie wypełnione)
};
// Bezpieczne sprawdzenie
if (user.Wiek.HasValue)
{
Console.WriteLine($"Wiek: {user.Wiek.Value}");
}
else
{
Console.WriteLine("Wiek nie podany");
}
Null-coalescing operators
int? wynik = null;
// Operator ?? - "jeśli null, użyj wartości domyślnej"
int bezpiecznyWynik = wynik ?? 0; // jeśli wynik jest null, zwróć 0
Console.WriteLine(bezpiecznyWynik); // 0
// Operator ??= - "jeśli null, przypisz wartość" (C# 8+)
int? liczba = null;
liczba ??= 10; // jeśli liczba jest null, przypisz 10
Console.WriteLine(liczba); // 10
liczba ??= 20; // liczba NIE jest null (=10), więc nic się nie zmienia
Console.WriteLine(liczba); // 10
❌ Stary sposób (2015)
int? wiek = GetWiekFromDatabase();
int finalWiek;
if (wiek.HasValue)
{
finalWiek = wiek.Value;
}
else
{
finalWiek = 18; // domyślny
}
Console.WriteLine(finalWiek);
✅ Nowoczesny sposób (2026)
int? wiek = GetWiekFromDatabase();
int finalWiek = wiek ?? 18; // jedna linia!
Console.WriteLine(finalWiek);
Nullable Reference Types - rewolucja z C# 8!
Problem: NullReferenceException - najbardziej popularny błąd w C#
Przez lata, najbardziej popularnym błędem w aplikacjach C# był:
System.NullReferenceException: Object reference not set to an instance of an object.
Ten błąd występuje gdy próbujesz użyć zmiennej która jest null:
W starym C# (przed C# 8), KAŻDA zmienna typu referencyjnego mogła być null. Kompilator NIE ostrzegał Cię. Dowiadywałeś się dopiero w runtime gdy aplikacja się wysypywała. 😱
// Stary C# - kompilator NIE widzi problemu
string GetUserName(int userId)
{
if (userId == 0)
return null; // ⚠️ Zwracam null
return "Jan";
}
var name = GetUserName(0);
Console.WriteLine(name.ToUpper()); // 💥 Runtime error!
Rozwiązanie: Nullable Reference Types (C# 8+)
Od C# 8.0 (2019) mamy nullable reference types. To zmienia WSZYSTKO!
🔍 Jak to działa?
Od teraz w C#:
string - NIE może być null (non-nullable)
string? - MOŻE być null (nullable)
Kompilator ostrzega gdy robisz coś niebezpiecznego z null!
// W .NET 10 z włączonymi nullable reference types:
// ✅ Non-nullable string - nie może być null
string imie = "Jan";
imie = null; // ⚠️ WARNING: Cannot convert null to non-nullable string
// ✅ Nullable string - może być null
string? nazwisko = null; // OK!
nazwisko = "Kowalski"; // OK!
// ⚠️ Kompilator ostrzega przed dereferencją nullable
string? potencjalnieNull = GetNazwisko();
Console.WriteLine(potencjalnieNull.ToUpper());
// ⚠️ WARNING: Dereference of a possibly null reference
// ✅ Bezpieczne sposoby:
// 1. Null check
if (potencjalnieNull != null)
{
Console.WriteLine(potencjalnieNull.ToUpper()); // ✅ OK
}
// 2. Null-conditional operator
Console.WriteLine(potencjalnieNull?.ToUpper()); // ✅ OK (zwróci null jeśli potencjalnieNull jest null)
// 3. Null-coalescing
Console.WriteLine(potencjalnieNull?.ToUpper() ?? "BRAK"); // ✅ OK
Jak włączyć nullable reference types?
W .NET 10, nowe projekty mają to włączone domyślnie! Sprawdź plik .csproj:
Jeśli masz stary projekt (sprzed C# 8), nullable reference types są wyłączone dla kompatybilności wstecznej. Możesz włączyć dodając <Nullable>enable</Nullable> do .csproj.
Ale uwaga - dostaniesz MNÓSTWO warningów! Warto je poprawić, ale może to zająć czas w dużych projektach.
Null-forgiving operator (!) - ostateczność
string? potencjalnieNull = GetString();
// Kompilator: ⚠️ WARNING: możliwy null
Console.WriteLine(potencjalnieNull.ToUpper());
// Jeśli JESTEŚ PEWIEN że nie jest null, możesz użyć !
Console.WriteLine(potencjalnieNull!.ToUpper()); // ! = "ufam mi, to nie jest null"
⚠️ Używaj ! bardzo oszczędnie!
Operator ! (null-forgiving) mówi kompilatorowi "zamknij się, wiem co robię". Ale jeśli się mylisz, dostaniesz NullReferenceException w runtime!
// Utworzenie stringa
string imie = "Jan";
string nazwisko = "Kowalski";
// String interpolation (C# 6+) - UŻYWAJ TEGO!
string powitanie = $"Cześć, {imie} {nazwisko}!";
Console.WriteLine(powitanie); // Cześć, Jan Kowalski!
// Stary sposób (NIE używaj) - konkatenacja
string starySpośob = "Cześć, " + imie + " " + nazwisko + "!"; // ❌ brzydkie
// Multi-line strings - RAW string literals (C# 11+)
string dlugitekst = """
To jest długi tekst
który ma wiele linii.
Nie musisz używać \n!
""";
// String z wyrażeniami
int wiek = 30;
string info = $"{imie} ma {wiek} lat, więc urodzil się w {2026 - wiek} roku.";
💡 String interpolation - zaawansowane użycie
// Formatowanie liczb
decimal cena = 19.99m;
Console.WriteLine($"Cena: {cena:C}"); // Cena: 19,99 zł
// Formatowanie dat
DateTime teraz = DateTime.Now;
Console.WriteLine($"Dzisiaj: {teraz:yyyy-MM-dd}"); // 2026-02-05
// Wyrównanie
string produkt = "Laptop";
int ilosc = 5;
Console.WriteLine($"{produkt,-20} {ilosc,5}"); // Laptop 5
// -20 = wyrównanie do lewej, 20 znaków
// 5 = wyrównanie do prawej, 5 znaków
Klasy (na razie podstawy)
Klasy poznamy szczegółowo w kolejnych wpisach. Na razie podstawy:
// Definicja klasy
class Osoba
{
public string Imie { get; set; }
public string Nazwisko { get; set; }
public int Wiek { get; set; }
}
// Utworzenie obiektu - stary sposób
Osoba osoba1 = new Osoba();
osoba1.Imie = "Jan";
osoba1.Nazwisko = "Kowalski";
osoba1.Wiek = 30;
// Utworzenie obiektu - object initializer (C# 3+)
var osoba2 = new Osoba
{
Imie = "Anna",
Nazwisko = "Nowak",
Wiek = 25
};
// Utworzenie obiektu - target-typed new (C# 9+)
Osoba osoba3 = new() { Imie = "Piotr", Nazwisko = "Wiśniewski", Wiek = 35 };
// Nie musisz powtarzać "Osoba" - kompilator wie z typu zmiennej!
🔍 Target-typed new - co to?
W C# 9+ możesz napisać new() zamiast new Osoba() gdy kompilator wie jaki typ tworzysz.
// Stary sposób
Osoba osoba = new Osoba();
List<string> lista = new List<string>();
// Nowy sposób (target-typed new)
Osoba osoba = new(); // kompilator wie że to Osoba
List<string> lista = new(); // kompilator wie że to List<string>
Krócej i czytelniej! Szczególnie przy długich nazwach generycznych. ✅
Tablice
// Utworzenie tablicy - stary sposób
int[] liczby1 = new int[5]; // tablica 5 elementów (wszystkie = 0)
liczby1[0] = 10;
liczby1[1] = 20;
// Utworzenie tablicy z wartościami - array initializer
int[] liczby2 = new int[] { 10, 20, 30, 40, 50 };
// Krótsza składnia (kompilator wywnioskuje typ)
int[] liczby3 = { 10, 20, 30, 40, 50 };
// Target-typed new
int[] liczby4 = new[] { 10, 20, 30, 40, 50 };
// Dostęp do elementów
Console.WriteLine(liczby3[0]); // 10
Console.WriteLine(liczby3[4]); // 50
// Długość tablicy
Console.WriteLine(liczby3.Length); // 5
// Iteracja
foreach (int liczba in liczby3)
{
Console.WriteLine(liczba);
}
var - type inference (wnioskowanie typów)
Kompilator może wywnioskować typ!
// Zamiast pisać typ explicite:
int liczba = 42;
string imie = "Jan";
List<string> lista = new List<string>();
// Możesz użyć var - kompilator wywnioskuje typ
var liczba2 = 42; // int
var imie2 = "Jan"; // string
var lista2 = new List<string>(); // List<string>
🔍 Czy var to typ dynamiczny?
NIE! C# jest NADAL statically-typed. var to tylko skrót:
var x = 5; // kompilator: "x jest typu int"
x = "tekst"; // ❌ BŁĄD! x jest int, nie możesz przypisać string
// To jest IDENTYCZNE:
var a = 10;
int a = 10;
var to compile-time feature. W skompilowanym kodzie typ jest konkretny (int, string, etc).
Kiedy używać var?
✅ Używaj var gdy typ jest oczywisty
var imie = "Jan"; // oczywiste że string
var liczba = 42; // oczywiste że int
var osoba = new Osoba(); // oczywiste
var lista = new List<string>(); // oczywiste
// Szczególnie przy długich typach generycznych
var dictionary = new Dictionary<string, List<int>>();
// vs
Dictionary<string, List<int>> dictionary = new Dictionary<string, List<int>>();
// var wygrywa! 🏆
❌ NIE używaj var gdy typ nie jest oczywisty
var wynik = GetData();
// ❌ Co to jest? string? int? List?
// Lepiej:
UserData wynik = GetData();
// ✅ Wiadomo że to UserData
var x = Calculate(10, 20);
// ❌ Co zwraca? int? double?
// Lepiej:
double x = Calculate(10, 20);
// ✅ Jasne
💡 Moja rekomendacja
Używaj var w 90% przypadków! Czytelność > verbose kod.
Większość programistów C# (włączając mnie) używa var prawie wszędzie. W Visual Studio możesz najechać myszką na var i zobaczyć dokładny typ. 👍
Init-only properties - C# 9+
Problem: chcemy immutable objects
// Stary sposób - properties z set
class Osoba
{
public string Imie { get; set; }
public string Nazwisko { get; set; }
}
var osoba = new Osoba { Imie = "Jan", Nazwisko = "Kowalski" };
osoba.Imie = "Anna"; // ❌ Można zmienić po utworzeniu!
// Czasami tego NIE chcemy!
Rozwiązanie: init accessor
// Nowoczesny sposób - init zamiast set
class Osoba
{
public string Imie { get; init; } // init zamiast set!
public string Nazwisko { get; init; }
}
var osoba = new Osoba { Imie = "Jan", Nazwisko = "Kowalski" }; // ✅ OK podczas tworzenia
osoba.Imie = "Anna"; // ❌ BŁĄD! Nie można zmienić po utworzeniu
🔍 get; init; vs get; set;
get; set; - można czytać i zmieniać zawsze
get; init; - można czytać zawsze, ale zmienić TYLKO podczas inicjalizacji obiektu
get; - można tylko czytać (wymaga ustawienia w konstruktorze)
💡 Kiedy używać init?
Używaj init gdy wartość nie powinna się zmieniać po utworzeniu obiektu:
class Zamowienie
{
public int Id { get; init; } // ID nie zmienia się po utworzeniu
public DateTime DataUtworzenia { get; init; } // Data utworzenia stała
public string NumerZamowienia { get; init; } // Numer stały
public string Status { get; set; } // Status MOŻE się zmieniać
public decimal Kwota { get; set; } // Kwota MOŻE się zmieniać
}
Required members - C# 11+
Problem: nie wszystkie properties muszą być wypełnione
class Uzytkownik
{
public string Imie { get; set; }
public string Email { get; set; }
}
// Kompilator pozwala na to:
var user = new Uzytkownik(); // ❌ Brak Imie i Email!
// W runtime może być NullReferenceException...
Rozwiązanie: required keyword
class Uzytkownik
{
public required string Imie { get; set; } // WYMAGANE!
public required string Email { get; set; } // WYMAGANE!
public string? Telefon { get; set; } // opcjonalne (nullable)
}
// Teraz kompilator wymusza:
var user1 = new Uzytkownik();
// ❌ BŁĄD: Required member 'Imie' must be set
// ❌ BŁĄD: Required member 'Email' must be set
// ✅ Poprawnie:
var user2 = new Uzytkownik
{
Imie = "Jan", // wymagane
Email = "jan@example.com" // wymagane
// Telefon opcjonalne - nie musimy ustawić
};
🔍 required vs konstruktor
Dwa sposoby wymuszenia wartości:
// Sposób 1: required properties
class Osoba
{
public required string Imie { get; init; }
}
var o1 = new Osoba { Imie = "Jan" }; // object initializer
// Sposób 2: konstruktor
class Osoba2
{
public string Imie { get; init; }
public Osoba2(string imie) => Imie = imie;
}
var o2 = new Osoba2("Jan"); // przez konstruktor
required jest prostsze gdy masz dużo properties! Konstruktor lepszy gdy jest dodatkowa logika. 👍
Kombinacja: required + init
// Najczęstszy pattern w nowoczesnym C#:
class Produkt
{
public required string Nazwa { get; init; } // wymagane + immutable
public required decimal Cena { get; init; } // wymagane + immutable
public string? Opis { get; init; } // opcjonalne + immutable
public int IloscNaMagazynie { get; set; } // może się zmieniać
}
var produkt = new Produkt
{
Nazwa = "Laptop", // musi być (required)
Cena = 2999.99m // musi być (required)
// Opis opcjonalny - może pominąć
};
// produkt.Nazwa = "Desktop"; // ❌ BŁĄD - init nie pozwala
produkt.IloscNaMagazynie = 10; // ✅ OK - ma set
Konwersja między typami
Implicit conversion - automatyczna
// C# automatycznie konwertuje gdy nie ma utraty danych
int malaLiczba = 100;
long duzaLiczba = malaLiczba; // ✅ OK - int mieści się w long
float f = 3.14f;
double d = f; // ✅ OK - float mieści się w double
byte b = 10;
int i = b; // ✅ OK
Explicit conversion - cast
// Gdy może być utrata danych, musisz użyć cast
double d = 3.14159;
int i = (int)d; // 3 (ucięcie części dziesiętnej!)
Console.WriteLine(i); // 3
long duzaLiczba = 1000;
int malaLiczba = (int)duzaLiczba; // ⚠️ Może się nie zmieścić!
Parse i TryParse - konwersja ze string
// Parse - rzuca wyjątek jeśli nie da się skonwertować
string tekst1 = "123";
int liczba1 = int.Parse(tekst1); // ✅ 123
string tekst2 = "abc";
int liczba2 = int.Parse(tekst2); // ❌ FormatException!
// TryParse - bezpieczniejsze, zwraca bool
string tekst3 = "456";
if (int.TryParse(tekst3, out int liczba3))
{
Console.WriteLine($"Sukces: {liczba3}"); // ✅ Sukces: 456
}
else
{
Console.WriteLine("Nie udało się skonwertować");
}
string tekst4 = "xyz";
if (int.TryParse(tekst4, out int liczba4))
{
Console.WriteLine($"Sukces: {liczba4}");
}
else
{
Console.WriteLine("Nie udało się"); // ✅ To się wykona
}
❌ Używanie Parse bez walidacji
Console.Write("Podaj wiek: ");
string input = Console.ReadLine();
int wiek = int.Parse(input);
// ❌ Jeśli user wpisze "abc" - crash!
✅ Używanie TryParse
Console.Write("Podaj wiek: ");
string input = Console.ReadLine();
if (int.TryParse(input, out int wiek))
{
Console.WriteLine($"Wiek: {wiek}");
}
else
{
Console.WriteLine("Niepoprawny wiek!");
}
// ✅ Bezpieczne!
Podsumowanie
To był intensywny wpis! Poznałeś fundament C# - system typów. Nauczyłeś się:
✅ Value types vs Reference types – stack vs heap, kopiowanie przez wartość vs referencję
✅ Podstawowe typy – int, double, decimal, bool, char, DateTime
✅ Nullable value types – int?, DateTime?, operator ??