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ń.
Czego się dziś nauczysz
- Odróżnisz
nullod 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 zHasValue,ValueiGetValueOrDefault. - 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ą
booloraz dane i powód odmowy przezout. - 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.
Trzy wiersze i pięć sposobów na kłopot
Trzywierszowy program, jakich pisałeś już kilkadziesiąt:
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żytkownik | Co się stanie |
|---|---|
17 | działa poprawnie |
| sam Enter | ArgumentNullException — program przerwany |
kot | FormatException — program przerwany |
17 (ze spacjami) | działa; Parse sam usuwa spacje z brzegów |
-5 | działa i wypisuje bzdurę: „za rok będziesz miał -4 lata” |
99999999999999 | OverflowException — 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.
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 ludzku | Length | Przykład z życia |
|---|---|---|---|
null | nie ma wartości, nie ma na co patrzeć | wyjątek! | pole „telefon” nigdy nie wypełnione |
"" | napis istnieje, ale jest pusty | 0 | użytkownik nacisnął Enter i wyczyściliśmy spacje |
" " | napis ze spacją — nie jest pusty | 1 | użytkownik nacisnął spację i Enter |
"0" | napis z jednym znakiem | 1 | wpisane zero |
0 | liczba 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:
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:
| Zapis | Znaczenie | Czy sprawdzać null |
|---|---|---|
string imie | zawsze ma napis | nie |
string? telefon | może nie mieć napisu | tak, zawsze |
int wiek | zawsze ma liczbę | nie da się — liczba nie bywa null |
int? ocena | może nie mieć liczby | tak |
Uczen u | zawsze wskazuje obiekt | nie |
Uczen? u | może nie wskazywać obiektu | tak |
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ć
nulldo 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
nulltam, gdzie typ tego nie przewiduje
int? od środka
int? to liczba albo informacja, że liczby nie ma. Sprawdza się to i odczytuje trzema sposobami:
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.
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
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: ??
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: ??=
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 ?[]
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”: !
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.
| Zapis | Co robi | Wynik dla null |
|---|---|---|
x is null | sprawdza brak wartości | true |
x is not null | sprawdza obecność wartości | false |
x ?? z | wartość albo zastępcza | z |
x ??= z | przypisz, jeśli puste | x staje się z |
x?.Metoda() | wywołaj, jeśli nie puste | null, bez wyjątku |
x?[0] | weź element, jeśli nie puste | null, bez wyjątku |
x! | wycisza ostrzeżenie | wyjątek — nic nie sprawdza |
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.
| Poziom | Pytanie | Narzędzie | Przykład odrzucenia |
|---|---|---|---|
| 1. Obecność | czy coś w ogóle podano? | string.IsNullOrWhiteSpace | sam Enter, jedna spacja |
| 2. Format | czy to jest liczba / data / kod? | int.TryParse, double.TryParse | kot, 17a, 1 7 |
| 3. Sens | czy ta wartość ma sens w tym zadaniu? | zwykły if z zakresem | wiek −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
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ście | IsNullOrEmpty | IsNullOrWhiteSpace |
|---|---|---|
null | true | true |
"" | true | true |
" " | false ✘ | true ✔ |
"\t\n" | false ✘ | true ✔ |
"Kasia" | false | false |
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
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
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:
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.
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ą dane | Zaufanie | Co robić |
|---|---|---|
| konsola — użytkownik | zero | sprawdzić wszystkie trzy poziomy |
| plik, sieć, baza | zero | sprawdzić wszystkie trzy poziomy |
| argumenty programu | zero | sprawdzić wszystkie trzy poziomy |
| zmienna policzona w programie | pełne | nie sprawdzać — już sprawdziłeś na wejściu |
| pole obiektu, który sam pilnuje zasad | pełne | nie 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:
// 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");
}
}
// 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):
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;
}
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}");
}
}
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.
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.
| Sytuacja | Da się sprawdzić warunkiem? | Czym się zabezpieczyć |
|---|---|---|
| użytkownik wpisał tekst zamiast liczby | tak | TryParse |
| ocena poza zakresem 1–6 | tak | zwykły if |
| dzielenie przez zero | tak | if (dzielnik != 0) |
| indeks poza tablicą | tak | if (i < tab.Length) |
| pusta kolekcja | tak | if (lista.Count > 0) |
| plik zniknął między sprawdzeniem a odczytem | nie | wyjątek |
| brak uprawnień do pliku | nie | wyjątek |
| dysk pełny w trakcie zapisu | nie | wyjątek |
| zerwane połączenie sieciowe | nie | wyją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:
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.
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 - 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;
}
=== 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 — przezreturnz poprawną wartością. Zły wpis kończy się komunikatem icontinue, czyli kolejnym pytaniem. WczytajTekstsprawdza dwa poziomy: obecność (IsNullOrWhiteSpace) i sens (długość). Zauważ, że długość liczy się poTrim()— inaczej trzy spacje przeszłyby jako trzy znaki.WczytajKlasedokłada trzeci warunek i przy okazji poprawia dane:3tpwchodzi, a zapisuje się jako3TP. Sprawdzanie i normalizowanie w jednym miejscu to dobry nawyk.WczytajIntnie sprawdzanullosobno —int.TryParseprzyjmujestring?i dlanullpo prostu zwracafalse. Jedno sprawdzenie zamiast dwóch.WczytajCzasodrzuca62.5z kropką, bo na polskich ustawieniach separatorem jest przecinek. PoTryParsestoi jeszczeIsFinite— bo tekstInfinityprzeszedłby przezTryParsejako poprawna liczba.WczytajTelefonzwracastring?, bo brak numeru jest poprawną odpowiedzią. Enter dajenullbez ż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 naNullReferenceException. - 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 == 0nie 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
- Zamień w
WczytajTekstmetodęIsNullOrWhiteSpacenaIsNullOrEmpty. Teraz da się przejść dalej, wpisując trzy spacje — i zawodnik nazywa się „ ”. - Usuń
?? "nie podano"z wypisywania telefonu. Program się skompiluje, a przy pustym numerze wypisze pusty wiersz — nie wyjątek, bo interpolacja radzi sobie znull. Potem usuń?ztel?.Length— tu dostaniesz CS8602, a po uruchomieniuNullReferenceException. - W
SprawdzOceneusuń 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. - Zamień
catch (FileNotFoundException)nacatch (Exception)i dopisz w blokutryliteró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.
Zestawienie elementów
| Element | Wynik | Działanie i ograniczenia |
|---|---|---|
null | — | Brak wartości. Różni się od 0 i od "". |
string?, int?, Uczen? | — | Typ dopuszczający brak wartości. |
x.HasValue | bool | Czy int? ma liczbę. |
x.Value | typ bazowy | Odczyt wartości; dla null rzuca InvalidOperationException. |
x.GetValueOrDefault() | typ bazowy | Wartość albo wartość domyślna typu (dla liczb zero). |
x is null / x is not null | bool | Sprawdzenie braku wartości; wycisza CS8602 w bloku. |
a ?? b | typ a | Zwraca 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) | bool | true dla null i ""; spacje przepuszcza. |
string.IsNullOrWhiteSpace(t) | bool | true także dla spacji i tabulatorów. Do danych od użytkownika. |
t.Trim() | string | Kopia bez spacji na brzegach; nie zmienia oryginału. |
int.TryParse(t, out w) | bool | Przyjmuje string?; dla null zwraca false. |
double.TryParse(t, out w) | bool | Separator zależy od ustawień systemu — na polskich to przecinek. |
double.IsFinite(w) | bool | false dla NaN i obu nieskończoności. |
char.IsDigit(z) | bool | Czy znak jest cyfrą (lekcja 07). |
try / catch (Typ) | — | Zabezpieczenie operacji, której nie da się sprawdzić z góry. Szerzej w lekcji 35. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
int.Parse(Console.ReadLine()) | Trzy sposoby na przerwanie programu w jednym wierszu: null, zły format, przekroczenie zakresu. Zawsze TryParse. |
| Ignorowanie żółtych podkreśleń CS86xx | To lista miejsc, w których program może się wywrócić na null. Kompilator wykonał za Ciebie pracę — przeczytaj wynik. |
wpis!.Length | Wykrzyknik 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 konsoli | Przepuszcza 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 zakresu | Wiek −5 i ocena 9 są poprawnymi liczbami. Trzeci poziom sprawdzenia musisz napisać sam. |
double.TryParse bez IsFinite | Tekst Infinity albo NaN przechodzi jako poprawna liczba i psuje każde dalsze obliczenie. |
| Liczenie średniej bez sprawdzenia liczby elementów | Pusta 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 if | Dzielenie 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 metodzie | Sprawdź raz, na granicy programu. Dalej krążą już dane czyste. |
Zadania
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.
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.
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.
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.
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.
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.
Co trzeba zapamiętać
nullznaczy „nie ma wartości” i to coś innego niż0oraz"".- 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 nanull— pustego napisu nie zastąpi.?.zwracanullzamiast 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.
IsNullOrWhiteSpacezamiastIsNullOrEmptydo wszystkiego, co wpisuje użytkownik.- Najpierw
Trim(), potem sprawdzanie długości — inaczej same spacje przejdą. TryParsezawsze zamiastParsedla danych z zewnątrz; przyjmuje teżnull.- Sprawdzaj raz, na granicy programu. Dalej krążą dane czyste.
- Klauzule strażnicze z wczesnym
returnsą 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.