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ć.

C# try/catch throw finally 55 min
CEL LEKCJI

Czego się dziś nauczysz

  • Odróżnisz sytuację na wyjątek od sytuacji na zwykły warunek
  • Zapiszesz try, kilka bloków catch we właściwej kolejności i finally
  • Zgłosisz własny błąd instrukcją throw z odpowiednim typem wyjątku
  • Wyjaśnisz różnicę między throw; a throw 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.

TEORIA

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.

SytuacjaSposób
Użytkownik wpisał literę zamiast liczbyTryParse i komunikat — to normalny przebieg programu
Wiek spoza zakresu 0–120warunek i ponowne pytanie
Plik, który mamy odczytać, nie istniejewyjątek — sprawdzenie i tak nie da gwarancji, plik może zniknąć chwilę później
Metoda dostała ujemną długość bokuwyjątek — to błąd w kodzie wywołującego, nie w danych
Dzielenie przez zero na liczbach całkowitychwyją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.

TEORIA

try, catch i finally

Program.cs
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.");
}
  • catch sprawdzane 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.
  • finally wykonuje się i po sukcesie, i po wyjątku, i po return — 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ł) i InnerException (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

Program.cs
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.

TEORIA

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.

definicja metody
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ątkuKiedy zgłaszać
ArgumentExceptionargument ma zły format albo wartość niepasującą do zadania
ArgumentNullExceptionargument jest null, a metoda go potrzebuje
ArgumentOutOfRangeExceptionargument jest liczbą spoza dozwolonego zakresu
InvalidOperationExceptionobiekt jest w stanie, w którym operacja nie ma sensu (np. pusta kolejka)
NotSupportedExceptionoperacja 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;

Program.cs
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

Przykład: bezpieczny kalkulator średniej

Program.cs
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");
    }
}
wynik w konsoli
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

  • ArgumentNullException i ArgumentOutOfRangeException dziedziczą po ArgumentException, więc jeden catch obsługuje wszystkie trzy przypadki.
  • Środowisko samo dokleja do komunikatu nazwę parametru — stąd fragment (Parameter 'oceny').
  • finally wykonuje 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.
ROZSZERZENIE

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:

definicja typu
class BladDanychException : Exception
{
    public int NumerWiersza { get; }

    public BladDanychException(int numerWiersza, string komunikat)
        : base(komunikat)
    {
        NumerWiersza = numerWiersza;
    }
}
użycie
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.

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZwracaDział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.MessagestringOpis dla człowieka. Nie nadaje się do rozpoznawania rodzaju błędu w kodzie — od tego jest typ wyjątku.
ex.StackTracestringŚlad wywołań od miejsca powstania wyjątku. Bezcenny przy szukaniu przyczyny.
ex.InnerExceptionException?Wyjątek źródłowy, gdy opakowano go w inny. Może być null.
nameof(x)stringNazwa 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).
CZĘSTE BŁĘDY

Na co uważać

ZapisProblem
catch (Exception) jako pierwszyPrzechwyci 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 MainWszystko 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żytkownikaZły format wpisu to normalna sytuacja, a nie błąd wykonania. Wyjątki są też wyraźnie wolniejsze.
Rozpoznawanie błędu po treści MessageKomunikat zależy od języka systemu. Rozpoznawaj po typie wyjątku.
ZADANIA

Zadania

ZAD 1Trzy wyjątki★☆☆

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.

ZAD 2Kolejność ma znaczenie★☆☆

Umieść catch (Exception) przed catch (FormatException) i zapisz treść błędu kompilatora. Potem przywróć poprawną kolejność.

ZAD 3finally w akcji★★☆

Napisz metodę, która wewnątrz try wykonuje return, a w finally wypisuje komunikat. Sprawdź, czy komunikat się pojawia, i wyjaśnij wynik.

ZAD 4Walidacja przez throw★★☆

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.

ZAD 5TryParse czy wyjątek★★☆

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.

ZAD 6Czytnik danych odporny na błędy★★★

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.

PODSUMOWANIE

Co trzeba zapamiętać

  • Wyjątek zgłaszamy, gdy metoda nie może wykonać zadania; dane od użytkownika sprawdzamy warunkiem i TryParse.
  • Bloki catch sprawdzane są od góry — od szczegółowych do ogólnych, catch (Exception) zawsze ostatni.
  • finally wykonuje się zawsze przy wyjściu z try, także po return.
  • throw new ArgumentException(...) z nameof daje 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 catch ukrywa błąd przed tobą, nie przed użytkownikiem.

Dokumentacja: Microsoft Learn — temat tej lekcji.