Przejdź do treści
School IT / Przestrzeń do nauki
← C# od podstawLekcja 34 z 58

Null i walidacja danych

Użytkownik może wpisać litery zamiast liczby, nacisnąć sam Enter albo podać wiek −5. Nauczymy się odróżniać brak wartości od zera i sprawdzać dane na trzech poziomach, zanim dojdą do obliczeń.

C# null string? ?? ?. TryParse walidacja 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Odróżnisz null od zera, pustego napisu i napisu ze spacją.
  • Odczytasz z typu zmiennej, czy trzeba sprawdzać null, i rozpoznasz ostrzeżenia CS8600, CS8602, CS8618 i CS8625.
  • Użyjesz int? wraz z HasValue, Value i GetValueOrDefault.
  • Zastosujesz is null, ??, ??=, ?. i ?[] oraz wyjaśnisz, dlaczego ! nie jest naprawą.
  • Sprawdzisz dane na trzech poziomach: obecność, format i sens.
  • Napiszesz metodę wczytującą z pętlą powtarzającą pytanie do poprawnej odpowiedzi.
  • Zapiszesz sprawdzenia klauzulami strażniczymi zamiast zagnieżdżonych instrukcji if.
  • Napiszesz metodę zwracającą bool oraz dane i powód odmowy przez out.
  • Rozstrzygniesz, kiedy wystarcza warunek, a kiedy konieczny jest blok try.

Przygotowanie: lekcje 01–33. Przewidywany czas: 90 min z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.

TEORIA

Trzy wiersze i pięć sposobów na kłopot

Trzywierszowy program, jakich pisałeś już kilkadziesiąt:

tak nie należy pisać
Console.Write("Podaj wiek: ");
string wpis = Console.ReadLine();          // OSTRZEZENIE CS8600
int wiek = int.Parse(wpis);
Console.WriteLine($"Za rok bedziesz mial {wiek + 1} lat");

Działa, dopóki użytkownik jest grzeczny. Sprawdźmy cztery zachowania, których na pewno się doczekasz:

Co wpisze użytkownikCo się stanie
17działa poprawnie
sam EnterArgumentNullException — program przerwany
kotFormatException — program przerwany
17 (ze spacjami)działa; Parse sam usuwa spacje z brzegów
-5działa i wypisuje bzdurę: „za rok będziesz miał -4 lata”
99999999999999OverflowException — liczba nie mieści się w int

Dwa różne rodzaje kłopotu naraz. Trzy przypadki przerywają program, a jeden — najgroźniejszy — przechodzi i daje bezsensowny wynik. Wiek −5 przejdzie każde sprawdzenie typu, bo to poprawna liczba całkowita. Tylko Ty wiesz, że nie jest poprawnym wiekiem.

Do tego kompilator podkreślił drugi wiersz ostrzeżeniem CS8600. Nie bez powodu: Console.ReadLine() może zwrócić brak wartości, a my wpisaliśmy tę wartość do zmiennej, która braku nie przewiduje.

Dwa tematy, jedna lekcja

Ta lekcja odpowiada na dwa pytania. Pierwsze: czym jest brak wartości i jak się z nim obchodzić, żeby nie dostać NullReferenceException. Drugie: jak sprawdzać dane, żeby złe nigdy nie doszły do obliczeń. Oba dotyczą tej samej sprawy — granicy między programem a światem zewnętrznym.

TEORIA

Brak wartości, zero i pusty napis

null znaczy „ta zmienna nie wskazuje na nic”. To nie to samo co zero i nie to samo co pusty napis, choć w rozmowie wszystkie trzy określa się słowem „nic”.

WartośćCo to znaczy po ludzkuLengthPrzykład z życia
nullnie ma wartości, nie ma na co patrzećwyjątek!pole „telefon” nigdy nie wypełnione
""napis istnieje, ale jest pusty0użytkownik nacisnął Enter i wyczyściliśmy spacje
" "napis ze spacją — nie jest pusty1użytkownik nacisnął spację i Enter
"0"napis z jednym znakiem1wpisane zero
0liczba zero — pełnoprawna wartość—zdobyto zero punktów

NullReferenceException

To najczęstszy wyjątek w programach początkujących i brzmi zawsze tak samo: „Object reference not set to an instance of an object”. Znaczy dokładnie jedno: próbowałeś czegoś zrobić ze zmienną, która jest null — odczytać jej pole, wywołać metodę, wziąć długość. Nie ma na czym pracować, więc program się przerywa.

Znak zapytania przy typie

C# pozwala zapisać w typie, czy brak wartości jest dopuszczalny. Robi to znak zapytania:

Program.cs
string imie = "Kasia";        // MUSI miec wartosc
string? telefon = null;       // MOZE byc puste - i jest

int wiek = 17;                // MUSI miec liczbe
int? ocena = null;            // MOZE nie miec liczby

// imie = null;               // OSTRZEZENIE CS8625 - przypisanie null do typu bez ?

To jedna z najbardziej użytecznych rzeczy w nowoczesnym C#. Sam typ zmiennej mówi Ci, czy musisz sprawdzać null:

ZapisZnaczenieCzy sprawdzać null
string imiezawsze ma napisnie
string? telefonmoże nie mieć napisutak, zawsze
int wiekzawsze ma liczbęnie da się — liczba nie bywa null
int? ocenamoże nie mieć liczbytak
Uczen uzawsze wskazuje obiektnie
Uczen? umoże nie wskazywać obiektutak

Ostrzeżenia, nie błędy

