1. Wstęp
Wyobraź sobie, że piszesz kalkulator do dzielenia dwóch liczb. Wszystko działa super, dopóki ktoś nie spróbuje podzielić przez zero:
int a = 10;
int b = 0;
int result = a / b; // Bum!
W tym momencie program się "wywala", a użytkownik widzi straszny komunikat o błędzie. To właśnie wyjątkowa sytuacja — zdarzenie, które nie powinno mieć miejsca w "normalnym" scenariuszu działania programu i wymaga specjalnej uwagi.
Wyjątek — to specjalny mechanizm, dzięki któremu .NET informuje twój program, że wydarzyło się coś nieoczekiwanego i wykonanie powinno zostać przerwane albo obsłużone w specjalny sposób.
Kiedy pojawia się błąd wykonania (np. dzielenie przez zero, próba dostępu do nieistniejącego pliku, albo — stary znajomy — dereferencja null), .NET "rzuca" (throw) specjalny obiekt-wyjątek. To prowadzi do szukania handlera, który potrafi "złapać" (catch) sytuację i wie, jak zareagować.
Dlaczego nie zwracać "kodu błędu"?
Można by zwracać kod błędu — i wiele starożytnych języków (Pascal, C, a nawet niektóre współczesne API) tak robi. Ale to niewygodne i niebezpieczne: łatwo zapomnieć sprawdzić kod błędu (wszyscy liczymy na najlepsze) — i bardzo trudno zrozumieć, gdzie dokładnie coś poszło nie tak. Wyjątki pozwalają centralnie śledzić wszelkie błędy i reagować na nie elastycznie, nie zaśmiecając głównego kodu masą ifów.
2. "Rzucanie" wyjątku
Wyjątki mogą pojawić się automatycznie — przy błędzie środowiska wykonawczego, albo być rzucone ręcznie za pomocą słowa kluczowego throw:
int[] arr = new int[2];
arr[10] = 5; // wyjątek poleci sam System.IndexOutOfRangeException
throw new Exception("Absolutna katastrofa!"); // ręcznie rzucamy wyjątek Exception
Nawet jeśli wydaje ci się, że twój program jest idealnie zaprojektowany, zawsze może się wydarzyć coś niespodziewanego: użytkownik zamknął plik, internet padł, ktoś wyłączył zasilanie komputera (no prawie).
Na szczęście .NET dał nam nie tylko mechanizm "rzucania" wyjątków, ale też narzędzia do ich łapania i obsługi — tym zajmiemy się za chwilę.
3. Cykl życia wyjątku: od throw do catch
Kiedy w kodzie dzieje się coś nieprzyjemnego, .NET uruchamia "awaryjną procedurę":
- Tworzy obiekt wyjątku (np. IndexOutOfRangeException).
- Rzuca go za pomocą słowa kluczowego throw.
- Szukaj handlera: przegląda stos wywołań od dołu do góry (od bieżącej metody do wywołującej) — szuka pierwszego bloku catch, który pasuje do typu rzuconego wyjątku.
- Jeśli znajdzie handler — program kontynuuje wykonanie wewnątrz bloku catch.
- Jeśli nie znajdzie żadnego handlera — program kończy się awaryjnie i wyświetla "stos wywołań" z detalami błędu.
Wygląda to jak napięty lot piłki po schodach — dopóki nie trafi na odbiorcę, może długo "skakać" po stosie.
4. Hierarchia wyjątków w .NET
Drzewo "ojców i dzieci"
W .NET (jak w większości języków OOP) wyjątki są zrobione jako klasy, które dziedziczą po sobie i tworzą całą hierarchię.
Wszystkie mają wspólnego przodka — bazową klasę System.Exception.
System.Object
└─ System.Exception
├─ System.SystemException
│ ├─ System.NullReferenceException
│ ├─ System.IndexOutOfRangeException
│ ├─ System.DivideByZeroException
│ ├─ System.OutOfMemoryException
│ └─ ... i wiele innych
├─ System.IO.IOException
│ ├─ System.IO.FileNotFoundException
│ ├─ System.IO.DirectoryNotFoundException
│ └─ ...
├─ System.ArgumentException
│ ├─ System.ArgumentNullException
│ └─ System.ArgumentOutOfRangeException
└─ (twoje własne Exception-klasy)
Dlaczego klasy?
Dzięki hierarchii klas możesz złapać od razu całą "klasę" problemów — na przykład wszystkie błędy związane z argumentami funkcji (ArgumentException i jego potomkowie). Co więcej, możesz dodawać własne typy błędów (o tym — na kolejnych wykładach).
Najpopularniejsze wyjątki .NET
- NullReferenceException — próba dostępu do metody lub właściwości obiektu, który jest null. Stary znajomy!
- DivideByZeroException — dzielenie przez zero.
- IndexOutOfRangeException — wyjście poza granice tablicy.
- ArgumentException, ArgumentNullException, ArgumentOutOfRangeException — błędne parametry metod.
- FileNotFoundException, IOException — problemy przy pracy z plikami.
- InvalidOperationException — operacja niemożliwa w bieżącym stanie obiektu.
- FormatException — nieprawidłowy format danych (np. próba konwersji "abcd" na liczbę).
5. Przykłady: jak "wystrzeliwują" różne wyjątki
Zobaczmy dla jasności przykłady kodu, które powodują różne wyjątki.
Przykład 1: NullReferenceException
string? str = null;
Console.WriteLine(str.Length); // Tutaj "Bum!" — str jest null
Wynik:
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
Przykład 2: DivideByZeroException
int a = 42;
int b = 0;
int c = a / b; // Niebezpieczne! Dzielenie przez 0
Wynik:
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.
Przykład 3: IndexOutOfRangeException
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[5]); // Nie ma elementu o indeksie 5
Tu ewidentnie są za duże indeksy, a wynik to:
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
Przykład 4: FileNotFoundException
using System.IO;
string content = File.ReadAllText("secret.txt");
Jeśli nie masz pliku o nazwie "secret.txt", dostaniesz mniej więcej taki komunikat:
Unhandled exception. System.IO.FileNotFoundException: Could not find file '/Users/zapp/RiderProjects/ConsoleApp1/ConsoleApp1/bin/Debug/net9.0/secret.txt'.
Przykład 5: FormatException
string input = "trzynascie";
int number = int.Parse(input); // String nie jest liczbą
Wynik:
Unhandled exception. System.FormatException: The input string 'trzynascie' was not in a correct format.
6. Jak wykorzystać tę wiedzę?
Zrozumienie, jak działają wyjątki, jest naprawdę ważne dla każdego programisty. To nie tylko "teoria dla teorii", ale praktyczna umiejętność, która bezpośrednio wpływa na stabilność twoich programów. Kiedy wiesz, jak poprawnie obsługiwać błędy, twój program nie będzie się "wywalał" przy pierwszym lepszym problemie, tylko spokojnie i jasno powie użytkownikowi, co poszło nie tak. To czyni go bardziej niezawodnym, profesjonalnym i po prostu przyjaźniejszym dla ludzi.
I to nie tylko kwestia wygody. Na rozmowach kwalifikacyjnych często pytają o mechanikę obsługi wyjątków, typy Exception i jak w ogóle działa ten mechanizm w .NET. Więc dobre zrozumienie tematu — to też plus do karmy przy szukaniu pracy.
Poza tym, jeszcze nie raz spotkasz się z wyjątkami w prawdziwych projektach. Praca z plikami, siecią, bazami danych, zewnętrznymi bibliotekami i frameworkami — wszystko to mocno korzysta z systemu wyjątków. Dlatego lepiej od razu ogarnąć, jak to działa, żeby potem nie było niespodzianek.
7. Typowe błędy przy pracy z wyjątkami
Błąd nr 1: ignorowanie wyjątków jako takich.
Niektórzy początkujący (a czasem i doświadczeni programiści) traktują wyjątki jak coś szkodliwego albo przeszkadzającego. Albo je całkiem ignorują, albo — co gorsza — opakowują kod w catch { } i nic nie robią. W efekcie program "po cichu" działa dalej w dziwnym stanie i nie da się dojść, dlaczego coś się wywaliło.
Błąd nr 2: mylenie wyjątków ze zwykłymi błędami.
Często zapomina się, że nie każdy błąd trzeba łapać przez try-catch. Na przykład, jeśli użytkownik wpisał złe dane, lepiej sprawdzić je ręcznie, a nie liczyć na FormatException i tym bardziej nie używać wyjątków jako mechanizmu logiki. Wyjątki są od wyjątkowych przypadków, a nie od rutyny.
Błąd nr 3: złe rozumienie hierarchii wyjątków.
Czasem ktoś próbuje łapać zbyt ogólną klasę (Exception) i łapie wszystko, nawet te błędy, których lepiej nie ruszać. A czasem odwrotnie — łapie zbyt wąskie typy (IndexOutOfRangeException, NullReferenceException), zapominając, że mogą się pojawić też inne. Ważne jest, żeby rozumieć, jak działa hierarchia wyjątków w .NET i co dokładnie chcesz obsłużyć.
I najważniejsze — pamiętaj: wyjątki w C# to nie awaria i nie sygnał "wszystko stracone", tylko po prostu mechanizm przełączania na specjalny scenariusz wykonania. To potężne narzędzie, jeśli używasz go z głową.
GO TO FULL VERSION