Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Udostępnij Udostępnij Kontakt
Wprowadzenie

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!

// ❌ ŹLE
double saldo = 100.10;
double oplata = 0.10;
Console.WriteLine(saldo - oplata);  // 99.99999999999999 😱

// ✅ DOBRZE
decimal saldo = 100.10m;
decimal oplata = 0.10m;
Console.WriteLine(saldo - oplata);  // 100.00 ✅

Inne typy wartościowe

// 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:

string imie = null;
Console.WriteLine(imie.ToUpper());  // 💥 NullReferenceException!
🔍 Dlaczego to był taki problem?

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:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>  👈 To włącza nullable reference types!
  </PropertyGroup>
</Project>
⚠️ WAŻNE dla starszych projektów

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!

Lepiej: Użyj null check (if (x != null)) lub null-conditional (x?.Method())

Typy referencyjne - string, class, array

String - tekst

// 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 ??
  • Nullable reference types – string?, ochrona przed NullReferenceException (C# 8+)
  • String – interpolation, raw literals, formatowanie
  • Target-typed new – new() zamiast new Osoba() (C# 9+)
  • var – type inference, kiedy używać
  • Init-only properties – immutable objects (C# 9+)
  • Required members – wymuszenie wypełnienia (C# 11+)
  • Konwersja typów – implicit, explicit, Parse, TryParse

W kolejnym wpisie nauczymy się o zmiennych, stałych i nowym field keyword z C# 14!

Zadanie dla Ciebie 🎯

Stwórz program "Kalkulator BMI":

  1. Poproś użytkownika o wzrost (w metrach, np. 1.75)
  2. Poproś o wagę (w kg, np. 70)
  3. Oblicz BMI: waga / (wzrost * wzrost)
  4. Wyświetl wynik z dokładnością do 2 miejsc po przecinku
  5. Użyj TryParse aby bezpiecznie konwertować input
  6. Jeśli użytkownik wpisze błędne dane, wyświetl komunikat
// Przykładowe działanie:
Podaj wzrost (w metrach): 1.75
Podaj wagę (w kg): 70
Twoje BMI: 22.86
🎯 BONUS: System rejestracji użytkownika

Stwórz klasę Uzytkownik z nullable reference types i required members:

Wymagania:

  1. Klasa Uzytkownik z properties:
    • required string Email - wymagany, immutable (init)
    • required string Haslo - wymagany, immutable
    • string? Imie - opcjonalny, immutable
    • string? Nazwisko - opcjonalny, immutable
    • DateTime DataRejestracji - auto-ustawiony na DateTime.Now, immutable
    • bool CzyAktywny - może się zmieniać (set), domyślnie true
  2. Program który:
    • Pyta o email (TryParse sprawdź czy zawiera @)
    • Pyta o hasło (minimum 6 znaków)
    • Pyta opcjonalnie o imię i nazwisko
    • Tworzy obiekt Uzytkownik
    • Wyświetla podsumowanie

To ćwiczenie połączy wszystko czego się nauczyłeś: nullable reference types, required members, init properties, string validation! 🚀