Kompilator z włączoną opcją Nullable — a taką ma domyślnie każdy nowy projekt .NET 8 — ostrzega, gdy traktujesz wartość mogącą być null jak pewną. Ostrzeżenie nie zatrzymuje kompilacji, więc bardzo łatwo je przegapić. Traktuj żółte podkreślenia jako listę miejsc, w których program może się wywrócić. Cztery, które zobaczysz najczęściej:

  • CS8600 — przypisanie wartości mogącej być null do typu bez ?
  • CS8602 — użycie zmiennej, która może być null, bez sprawdzenia
  • CS8618 — pole obiektu nie dostało wartości w konstruktorze
  • CS8625 — jawne przypisanie null tam, gdzie typ tego nie przewiduje

int? od środka

int? to liczba albo informacja, że liczby nie ma. Sprawdza się to i odczytuje trzema sposobami:

Program.cs
int? ocena = null;

Console.WriteLine(ocena.HasValue);            // False - czy jest wartosc
Console.WriteLine(ocena.GetValueOrDefault()); // 0 - wartosc albo zero
Console.WriteLine(ocena ?? -1);               // -1 - wartosc albo nasza zastepcza

// Console.WriteLine(ocena.Value);            // WYJATEK - nie ma czego odczytac

ocena = 5;

Console.WriteLine(ocena.HasValue);            // True
Console.WriteLine(ocena.Value);               // 5 - teraz wolno
Console.WriteLine(ocena + 1);                 // 6

int pewna = ocena.Value;                      // przepisanie do zwyklego int

Po co komu int?

Bo czasem zero jest prawdziwą odpowiedzią. „Ile punktów zdobył Marek?” — 0 znaczy „startował i nie zdobył nic”, a null znaczy „nie startował”. Gdybyś oba przypadki zapisał zerem, średnia klasy byłaby policzona źle. Ta sama różnica dotyczy ocen, temperatur i wyników pomiarów.

TEORIA

Pięć krótkich zapisów

C# ma pięć krótkich zapisów do obchodzenia się z brakiem wartości. Wszystkie oszczędzają miejsce i wszystkie warto znać, bo spotkasz je w każdym cudzym kodzie.

1. Sprawdzenie: is null i is not null

Program.cs
string? telefon = null;

if (telefon is null)
{
    Console.WriteLine("brak numeru");
}

if (telefon is not null)
{
    Console.WriteLine($"numer ma {telefon.Length} znakow");   // tu bez ostrzezenia
}

Wewnątrz drugiego if kompilator wie, że telefon nie jest null, więc ostrzeżenie CS8602 znika samo. Zapis == null też działa, ale is null jest pewniejszy: nie da się go podmienić własnym operatorem (lekcja 50) i nie pomylisz go z pojedynczym znakiem równości.

2. Wartość zastępcza: ??

Program.cs
string? wpisane = null;

string doWypisania = wpisane ?? "nie podano";
Console.WriteLine(doWypisania);              // nie podano

int? punkty = null;
int doObliczen = punkty ?? 0;
Console.WriteLine(doObliczen);               // 0

// UWAGA: ?? reaguje TYLKO na null
string? puste = "";
Console.WriteLine(puste ?? "zastepcze");     // wypisze pusty wiersz, NIE "zastepcze"!

3. Przypisanie, jeśli puste: ??=

Program.cs
string? nazwa = null;

nazwa ??= "bez nazwy";        // przypisz, ale tylko jesli jest null
Console.WriteLine(nazwa);     // bez nazwy

nazwa ??= "inna";             // nie zadziala - nazwa juz ma wartosc
Console.WriteLine(nazwa);     // bez nazwy

4. Bezpieczny dostęp: ?. i ?[]

Program.cs
string? tekst = null;

// int dlugosc = tekst.Length;              // WYJATEK NullReferenceException
int? dlugosc = tekst?.Length;               // null - bez wyjatku
Console.WriteLine(dlugosc ?? -1);           // -1

int[]? tablica = null;
int? pierwszy = tablica?[0];                // null - bez wyjatku

// lancuch: jesli ktorykolwiek element jest null, calosc daje null
string? klasa = uczen?.Wychowawca?.Nazwisko;
Console.WriteLine(klasa ?? "brak danych");

Dlaczego wynik ?. ma znak zapytania

tekst?.Length zwraca int?, nie int — bo gdy tekst jest null, nie ma żadnej długości do zwrócenia. Dlatego zapis int dlugosc = tekst?.Length; się nie skompiluje (CS0266): trzeba albo int?, albo dopisać ?? 0.

5. „Wiem, że nie jest null”: !

tak nie należy pisać
string? wpis = Console.ReadLine();

int dlugosc = wpis!.Length;      // ostrzezenie znika... ale wyjatek zostaje!

To nie jest naprawa, to zaklejenie taśmą

Wykrzyknik po zmiennej (operator „null-forgiving”) mówi kompilatorowi: „nie ostrzegaj mnie tutaj”. Nie sprawdza niczego i niczego nie naprawia — zmienna nadal może być null, a program nadal przerwie się wyjątkiem. Zniknie tylko żółte podkreślenie, czyli jedyne ostrzeżenie, jakie miałeś. Używaj go wyłącznie wtedy, gdy potrafisz udowodnić, że wartość istnieje, a kompilator nie ma jak tego wiedzieć. W zadaniach szkolnych: praktycznie nigdy.

ZapisCo robiWynik dla null
x is nullsprawdza brak wartościtrue
x is not nullsprawdza obecność wartościfalse
x ?? zwartość albo zastępczaz
x ??= zprzypisz, jeśli pustex staje się z
x?.Metoda()wywołaj, jeśli nie pustenull, bez wyjątku
x?[0]weź element, jeśli nie pustenull, bez wyjątku
x!wycisza ostrzeżeniewyjątek — nic nie sprawdza
TEORIA

Obecność, format, sens

