1. Wprowadzenie
W idealnym świecie kod powinien być idealny. Ale niestety, nawet najbardziej ogarnięty programista kiedyś natknie się na niespodziewane sytuacje: nie znaleziono pliku, użytkownik wpisał tekst zamiast liczby, sieć nagle padła, albo coś wróciło z bazy danych nie tak, jak się spodziewaliśmy.
Jeśli takich problemów nie łapać — program po prostu się "wywali", czasem z tajemniczym komunikatem o błędzie i stack trace (stosem wywołań). To nie jest to, czego oczekuje użytkownik albo przyszły dział wsparcia. Trzeba nauczyć się "łapać" takie błędy i reagować na nie: spokojnie zakończyć działanie, wyświetlić zrozumiały komunikat albo nawet naprawić sytuację na bieżąco.
try-catch — to sposób, żeby powiedzieć kompilatorowi i środowisku uruchomieniowemu: "Spróbuj wykonać ten kawałek kodu. Jeśli pojawi się problem, nie panikuj, daj mi szansę ogarnąć!"
2. Podstawowa składnia try-catch
Ogólna struktura jest bardzo prosta:
try
{
// Tutaj piszemy niebezpieczny (albo potencjalnie niebezpieczny) kod
}
catch (ExceptionType variableName)
{
// Tutaj piszemy, co zrobić, jeśli pojawi się wyjątek typu ExceptionType
}
- Blok try — to "strefa zagrożenia". Wrzucamy tam operacje, które potencjalnie mogą "wystrzelić".
- Blok catch — łapie wyjątki określonego typu. Jeśli w środku try pojawi się wyjątek, sterowanie od razu przeskoczy do pierwszego pasującego catch, a kod po "problemowym miejscu" w try już się nie wykona.
Przykład: prosta obsługa wyjątku
Załóżmy, że nasz mini-kalkulator z poprzednich wykładów teraz umie dzielić liczby. Ale użytkownik może wpisać zero! Zobaczmy, jak to działa bez obsługi błędów:
int a = 10;
int b = 0;
int result = a / b; // BUM! System.DivideByZeroException
Console.WriteLine(result);
Program zakończy się awaryjnie z błędem DivideByZeroException:
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.
Teraz "naprawimy" to przy pomocy try-catch:
int a = 10;
int b = 0;
try
{
int result = a / b; // niebezpieczna operacja
Console.WriteLine("Wynik dzielenia: " + result);
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Ups! Dzielenie przez zero jest niemożliwe: " + ex.Message);
}
Teraz program się nie "wywali". W konsoli pojawi się komunikat o niemożności dzielenia przez zero:
Ups! Dzielenie przez zero jest niemożliwe: Attempted to divide by zero.
3. Jak działa try-catch: krok po kroku
Tak naprawdę w środku try może być dowolnie dużo linii kodu. Jeśli któraś z akcji w bloku wyrzuci wyjątek, sterowanie od razu przeskakuje do najbliższego pasującego catch. Wszystko, co napisane po miejscu błędu w bloku try, nie zostanie wykonane.
Zobaczmy większy przykład:
try
{
Console.WriteLine("Zaczynamy...");
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[1]); // to ok
Console.WriteLine(numbers[5]); // błąd: indeks poza zakresem tablicy
Console.WriteLine("Ten komunikat już się nie pojawi!");
}
catch (IndexOutOfRangeException)
{
Console.WriteLine("Błąd: Próba dostępu do nieistniejącego elementu tablicy");
}
Wynik w konsoli będzie taki:
Zaczynamy...
2
Błąd: Próba dostępu do nieistniejącego elementu tablicy
Process finished with exit code 0.
Jak działa ten program?
- Pojawia się komunikat "Zaczynamy...".
- Potem program próbuje wyświetlić element numbers[1] — to 2.
- Kiedy dochodzi do numbers[5], następuje "awaria", przy której "wyrzuci się" IndexOutOfRangeException.
- Program od razu przeskakuje do bloku catch, pomijając Console.WriteLine("Ten komunikat już się nie pojawi!");
- W konsoli pojawi się nasz komunikat z catch.
4. Czy można obsługiwać różne typy błędów na różne sposoby?
Tak! Możemy podać kilka bloków catch, żeby różnie reagować na różne typy wyjątków. To wygodne: na przykład, jeśli jedna akcja dotyczy czytania pliku (może być FileNotFoundException), a inna — dzielenia (może być DivideByZeroException).
try
{
// Twój niebezpieczny kod
}
catch (DivideByZeroException)
{
Console.WriteLine("Błąd dzielenia przez zero");
}
catch (IndexOutOfRangeException)
{
Console.WriteLine("Błąd: indeks poza zakresem tablicy");
}
catch (Exception ex)
{
Console.WriteLine("Jakiś nieprzewidziany błąd: " + ex.Message);
}
Tutaj ostatni blok catch z typem Exception — to taki "uniwersalny łapacz". Złapie wszystkie inne wyjątki, które nie pasowały do poprzednich bloków. Ale pamiętaj: jeśli napiszesz go jako pierwszy, reszta już się nie wykona! Zawsze dawaj "szerokie" catch na końcu.
5. Jak wygląda obiekt wyjątku?
W środku bloku catch możemy opcjonalnie podać zmienną — na przykład, catch (Exception ex). Ona zawiera wszystkie info o błędzie: komunikat, typ błędu, stack trace, zagnieżdżone wyjątki.
Oto mały przykład:
try
{
string? text = null;
Console.WriteLine(text.Length); // Opa! NullReferenceException
}
catch (NullReferenceException ex)
{
Console.WriteLine("Złapano wyjątek: " + ex.Message);
Console.WriteLine("Stack trace: " + ex.StackTrace);
}
Takie podejście — to zbawienie przy debugowaniu trudnych błędów: zawsze możesz zobaczyć, gdzie i dlaczego program się "wywrócił".
6. Łapiemy błąd i lecimy dalej
W prawdziwych, zwłaszcza użytkowych, aplikacjach błąd — to nie koniec świata. No ktoś wpisał złą ścieżkę do pliku, zdarza się. To jeszcze nie powód, żeby program wpadał w panikę. Zamiast tego spokojnie prosimy go, żeby spróbował jeszcze raz.
Poniższy przykład pokazuje prosty, ale skuteczny sposób: opakowujemy czytanie pliku w pętlę, i jeśli coś pójdzie nie tak, po prostu informujemy o błędzie i prosimy o ponowną próbę. Bez paniki, bez czerwonych ekranów.
bool success = false;
while (!success)
{
Console.Write("Podaj nazwę pliku: ");
string fileName = Console.ReadLine() ?? "";
try
{
string content = File.ReadAllText(fileName);
Console.WriteLine("Plik został pomyślnie odczytany!");
success = true; // HURA!
}
catch (FileNotFoundException)
{
Console.WriteLine("Błąd: plik nie znaleziony. Spróbuj jeszcze raz.");
}
}
Tutaj kluczowa idea — nie tylko złapać wyjątek, ale ogarnąć go z głową: nie pozwalamy programowi się wywalić, tylko przełączamy go w tryb "spróbuj jeszcze raz, ale poprawnie". Użytkownik dostaje jasną podpowiedź, program — drugą szansę, a ty — wdzięczność i szacunek.
7. Typowe błędy przy pracy z try-catch
Błąd nr 1: Opakowywanie całego kodu w jeden uniwersalny try-catch.
To wygląda kusząco — jeden try, jeden catch (Exception), i niby "wszystko chronione". Ale w praktyce tracisz kontrolę nad sytuacją. Nie wiadomo, gdzie dokładnie był problem i na jaki błąd tak naprawdę reagujesz. W efekcie, zamiast niezawodności — totalna utrata diagnostyki.
Błąd nr 2: Łapanie wyjątków i nic z nimi nie robienie.
Przykład z życia:
try
{
// coś potencjalnie niebezpiecznego
}
catch
{
// cisza...
}
Taki kod dosłownie mówi: "Coś poszło nie tak... no i trudno". To niebezpieczne: program może działać dalej w złym stanie, a ty nawet nie wiesz, że był błąd. Zawsze przynajmniej wyświetl komunikat albo zaloguj problem. Żadnych pustych
catch!
Błąd nr 3: Łapanie zbyt ogólnego typu wyjątku.
Jeśli łapiesz Exception, chociaż wiesz, że może się pojawić na przykład FormatException albo FileNotFoundException, — odbierasz sobie możliwość sensownej reakcji na konkretny problem. Im węższy i dokładniejszy twój catch, tym bardziej przewidywalnie i sensownie zachowuje się program w nietypowej sytuacji.
8. Do czego NIE zaleca się używać wyjątków
Czasem początkujący zaczynają używać wyjątków... jako zwykłego mechanizmu sterowania logiką, na przykład:
try
{
if (x < 0) throw new Exception("x musi być >= 0");
}
catch (Exception)
{
// cofnięcie stanu, kontynuacja pracy...
}
Tak nie rób! Wyjątki — to narzędzie do nieoczekiwanych, rzadkich sytuacji (błąd odczytu pliku, padnięcie sieci itd.), a nie do zwykłych sprawdzeń poprawności danych użytkownika. Do takich rzeczy używaj instrukcji warunkowych (if, else) i zwracaj specjalne wartości, jeśli coś jest nie tak.
GO TO FULL VERSION