Wyjątki — throw, finally i własne typy błędów
Plik może zniknąć, dysk się zapełnić, a metoda dostać dane, z którymi nie da się nic zrobić. Poznasz mechanizm, który przerywa obliczenie i przekazuje sterowanie temu, kto potrafi zareagować.
Czego się dziś nauczysz
- Odróżnisz sytuację na wyjątek od sytuacji na zwykły warunek
- Zapiszesz
try, kilka blokówcatchwe właściwej kolejności ifinally - Zgłosisz własny błąd instrukcją
throwz odpowiednim typem wyjątku - Wyjaśnisz różnicę między
throw;athrow ex; - Zaprojektujesz obsługę błędów, która nie ukrywa przyczyny
Przygotowanie: lekcje 01–34. Przewidywany czas: 45–90 minut z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.
Kiedy wyjątek, a kiedy zwykły warunek
Z lekcji 34 wiesz, że dane od użytkownika sprawdzamy warunkiem i metodą TryParse. Wyjątki są do czegoś innego: do sytuacji, w których metoda nie może wykonać swojego zadania i nie ma sensownej wartości do zwrócenia.
| Sytuacja | Sposób |
|---|---|
| Użytkownik wpisał literę zamiast liczby | TryParse i komunikat — to normalny przebieg programu |
| Wiek spoza zakresu 0–120 | warunek i ponowne pytanie |
| Plik, który mamy odczytać, nie istnieje | wyjątek — sprawdzenie i tak nie da gwarancji, plik może zniknąć chwilę później |
| Metoda dostała ujemną długość boku | wyjątek — to błąd w kodzie wywołującego, nie w danych |
| Dzielenie przez zero na liczbach całkowitych | wyjątek zgłasza samo środowisko |
Wyjątek to nie awaria
Nazwa myli: „wyjątek” nie znaczy „program się zepsuł”. To komunikat, który przerywa bieżące obliczenie i przekazuje sterowanie temu, kto potrafi zareagować. Nieobsłużony — kończy program i wypisuje ślad stosu.
Skąd tu pliki
W przykładach pojawia się File.ReadAllText("dane.txt") — metoda wczytująca cały plik jako jeden napis. Plikami zajmiemy się w lekcji 36; tutaj są tylko wygodnym źródłem typowych błędów wykonania, bo plik może nie istnieć, być zajęty albo zawierać bzdury. Do wykonania zadań z tej lekcji znajomość plików nie jest potrzebna.
try, catch i finally
try
{
string tresc = File.ReadAllText("dane.txt");
int liczba = int.Parse(tresc);
Console.WriteLine(100 / liczba);
}
catch (FileNotFoundException ex) // najbardziej szczegolowy - pierwszy
{
Console.WriteLine($"Brak pliku: {ex.FileName}");
}
catch (FormatException)
{
Console.WriteLine("Plik nie zawiera liczby calkowitej.");
}
catch (DivideByZeroException)
{
Console.WriteLine("W pliku jest zero - nie moge dzielic.");
}
catch (Exception ex) // siatka bezpieczenstwa - ostatnia
{
Console.WriteLine($"Nieoczekiwany blad: {ex.Message}");
}
finally
{
Console.WriteLine("Ten blok wykona sie zawsze.");
}
catchsprawdzane są po kolei, od góry. Pierwszy pasujący typ obsługuje wyjątek, reszta jest pomijana.- Dlatego
catch (Exception)musi być ostatni — postawiony wyżej przechwyci wszystko i zasłoni pozostałe bloki. Kompilator zgłosi to jako błąd CS0160. finallywykonuje się i po sukcesie, i po wyjątku, i poreturn— służy do sprzątania. Nie działa po zakończeniu procesu ani po odcięciu zasilania.- Obiekt wyjątku (
ex) ma m.in.Message(opis dla człowieka),StackTrace(gdzie powstał) iInnerException(przyczyna źródłowa).
Pusty catch to najgorsze, co możesz zrobić
catch { } udaje, że nic się nie stało. Program idzie dalej z niepełnymi danymi, a przyczyna błędu znika bez śladu — także dla ciebie, gdy będziesz go szukać. Jeśli naprawdę chcesz wyjątek zignorować, napisz komentarz, dlaczego jest to bezpieczne.
Filtr when
catch (IOException ex) when (ex.Message.Contains("dysk"))
{
Console.WriteLine("Problem z dyskiem.");
}
Blok złapie wyjątek tylko wtedy, gdy warunek jest prawdziwy. W przeciwnym razie wyjątek leci dalej, jakby tego catch nie było.
throw — zgłaszanie własnych błędów
Metoda, która dostała dane niemożliwe do przetworzenia, powinna powiedzieć o tym głośno, a nie zwracać zero albo -1.
static double PoleProstokata(double a, double b)
{
if (a <= 0)
{
throw new ArgumentOutOfRangeException(nameof(a), "Bok musi byc dodatni.");
}
if (b <= 0)
{
throw new ArgumentOutOfRangeException(nameof(b), "Bok musi byc dodatni.");
}
return a * b;
}
| Typ wyjątku | Kiedy zgłaszać |
|---|---|
ArgumentException | argument ma zły format albo wartość niepasującą do zadania |
ArgumentNullException | argument jest null, a metoda go potrzebuje |
ArgumentOutOfRangeException | argument jest liczbą spoza dozwolonego zakresu |
InvalidOperationException | obiekt jest w stanie, w którym operacja nie ma sensu (np. pusta kolejka) |
NotSupportedException | operacja nie jest w tym typie przewidziana |
Operator nameof(a) wstawia nazwę parametru jako tekst. Gdy zmienisz nazwę parametru, komunikat zmieni się razem z nim — inaczej niż wpisany ręcznie napis.
throw; kontra throw ex;
try
{
Zapisz();
}
catch (IOException)
{
Console.WriteLine("Zapis sie nie udal, przekazuje dalej.");
throw; // TAK - zachowuje miejsce, w ktorym wyjatek powstal
// throw ex; // NIE - kasuje slad stosu i wskaze te linie
}
Łap tylko to, na co potrafisz zareagować
Jeśli w danym miejscu nie umiesz sensownie zareagować na wyjątek, nie przechwytuj go. Wyjątek, który dojdzie do Main, przynajmniej pokaże prawdziwą przyczynę. Obsłużony i zamieniony na komunikat „coś poszło nie tak” — nie pokaże niczego.
Przykład: bezpieczny kalkulator średniej
class Program
{
static double Srednia(int[] oceny)
{
if (oceny == null)
{
throw new ArgumentNullException(nameof(oceny));
}
if (oceny.Length == 0)
{
throw new ArgumentException("Tablica ocen nie moze byc pusta.", nameof(oceny));
}
int suma = 0;
foreach (int o in oceny)
{
if (o < 1 || o > 6)
{
throw new ArgumentOutOfRangeException(nameof(oceny), $"Ocena {o} jest spoza zakresu 1-6.");
}
suma += o;
}
return (double)suma / oceny.Length;
}
static void Pokaz(int[] dane, string opis)
{
try
{
Console.WriteLine($"{opis}: {Srednia(dane):F2}");
}
catch (ArgumentException ex)
{
Console.WriteLine($"{opis}: odrzucone - {ex.Message}");
}
finally
{
Console.WriteLine($"{opis}: sprawdzanie zakonczone.");
}
}
static void Main()
{
Pokaz(new int[] { 5, 4, 3 }, "poprawne");
Pokaz(new int[0], "puste");
Pokaz(new int[] { 5, 9 }, "spoza zakresu");
}
}
poprawne: 4,00
poprawne: sprawdzanie zakonczone.
puste: odrzucone - Tablica ocen nie moze byc pusta. (Parameter 'oceny')
puste: sprawdzanie zakonczone.
spoza zakresu: odrzucone - Ocena 9 jest spoza zakresu 1-6. (Parameter 'oceny')
spoza zakresu: sprawdzanie zakonczone.
Co dzieje się po kolei
ArgumentNullExceptioniArgumentOutOfRangeExceptiondziedziczą poArgumentException, więc jedencatchobsługuje wszystkie trzy przypadki.- Środowisko samo dokleja do komunikatu nazwę parametru — stąd fragment
(Parameter 'oceny'). finallywykonuje się w każdym z trzech wywołań, także tam, gdzie wszystko się udało.- Metoda licząca średnią nic nie wypisuje. Za komunikaty odpowiada
Pokaz— obliczenie i prezentacja to dwie różne odpowiedzialności.
Własny typ wyjątku (po lekcji 45)
Ta część korzysta z dziedziczenia, które poznasz w lekcji 45. Wróć do niej wtedy — albo przeczytaj teraz i potraktuj jako zapowiedź.
Gdy w programie pojawia się błąd charakterystyczny dla twojej dziedziny — „uczeń o tym numerze już istnieje”, „zbyt mało punktów” — warto mieć dla niego osobny typ. Wystarczy klasa dziedzicząca po Exception:
class BladDanychException : Exception
{
public int NumerWiersza { get; }
public BladDanychException(int numerWiersza, string komunikat)
: base(komunikat)
{
NumerWiersza = numerWiersza;
}
}
try
{
throw new BladDanychException(7, "Brak srednika w wierszu.");
}
catch (BladDanychException ex)
{
Console.WriteLine($"Wiersz {ex.NumerWiersza}: {ex.Message}");
}
Zapis : base(komunikat) przekazuje tekst do konstruktora klasy Exception, dzięki czemu właściwość Message działa jak w wyjątkach wbudowanych. Własna właściwość NumerWiersza niesie informację, której żaden wbudowany typ nie ma.
Nie mnóż typów bez potrzeby
Własny wyjątek ma sens, gdy ktoś będzie go osobno przechwytywał albo gdy niesie dodatkowe dane. Jeśli chodzi tylko o inny komunikat, wystarczy ArgumentException z dobrym opisem.
Zestawienie elementów
| Element | Zwraca | Działanie i ograniczenia |
|---|---|---|
try { } catch (T ex) { } | — | Blok chroniony i obsługa wyjątku typu T lub pochodnego. Bloki sprawdzane są po kolei. |
finally { } | — | Wykonuje się przy każdym wyjściu z try, także przez return. Nie wykona się po zabiciu procesu. |
throw new T(...) | — | Zgłasza nowy wyjątek i przerywa bieżącą metodę. |
throw; | — | Wewnątrz catch: przekazuje ten sam wyjątek dalej, zachowując ślad stosu. |
ex.Message | string | Opis dla człowieka. Nie nadaje się do rozpoznawania rodzaju błędu w kodzie — od tego jest typ wyjątku. |
ex.StackTrace | string | Ślad wywołań od miejsca powstania wyjątku. Bezcenny przy szukaniu przyczyny. |
ex.InnerException | Exception? | Wyjątek źródłowy, gdy opakowano go w inny. Może być null. |
nameof(x) | string | Nazwa zmiennej lub parametru jako tekst; sprawdzana przez kompilator. |
catch (T ex) when (warunek) | — | Przechwytuje tylko wtedy, gdy warunek jest prawdziwy. |
using (…) { } | — | Gwarantuje wywołanie Dispose — częsty i wygodniejszy zamiennik finally przy zasobach (lekcja 36). |
Na co uważać
| Zapis | Problem |
|---|---|
catch (Exception) jako pierwszy | Przechwyci wszystko i zasłoni bloki szczegółowe. Kompilator zgłasza CS0160. |
catch { } | Błąd znika bez śladu, a program pracuje na niepełnych danych. |
throw ex; zamiast throw; | Kasuje ślad stosu — StackTrace wskaże ten catch, a nie miejsce powstania błędu. |
try wokół całego Main | Wszystko trafia do jednego bloku i nie wiadomo, co zawiodło. Obejmuj tylko tę operację, która może się nie udać. |
Wyjątek zamiast TryParse dla danych od użytkownika | Zły format wpisu to normalna sytuacja, a nie błąd wykonania. Wyjątki są też wyraźnie wolniejsze. |
Rozpoznawanie błędu po treści Message | Komunikat zależy od języka systemu. Rozpoznawaj po typie wyjątku. |
Zadania
Napisz program, który celowo wywołuje trzy różne wyjątki: dzielenie przez zero, odczyt t[10] z pięcioelementowej tablicy oraz int.Parse("kot"). Każdy obsłuż osobnym blokiem catch i wypisz typ oraz komunikat.
Umieść catch (Exception) przed catch (FormatException) i zapisz treść błędu kompilatora. Potem przywróć poprawną kolejność.
Napisz metodę, która wewnątrz try wykonuje return, a w finally wypisuje komunikat. Sprawdź, czy komunikat się pojawia, i wyjaśnij wynik.
Napisz metodę int Podziel(int a, int b), która przy b równym zero zgłasza ArgumentException z sensownym komunikatem i nazwą parametru. Obsłuż ją w Main.
Zbuduj wczytywanie oceny na dwa sposoby: przez TryParse i przez int.Parse w bloku try. Wykonaj oba dla stu błędnych wpisów w pętli, zmierz czas klasą Stopwatch i zapisz wniosek.
Dane przychodzą jako tablica napisów w formacie imie;punkty — wpisz ją na sztywno w kodzie. Wiersz o złej liczbie pól, punkty niebędące liczbą i punkty spoza zakresu 0–100 mają dawać trzy różne komunikaty, a program ma dokończyć pracę i podać liczbę wierszy poprawnych oraz odrzuconych. Rozstrzygnij świadomie, które z tych sytuacji zasługują na wyjątek, a które na zwykły warunek, i zapisz uzasadnienie w komentarzu.
Co trzeba zapamiętać
- Wyjątek zgłaszamy, gdy metoda nie może wykonać zadania; dane od użytkownika sprawdzamy warunkiem i
TryParse. - Bloki
catchsprawdzane są od góry — od szczegółowych do ogólnych,catch (Exception)zawsze ostatni. finallywykonuje się zawsze przy wyjściu ztry, także poreturn.throw new ArgumentException(...)znameofdaje komunikat, który nie zestarzeje się po zmianie nazwy parametru.throw;przekazuje wyjątek dalej z zachowaniem śladu stosu;throw ex;ten ślad kasuje.- Pusty
catchukrywa błąd przed tobą, nie przed użytkownikiem.
Dokumentacja: Microsoft Learn — temat tej lekcji.