Sprawdzanie null to dopiero pierwsza warstwa. Dane od użytkownika trzeba przepuścić przez trzy sita, w tej kolejności — bo każde następne ma sens tylko wtedy, gdy poprzednie zostało przejdzie.

PoziomPytanieNarzędziePrzykład odrzucenia
1. Obecnośćczy coś w ogóle podano?string.IsNullOrWhiteSpacesam Enter, jedna spacja
2. Formatczy to jest liczba / data / kod?int.TryParse, double.TryParsekot, 17a, 1 7
3. Sensczy ta wartość ma sens w tym zadaniu?zwykły if z zakresemwiek −5, ocena 9, czas 0 sekund

Trzeci poziom jest tym, o którym się zapomina

Pierwsze dwa poziomy sprawdzają, czy dana jest technicznie poprawna. Trzeciego nie sprawdzi za Ciebie żadna biblioteka, bo tylko Ty wiesz, że ocena może być od 1 do 6, a wzrost ucznia nie wynosi 4 metrów. Program bez trzeciego poziomu nie przerywa się — po prostu liczy bzdury i nikt tego nie zauważy do rozliczenia.

Poziom 1 i 2: wzorzec, który zapamiętasz na zawsze

Program.cs
Console.Write("Podaj wiek: ");
string? wpis = Console.ReadLine();

if (string.IsNullOrWhiteSpace(wpis))
{
    Console.WriteLine("Nic nie podano.");
    return;
}

if (!int.TryParse(wpis, out int wiek))
{
    Console.WriteLine("To nie jest liczba calkowita.");
    return;
}

Console.WriteLine($"Za rok bedziesz mial {wiek + 1} lat");

Dlaczego po IsNullOrWhiteSpace nie ma już ostrzeżenia

Za if z return kompilator wie, że wpis nie jest null — metoda IsNullOrWhiteSpace jest tak oznaczona w bibliotece. Dlatego wpis.Trim() w dalszej części nie wywoła CS8602. Sprawdzenie, które naprawdę coś sprawdza, wycisza ostrzeżenie i zabezpiecza program — inaczej niż wykrzyknik.

IsNullOrEmpty czy IsNullOrWhiteSpace

WejścieIsNullOrEmptyIsNullOrWhiteSpace
nulltruetrue
""truetrue
" "false ✘true ✔
"\t\n"false ✘true ✔
"Kasia"falsefalse

Do danych od użytkownika używaj zawsze IsNullOrWhiteSpace. Przypadkowa spacja przed Enterem to najzwyklejsza rzecz na świecie, a IsNullOrEmpty ją przepuści.

Porządkowanie tekstu przed sprawdzeniem

Program.cs
string czysty = wpis.Trim();                 // usun spacje z brzegow
string bezWielkosci = czysty.ToUpper();      // gdy wielkosc liter nie ma znaczenia

// porownanie odporne na wielkosc liter, bez tworzenia kopii:
if (czysty.Equals("tak", StringComparison.OrdinalIgnoreCase))
{
    Console.WriteLine("zgoda");
}

Liczby rzeczywiste — jedna pułapka więcej

Program.cs
Console.Write("Podaj czas w sekundach: ");
string? wpis = Console.ReadLine();

if (!double.TryParse(wpis, out double czas))
{
    Console.WriteLine("To nie jest liczba.");
    return;
}

if (!double.IsFinite(czas))          // odrzuca NaN, Infinity i -Infinity
{
    Console.WriteLine("Ta wartosc nie jest zwykla liczba.");
    return;
}

if (czas <= 0)
{
    Console.WriteLine("Czas musi byc dodatni.");
    return;
}

Kropka czy przecinek

double.TryParse bez dodatkowych argumentów używa ustawień systemu. Na polskim Windowsie separatorem jest przecinek, więc 62.5 zostanie odrzucone, a 62,5 przyjęte. Przy danych z pliku albo z internetu, gdzie separatorem jest kropka, trzeba podać kulturę jawnie — do tego wrócimy w lekcji 36.

Pytaj, aż dostaniesz poprawną odpowiedź

W programie interaktywnym return po pierwszym błędzie jest niegrzeczny — użytkownik musi uruchamiać program od nowa. Lepiej pytać w pętli:

Program.cs
static int WczytajInt(string zacheta, int min, int max)
{
    while (true)
    {
        Console.Write(zacheta);
        string? wpis = Console.ReadLine();

        if (!int.TryParse(wpis, out int liczba))
        {
            Console.WriteLine("  To nie jest liczba calkowita.");
            continue;
        }

        if (liczba < min || liczba > max)
        {
            Console.WriteLine($"  Podaj liczbe od {min} do {max}.");
            continue;
        }

        return liczba;          // wyjscie z petli tylko przez poprawna wartosc
    }
}

while (true) z return w środku wygląda niepokojąco, ale to najczytelniejszy zapis tego wzorca: z pętli wychodzi się jedynym wyjściem — przez poprawną wartość. Napisana raz, taka metoda obsługuje wszystkie liczby w programie, a komunikaty dla użytkownika są w jednym miejscu.

TEORIA

Granica programu i strażnicy

Gdzie w programie ma stać sprawdzanie? Odpowiedź jest jedna i upraszcza bardzo dużo: na granicy programu.

Skąd pochodzą daneZaufanieCo robić
konsola — użytkownikzerosprawdzić wszystkie trzy poziomy
plik, sieć, bazazerosprawdzić wszystkie trzy poziomy
argumenty programuzerosprawdzić wszystkie trzy poziomy
zmienna policzona w programiepełnenie sprawdzać — już sprawdziłeś na wejściu
pole obiektu, który sam pilnuje zasadpełnenie sprawdzać — o tym jest lekcja 42

Zasada „brudne na zewnątrz, czyste w środku”

Wszystko, co przychodzi z zewnątrz, jest brudne i sprawdzasz to raz, na samym wejściu. Od tego momentu w programie krążą już tylko dane czyste, więc nie musisz sprawdzać ich po raz dziesiąty w każdej metodzie. Program, w którym każda metoda sprawdza wszystko, jest nieczytelny i nadal ma luki — bo zawsze znajdzie się jedna, która zapomniała.

Klauzule strażnicze zamiast zagnieżdżeń

Porównaj dwa zapisy tej samej logiki:

trudne do czytania
// zagniezdzone - kazde sprawdzenie dodaje poziom wciecia
static void Zapisz(string? imie, string? klasa, int wiek)
{
    if (!string.IsNullOrWhiteSpace(imie))
    {
        if (!string.IsNullOrWhiteSpace(klasa))
        {
            if (wiek >= 14 && wiek <= 21)
            {
                Console.WriteLine($"zapisano {imie} z {klasa}");
            }
            else
            {
                Console.WriteLine("zly wiek");
            }
        }
        else
        {
            Console.WriteLine("brak klasy");
        }
    }
    else
    {
        Console.WriteLine("brak imienia");
    }
}
czytelne — ten sam efekt
// klauzule straznicze - kazdy zly przypadek wychodzi od razu
static void Zapisz(string? imie, string? klasa, int wiek)
{
    if (string.IsNullOrWhiteSpace(imie))
    {
        Console.WriteLine("brak imienia");
        return;
    }

    if (string.IsNullOrWhiteSpace(klasa))
    {
        Console.WriteLine("brak klasy");
        return;
    }

    if (wiek < 14 || wiek > 21)
    {
        Console.WriteLine("zly wiek");
        return;
    }

    Console.WriteLine($"zapisano {imie} z {klasa}");     // wlasciwa praca, bez wciec
}

Drugi zapis nazywa się klauzulami strażniczymi: najpierw odsiewasz wszystkie złe przypadki, każdy z własnym komunikatem i własnym return, a właściwa praca zostaje na końcu, na jednym poziomie wcięcia. Przy pięciu warunkach różnica między tymi dwoma zapisami jest ogromna.

Metoda, która zwraca wynik i powód odmowy

Gdy sprawdzanie ma być użyte w kilku miejscach, opłaca się je wydzielić. Wzorzec jest ten sam co w TryParse — bool jako wynik i dane przez out (lekcja 20):

Program.cs
static bool SprawdzOcene(string? wpis, out int ocena, out string blad)
{
    ocena = 0;
    blad = "";

    if (string.IsNullOrWhiteSpace(wpis))
    {
        blad = "nie podano oceny";
        return false;
    }

    if (!int.TryParse(wpis.Trim(), out ocena))
    {
        blad = $"\"{wpis.Trim()}\" nie jest liczba";
        return false;
    }

    if (ocena < 1 || ocena > 6)
    {
        blad = $"ocena {ocena} jest poza zakresem 1-6";
        return false;
    }

    return true;
}
Program.cs
string[] wpisy = { "5", "kot", "9", "  4 ", "", "1" };

foreach (string w in wpisy)
{
    if (SprawdzOcene(w, out int ocena, out string blad))
    {
        Console.WriteLine($"OK       {ocena}");
    }
    else
    {
        Console.WriteLine($"ODRZUCONE  {blad}");
    }
}
wynik w konsoli
OK       5
ODRZUCONE  "kot" nie jest liczba
ODRZUCONE  ocena 9 jest poza zakresem 1-6
OK       4
ODRZUCONE  nie podano oceny
OK       1

Taka metoda przyda się przy wczytywaniu z konsoli, z pliku i z tablicy testowej — a komunikat o błędzie jest zawsze ten sam i zawsze konkretny. „Błędne dane” nie pomaga nikomu; „ocena 9 jest poza zakresem 1-6” pomaga od razu.

TEORIA

Zapowiedź lekcji 35

Walidacja załatwia wszystko, co możesz sprawdzić z góry. Ale są sytuacje, których sprawdzić nie da się w żaden sposób — i wtedy potrzebne jest inne narzędzie.

SytuacjaDa się sprawdzić warunkiem?Czym się zabezpieczyć
użytkownik wpisał tekst zamiast liczbytakTryParse
ocena poza zakresem 1–6takzwykły if
dzielenie przez zerotakif (dzielnik != 0)
indeks poza tablicątakif (i < tab.Length)
pusta kolekcjatakif (lista.Count > 0)
plik zniknął między sprawdzeniem a odczytemniewyjątek
brak uprawnień do plikuniewyjątek
dysk pełny w trakcie zapisuniewyjątek
zerwane połączenie siecioweniewyjątek

Reguła wyboru

Jeśli możesz sprawdzić warunkiem — sprawdź warunkiem. Wyjątki zostaw do rzeczy, o których nie da się dowiedzieć wcześniej niż w chwili, gdy zawiodą. Program, w którym zwykłe sprawdzenie zakresu zastąpiono blokiem try, jest wolniejszy i o wiele trudniejszy do czytania.

Zabezpieczenie w najprostszej postaci wygląda tak:

Program.cs
try
{
    string tresc = File.ReadAllText("wyniki.txt");
    Console.WriteLine(tresc);
}
catch (FileNotFoundException)
{
    Console.WriteLine("Nie ma pliku wyniki.txt - zaczynamy od pustej listy.");
}

Console.WriteLine("Program dziala dalej.");

try obejmuje operację, która może zawieść. catch przechwytuje konkretny rodzaj problemu i decyduje, co zrobić. Program nie przerywa się — wykonanie przechodzi za catch i leci dalej.

Dwie rzeczy, których nie rób

Nie łap wszystkiego. catch (Exception) { } ukrywa każdy błąd, także literówkę w Twoim kodzie — program „działa”, a wyniki są złe i nie wiesz dlaczego. Łap ten rodzaj wyjątku, na który naprawdę masz plan.

Nie licz na powrót do try. Po obsłużeniu wyjątku program nie wraca do przerwanej instrukcji. Jeśli operacja ma być powtórzona, trzeba to napisać samemu — pętlą.

To wszystko, co potrzebne w tej lekcji. Zgłaszanie własnych błędów instrukcją throw, blok finally, kolejność kilku bloków catch, filtry when i własne typy wyjątków to temat lekcji 35, do której przechodzimy zaraz po tej.

PRZYKŁAD

Formularz zapisów na turniej

Formularz zapisów na turniej. Pięć pól, każde z innym zestawem sprawdzeń, jedno pole opcjonalne, na końcu sprawdzanie listy ocen i jeden przypadek, którego warunkiem nie da się załatwić.

Program.cs — pełny plik
// Program.cs - pelny plik
Console.WriteLine("=== zapisy na turniej ===");

string imie    = WczytajTekst("Imie i nazwisko: ", 3, 40);
string klasa   = WczytajKlase("Klasa (np. 3TP): ");
int wiek       = WczytajInt("Wiek: ", 14, 21);
double czas    = WczytajCzas("Czas z eliminacji w sekundach: ", 10.0, 600.0);
string? tel    = WczytajTelefon("Telefon (Enter = pomin): ");

Console.WriteLine();
Console.WriteLine("=== zapisano ===");
Console.WriteLine($"Zawodnik:  {imie}");
Console.WriteLine($"Klasa:     {klasa}");
Console.WriteLine($"Wiek:      {wiek}");
Console.WriteLine($"Czas:      {czas:F2} s");
Console.WriteLine($"Telefon:   {tel ?? "nie podano"}");
Console.WriteLine($"Dlugosc nr: {tel?.Length ?? 0}");

Console.WriteLine();
Console.WriteLine("=== sprawdzanie listy ocen ===");
string[] wpisy = { "5", "kot", "9", "  4 ", "", "1" };

int suma = 0;
int przyjete = 0;

foreach (string w in wpisy)
{
    if (SprawdzOcene(w, out int ocena, out string blad))
    {
        Console.WriteLine($"  OK         {ocena}");
        suma += ocena;
        przyjete++;
    }
    else
    {
        Console.WriteLine($"  ODRZUCONE  {blad}");
    }
}

if (przyjete == 0)
{
    Console.WriteLine("  Brak poprawnych ocen - nie licze sredniej.");
}
else
{
    Console.WriteLine($"  Przyjeto {przyjete} z {wpisy.Length}, srednia {(double)suma / przyjete:F2}");
}

Console.WriteLine();
Console.WriteLine("=== czego warunkiem nie sprawdzisz ===");
try
{
    string tresc = File.ReadAllText("nie-ma-takiego-pliku.txt");
    Console.WriteLine(tresc);
}
catch (FileNotFoundException)
{
    Console.WriteLine("  Pliku nie ma - pracuje na pustej liscie.");
}

Console.WriteLine("  Program dziala dalej.");


static string WczytajTekst(string zacheta, int min, int max)
{
    while (true)
    {
        Console.Write(zacheta);
        string? wpis = Console.ReadLine();

        if (string.IsNullOrWhiteSpace(wpis))
        {
            Console.WriteLine("  To pole nie moze byc puste.");
            continue;
        }

        string czysty = wpis.Trim();

        if (czysty.Length < min || czysty.Length > max)
        {
            Console.WriteLine($"  Podaj od {min} do {max} znakow (podano {czysty.Length}).");
            continue;
        }

        return czysty;
    }
}

static string WczytajKlase(string zacheta)
{
    while (true)
    {
        Console.Write(zacheta);
        string? wpis = Console.ReadLine();

        if (string.IsNullOrWhiteSpace(wpis))
        {
            Console.WriteLine("  Podaj klase.");
            continue;
        }

        string czysty = wpis.Trim().ToUpper();

        if (czysty.Length < 3 || czysty.Length > 4)
        {
            Console.WriteLine("  Klasa ma 3 lub 4 znaki, np. 3TP.");
            continue;
        }

        if (!char.IsDigit(czysty[0]))
        {
            Console.WriteLine("  Klasa zaczyna sie cyfra, np. 3TP.");
            continue;
        }

        return czysty;
    }
}

static int WczytajInt(string zacheta, int min, int max)
{
    while (true)
    {
        Console.Write(zacheta);
        string? wpis = Console.ReadLine();

        if (!int.TryParse(wpis, out int liczba))
        {
            Console.WriteLine("  To nie jest liczba calkowita.");
            continue;
        }

        if (liczba < min || liczba > max)
        {
            Console.WriteLine($"  Podaj liczbe od {min} do {max}.");
            continue;
        }

        return liczba;
    }
}

static double WczytajCzas(string zacheta, double min, double max)
{
    while (true)
    {
        Console.Write(zacheta);
        string? wpis = Console.ReadLine();

        if (!double.TryParse(wpis, out double liczba))
        {
            Console.WriteLine("  To nie jest liczba. Separatorem jest przecinek.");
            continue;
        }

        if (!double.IsFinite(liczba))
        {
            Console.WriteLine("  Ta wartosc nie jest zwykla liczba.");
            continue;
        }

        if (liczba < min || liczba > max)
        {
            Console.WriteLine($"  Podaj wartosc od {min:F1} do {max:F1}.");
            continue;
        }

        return liczba;
    }
}

static string? WczytajTelefon(string zacheta)
{
    Console.Write(zacheta);
    string? wpis = Console.ReadLine();

    if (string.IsNullOrWhiteSpace(wpis))
    {
        return null;                    // brak telefonu to poprawna sytuacja
    }

    string cyfry = "";

    foreach (char znak in wpis)
    {
        if (char.IsDigit(znak))
        {
            cyfry += znak;
        }
    }

    if (cyfry.Length != 9)
    {
        Console.WriteLine("  Numer pominiety - nie ma dziewieciu cyfr.");
        return null;
    }

    return cyfry;
}

static bool SprawdzOcene(string? wpis, out int ocena, out string blad)
{
    ocena = 0;
    blad = "";

    if (string.IsNullOrWhiteSpace(wpis))
    {
        blad = "nie podano oceny";
        return false;
    }

    string czysty = wpis.Trim();

    if (!int.TryParse(czysty, out ocena))
    {
        blad = $"\"{czysty}\" nie jest liczba";
        return false;
    }

    if (ocena < 1 || ocena > 6)
    {
        blad = $"ocena {ocena} jest poza zakresem 1-6";
        return false;
    }

    return true;
}
przebieg z celowo złymi odpowiedziami
=== zapisy na turniej ===
Imie i nazwisko:
  To pole nie moze byc puste.
Imie i nazwisko: Ka
  Podaj od 3 do 40 znakow (podano 2).
Imie i nazwisko: Kasia Nowak
Klasa (np. 3TP): TP3
  Klasa zaczyna sie cyfra, np. 3TP.
Klasa (np. 3TP): 3tp
Wiek: siedemnascie
  To nie jest liczba calkowita.
Wiek: 5
  Podaj liczbe od 14 do 21.
Wiek: 17
Czas z eliminacji w sekundach: 62.5
  To nie jest liczba. Separatorem jest przecinek.
Czas z eliminacji w sekundach: 62,5
Telefon (Enter = pomin):

=== zapisano ===
Zawodnik:  Kasia Nowak
Klasa:     3TP
Wiek:      17
Czas:      62,50 s
Telefon:   nie podano
Dlugosc nr: 0

=== sprawdzanie listy ocen ===
  OK         5
  ODRZUCONE  "kot" nie jest liczba
  ODRZUCONE  ocena 9 jest poza zakresem 1-6
  OK         4
  ODRZUCONE  nie podano oceny
  OK         1
  Przyjeto 3 z 6, srednia 3,33

=== czego warunkiem nie sprawdzisz ===
  Pliku nie ma - pracuje na pustej liscie.
  Program dziala dalej.

Co dzieje się po kolei

  • Każda metoda wczytująca ma pętlę while (true) z jednym wyjściem — przez return z poprawną wartością. Zły wpis kończy się komunikatem i continue, czyli kolejnym pytaniem.
  • WczytajTekst sprawdza dwa poziomy: obecność (IsNullOrWhiteSpace) i sens (długość). Zauważ, że długość liczy się po Trim() — inaczej trzy spacje przeszłyby jako trzy znaki.
  • WczytajKlase dokłada trzeci warunek i przy okazji poprawia dane: 3tp wchodzi, a zapisuje się jako 3TP. Sprawdzanie i normalizowanie w jednym miejscu to dobry nawyk.
  • WczytajInt nie sprawdza null osobno — int.TryParse przyjmuje string? i dla null po prostu zwraca false. Jedno sprawdzenie zamiast dwóch.
  • WczytajCzas odrzuca 62.5 z kropką, bo na polskich ustawieniach separatorem jest przecinek. Po TryParse stoi jeszcze IsFinite — bo tekst Infinity przeszedłby przez TryParse jako poprawna liczba.
  • WczytajTelefon zwraca string?, bo brak numeru jest poprawną odpowiedzią. Enter daje null bez żadnego komunikatu. Numer z myślnikami i spacjami zostaje wyczyszczony do samych cyfr i dopiero wtedy sprawdzony.
  • Wypisanie telefonu używa ??, a długości — ?.Length ?? 0. Bez tych dwóch operatorów oba wiersze wywróciłyby program na NullReferenceException.
  • W bloku ocen trzy wpisy przechodzą, trzy są odrzucane, a każdy z konkretnym powodem. Średnia liczy się tylko z przyjętych — dlatego 10 / 3 = 3,33, a nie 10 / 6.
  • Sprawdzenie przyjete == 0 nie jest ozdobą: bez niego pusta lista dałaby dzielenie przez zero.
  • Ostatni blok pokazuje granicę walidacji. Nie da się z góry sprawdzić, czy plik istnieje w chwili odczytu — pomiędzy sprawdzeniem a odczytem ktoś mógłby go usunąć. Dlatego tam jest try.

Sprawdź cztery rzeczy

  1. Zamień w WczytajTekst metodę IsNullOrWhiteSpace na IsNullOrEmpty. Teraz da się przejść dalej, wpisując trzy spacje — i zawodnik nazywa się „ ”.
  2. Usuń ?? "nie podano" z wypisywania telefonu. Program się skompiluje, a przy pustym numerze wypisze pusty wiersz — nie wyjątek, bo interpolacja radzi sobie z null. Potem usuń ? z tel?.Length — tu dostaniesz CS8602, a po uruchomieniu NullReferenceException.
  3. W SprawdzOcene usuń ostatnie sprawdzenie zakresu. Ocena 9 zostanie przyjęta i średnia wzrośnie do 4,75 — program nie zgłosi żadnego problemu. To cały trzeci poziom walidacji w jednym doświadczeniu.
  4. Zamień catch (FileNotFoundException) na catch (Exception) i dopisz w bloku try literówkę, na przykład dzielenie przez zero. Program wypisze komunikat o braku pliku, choć plik nie ma z tym nic wspólnego — tak wygląda ukrywanie własnych błędów.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementWynikDziałanie i ograniczenia
null—Brak wartości. Różni się od 0 i od "".
string?, int?, Uczen?—Typ dopuszczający brak wartości.
x.HasValueboolCzy int? ma liczbę.
x.Valuetyp bazowyOdczyt wartości; dla null rzuca InvalidOperationException.
x.GetValueOrDefault()typ bazowyWartość albo wartość domyślna typu (dla liczb zero).
x is null / x is not nullboolSprawdzenie braku wartości; wycisza CS8602 w bloku.
a ?? btyp aZwraca b tylko gdy a jest null. Nie reaguje na pusty napis.
a ??= b—Przypisuje b tylko gdy a jest null.
a?.Metoda()typ z ?Wywołanie tylko dla wartości różnej od null.
a?[i]typ z ?Odczyt elementu tylko dla wartości różnej od null.
a!typ bez ?Tylko wycisza ostrzeżenie — nie sprawdza i nie naprawia niczego.
string.IsNullOrEmpty(t)booltrue dla null i ""; spacje przepuszcza.
string.IsNullOrWhiteSpace(t)booltrue także dla spacji i tabulatorów. Do danych od użytkownika.
t.Trim()stringKopia bez spacji na brzegach; nie zmienia oryginału.
int.TryParse(t, out w)boolPrzyjmuje string?; dla null zwraca false.
double.TryParse(t, out w)boolSeparator zależy od ustawień systemu — na polskich to przecinek.
double.IsFinite(w)boolfalse dla NaN i obu nieskończoności.
char.IsDigit(z)boolCzy znak jest cyfrą (lekcja 07).
try / catch (Typ)—Zabezpieczenie operacji, której nie da się sprawdzić z góry. Szerzej w lekcji 35.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
int.Parse(Console.ReadLine())Trzy sposoby na przerwanie programu w jednym wierszu: null, zły format, przekroczenie zakresu. Zawsze TryParse.
Ignorowanie żółtych podkreśleń CS86xxTo lista miejsc, w których program może się wywrócić na null. Kompilator wykonał za Ciebie pracę — przeczytaj wynik.
wpis!.LengthWykrzyknik wycisza ostrzeżenie i nic nie sprawdza. Wyjątek zostaje, a ostrzeżenie — jedyny sygnał — znika.
?? do wyłapania pustego napisu"" ?? "zastepcze" daje pusty napis. ?? reaguje wyłącznie na null.
IsNullOrEmpty dla danych z konsoliPrzepuszcza same spacje. Do wpisów użytkownika zawsze IsNullOrWhiteSpace.
Sprawdzenie długości przed Trim()„ ” ma trzy znaki i przejdzie warunek minimalnej długości. Najpierw czyść, potem sprawdzaj.
Walidacja formatu bez walidacji zakresuWiek −5 i ocena 9 są poprawnymi liczbami. Trzeci poziom sprawdzenia musisz napisać sam.
double.TryParse bez IsFiniteTekst Infinity albo NaN przechodzi jako poprawna liczba i psuje każde dalsze obliczenie.
Liczenie średniej bez sprawdzenia liczby elementówPusta lista daje dzielenie przez zero albo NaN. Sprawdź licznik przed dzieleniem.
catch (Exception) { }Ukrywa wszystko, także Twoje literówki. Program „działa” i daje złe wyniki bez żadnego sygnału.
try tam, gdzie wystarczy ifDzielenie przez zero i indeks poza tablicą sprawdza się warunkiem. Wyjątki są do rzeczy, których sprawdzić nie można.
Komunikat „błędne dane”Nie pomaga nikomu. Napisz, co i dlaczego zostało odrzucone — tak jak w metodzie SprawdzOcene.
Sprawdzanie tych samych danych w każdej metodzieSprawdź raz, na granicy programu. Dalej krążą już dane czyste.
ZADANIA

Zadania

ZAD 1null, zero i pusty napis★☆☆

Zadeklaruj cztery zmienne: string? a = null;, string b = "";, string c = " "; i string d = "0";. Dla każdej wypisz w jednym wierszu: wynik string.IsNullOrEmpty, wynik string.IsNullOrWhiteSpace oraz — tam, gdzie to możliwe — długość. Przy zmiennej a spróbuj odczytać a.Length: zapisz w komentarzu numer ostrzeżenia, które pokazuje kompilator, uruchom program i zapisz nazwę wyjątku. Potem popraw ten odczyt na a?.Length ?? -1 i sprawdź, co się wypisze. Na koniec wypisz wyniki a ?? "zastepcze" oraz b ?? "zastepcze" i wyjaśnij w komentarzu, dlaczego drugi wynik zaskakuje.

ZAD 2Wczytywanie liczby bez awarii★☆☆

Napisz program, który prosi o liczbę całkowitą i wypisuje jej kwadrat. Zabezpiecz go tak, żeby żadne wejście nie przerwało programu: obsłuż sam Enter, tekst niebędący liczbą oraz liczbę za dużą dla typu int. Użyj wyłącznie IsNullOrWhiteSpace i TryParse — bez try i bez Parse. Dla każdego z trzech złych wejść wypisz inny, konkretny komunikat. Sprawdź program, podając kolejno: Enter, kot, 99999999999999, -3 i 7, i zapisz wyniki w komentarzu.

ZAD 3Trzy poziomy walidacji★★☆

Napisz metodę static int WczytajOcene(string zacheta), która w pętli prosi o ocenę i zwraca ją dopiero wtedy, gdy przejdzie wszystkie trzy poziomy sprawdzenia: obecność, format i zakres 1–6. Każdy poziom ma mieć własny komunikat. Wywołaj ją pięć razy, zbierz oceny do tablicy i wypisz średnią z dokładnością do dwóch miejsc oraz ocenę najwyższą i najniższą. Następnie usuń sprawdzenie zakresu, uruchom ponownie i podaj oceny 5, 5, 9, 5, 5. Zapisz w komentarzu obie średnie i jedno zdanie o tym, dlaczego program bez trzeciego poziomu jest groźniejszy niż program, który się przerywa.

ZAD 4Klauzule strażnicze★★☆

Napisz metodę static void ZapiszZawodnika(string? imie, string? klasa, int wiek, double czas) w dwóch wersjach. Wersja pierwsza — z zagnieżdżonymi instrukcjami if, sprawdzająca kolejno: imię niepuste i o długości od 3 do 40 znaków, klasę niepustą o 3 lub 4 znakach, wiek od 14 do 21 oraz czas dodatni i skończony. Wersja druga — ta sama logika zapisana klauzulami strażniczymi z wczesnym return. Wywołaj obie dla pięciu zestawów danych: jednego poprawnego i czterech, z których każdy narusza inny warunek. Sprawdź, że obie wersje dają identyczne komunikaty. W komentarzu policz, ile poziomów wcięcia ma najgłębsze miejsce w każdej wersji.

ZAD 5Metoda sprawdzająca z powodem odmowy★★☆

Napisz metodę static bool SprawdzWynik(string? wiersz, out string imie, out double czas, out string blad). Parametr wiersz ma mieć postać imie;czas. Metoda odrzuca wiersz, gdy: jest pusty albo sam z białych znaków; po podziale metodą Split(';') nie ma dokładnie dwóch pól; imię po Trim() jest puste lub krótsze niż 3 znaki; czas nie daje się wczytać przez double.TryParse; czas nie jest skończony; czas nie mieści się w zakresie od 10 do 600 sekund. Dla każdego przypadku ustaw inny, konkretny komunikat w parametrze blad. Przetestuj metodę na tablicy co najmniej ośmiu wierszy pokrywającej wszystkie przypadki — poprawne i błędne — wypisz wynik dla każdego, a na końcu podaj, ile wierszy przyjęto, ile odrzucono i jaki jest najlepszy czas wśród przyjętych. Pamiętaj o sytuacji, w której nie przyjęto ani jednego wiersza.

ZAD 6Formularz zapisów — pełny program★★★

Napisz program obsługujący zapisy na szkolne zawody. Program ma wczytywać dane kolejnych zawodników w pętli, dopóki użytkownik nie wpisze jako imienia słowa koniec. Dla każdego zawodnika wczytaj: imię i nazwisko (obecność, długość 3–40 znaków, musi zawierać co najmniej jedną spację — inaczej odrzuć z komunikatem); klasę (3 lub 4 znaki, pierwszy znak cyfra, zapisz wielkimi literami); wiek (14–21); czas (liczba rzeczywista, skończona, od 10 do 600 sekund); numer telefonu opiekuna jako pole opcjonalne — Enter oznacza brak, a podany numer wyczyść do samych cyfr i przyjmij tylko wtedy, gdy ma ich dokładnie dziewięć. Każde pole wczytuj osobną metodą z pętlą powtarzającą pytanie do poprawnej odpowiedzi; numer telefonu przechowuj jako string?. Zawodników trzymaj w List<Zawodnik>, gdzie Zawodnik jest klasą z odpowiednimi właściwościami i przesłoniętym ToString(), które buduje cały gotowy wiersz tabelki — wraz z wyrównanymi kolumnami i tekstem „brak” w miejscu niepodanego telefonu. Po zakończeniu zapisów wypisz: listę zawodników pętlą, podając do WriteLine samą zmienną, żeby wiersz powstawał w jednym miejscu programu; liczbę zapisanych; najlepszy i najgorszy czas; średni czas; liczbę zawodników bez podanego telefonu. Na końcu, w komentarzu, odpowiedz na trzy pytania: które pole ma typ z ? i dlaczego pozostałe go nie mają; w którym miejscu programu dane przestają być „brudne”; oraz który z warunków sprawdza sens danej, a nie jej format.

PODSUMOWANIE

Co trzeba zapamiętać

  • null znaczy „nie ma wartości” i to coś innego niż 0 oraz "".
  • Znak zapytania w typie (string?, int?) mówi, że brak wartości jest dopuszczalny — i że trzeba go sprawdzać.
  • Ostrzeżenia CS86xx to lista miejsc, w których program może się wywrócić na null. Nie wolno ich ignorować.
  • ?? reaguje tylko na null — pustego napisu nie zastąpi.
  • ?. zwraca null zamiast rzucać wyjątek, ale jego wynik ma typ z ?.
  • Wykrzyknik ! wycisza ostrzeżenie i niczego nie sprawdza. Prawdziwe sprawdzenie wycisza ostrzeżenie samo.
  • Dane z zewnątrz sprawdzaj na trzech poziomach: obecność, format, sens. Trzeci musisz napisać sam.
  • IsNullOrWhiteSpace zamiast IsNullOrEmpty do wszystkiego, co wpisuje użytkownik.
  • Najpierw Trim(), potem sprawdzanie długości — inaczej same spacje przejdą.
  • TryParse zawsze zamiast Parse dla danych z zewnątrz; przyjmuje też null.
  • Sprawdzaj raz, na granicy programu. Dalej krążą dane czyste.
  • Klauzule strażnicze z wczesnym return są czytelniejsze od zagnieżdżonych warunków.
  • Komunikat o odmowie ma mówić co i dlaczego — „błędne dane” nie pomaga nikomu.
  • Co da się sprawdzić warunkiem — sprawdzaj warunkiem. Wyjątki są do rzeczy, o których nie dowiesz się wcześniej.

Dokumentacja: Microsoft Learn — temat tej lekcji.

Postęp zapisuje się w tej przeglądarce.