Wyjątki w C# – obsługa błędów
Poznasz mechanizm wyjątków – jak przechwytywać błędy, reagować na nie i tworzyć odporne aplikacje. Nauczysz się używać try/catch/finally oraz rzucać własne wyjątki!
Problem – program się wysypuje
Co się stanie, gdy użytkownik wpisze literę zamiast liczby? Gdy podzielisz przez zero? Gdy plik nie istnieje? Program się wysypie!
// 😰 Program bez obsługi błędów Console.Write("Podaj liczbę: "); string input = Console.ReadLine(); // Co jeśli użytkownik wpisze "abc"? int liczba = int.Parse(input); // 💥 CRASH! Console.WriteLine($"Wpisałeś: {liczba}"); // Wynik dla "abc": // Unhandled exception. System.FormatException: // Input string was not in a correct format.
Bez obsługi błędów każdy nieprzewidziany przypadek kończy działanie programu. Użytkownik widzi czerwony komunikat błędu i program się zamyka.
Typowe przyczyny błędów
// 1. Nieprawidłowy format danych int x = int.Parse("abc"); // 💥 FormatException // 2. Dzielenie przez zero int wynik = 10 / 0; // 💥 DivideByZeroException // 3. Brak pliku string txt = File.ReadAllText("nieistnieje.txt"); // 💥 FileNotFoundException // 4. Null reference string s = null; int len = s.Length; // 💥 NullReferenceException // 5. Indeks poza tablicą int[] arr = { 1, 2, 3 }; int v = arr[10]; // 💥 IndexOutOfRangeException
Czym jest wyjątek?
Wyjątek (exception) to obiekt reprezentujący błąd, który wystąpił podczas działania programu. Gdy coś pójdzie nie tak, system „rzuca” wyjątek.
Wyobraź sobie budynek biurowy:
- Normalne działanie = ludzie pracują
- Wyjątek (pożar) = alarm się włącza
- Obsługa wyjątku = procedura ewakuacji
Bez procedury (catch) – panika! Z procedurą – kontrolowana reakcja.
- throw = rzucasz piłkę (zgłaszasz problem)
- catch = łapiesz piłkę (obsługujesz problem)
- Jeśli nikt nie złapie – piłka uderza w ziemię (crash!)
try/catch – podstawy
Blok try/catch pozwala przechwycić wyjątek i zareagować na błąd zamiast pozwolić programowi się wysypać.
Console.Write("Podaj liczbę: "); string input = Console.ReadLine(); try { // Kod który MOŻE rzucić wyjątek int liczba = int.Parse(input); Console.WriteLine($"Wpisałeś: {liczba}"); } catch { // Kod wykonywany GDY wystąpi błąd Console.WriteLine("To nie jest poprawna liczba!"); } Console.WriteLine("Program działa dalej..."); // Dla "abc": // To nie jest poprawna liczba! // Program działa dalej...
- Kod w
tryjest wykonywany normalnie - Jeśli wystąpi błąd → skok do
catch - Jeśli NIE ma błędu →
catchjest pomijany - Program kontynuuje po bloku try/catch
Przepływ sterowania
try { int x = int.Parse("42"); // OK! Console.WriteLine($"Liczba: {x}"); // Wykonane } catch { Console.WriteLine("Błąd!"); // POMINIĘTE } // Wynik: // Liczba: 42
Obiekt wyjątku
Wyjątek to obiekt klasy Exception. Możesz go przechwycić i odczytać informacje o błędzie.
try { int x = int.Parse("abc"); } catch (Exception ex) // Przechwytujemy obiekt wyjątku { Console.WriteLine($"Typ: {ex.GetType().Name}"); Console.WriteLine($"Komunikat: {ex.Message}"); Console.WriteLine($"Stack trace: {ex.StackTrace}"); } // Wynik: // Typ: FormatException // Komunikat: Input string was not in a correct format. // Stack trace: at System.Number.ThrowOverflowOrFormat...
| Właściwość | Opis | Przykład |
|---|---|---|
Message | Opis błędu | „Input string was not in a correct format.” |
StackTrace | Gdzie wystąpił błąd | Linia kodu, metoda, plik |
GetType() | Typ wyjątku | FormatException |
InnerException | Wyjątek zagnieżdżony | Przyczyna głębsza |
Source | Źródło błędu | Nazwa assembly |
Console.Write("Podaj wiek: "); string input = Console.ReadLine(); try { int wiek = int.Parse(input); Console.WriteLine($"Za 10 lat będziesz mieć {wiek + 10} lat"); } catch (Exception ex) { // Przyjazny komunikat dla użytkownika Console.WriteLine("❌ Podaj poprawną liczbę całkowitą!"); // Szczegóły dla programisty (np. do logów) Console.WriteLine($"[DEBUG] {ex.GetType().Name}: {ex.Message}"); }
Typy wyjątków
W .NET istnieje wiele typów wyjątków. Każdy opisuje inny rodzaj błędu.
| Wyjątek | Kiedy występuje | Przykład |
|---|---|---|
FormatException | Nieprawidłowy format danych | int.Parse("abc") |
DivideByZeroException | Dzielenie przez zero | 10 / 0 |
NullReferenceException | Użycie null | null.ToString() |
IndexOutOfRangeException | Indeks poza zakresem | arr[100] |
FileNotFoundException | Brak pliku | File.ReadAllText("brak.txt") |
ArgumentException | Nieprawidłowy argument | Przekazano pusty string |
ArgumentNullException | Argument jest null | Przekazano null |
InvalidOperationException | Nieprawidłowa operacja | Operacja w złym stanie |
OverflowException | Przepełnienie | Liczba za duża |
IOException | Błąd I/O | Problem z plikiem/siecią |
Hierarchia wyjątków
Exception ← Klasa bazowa ├── SystemException │ ├── FormatException │ ├── DivideByZeroException │ ├── NullReferenceException │ ├── IndexOutOfRangeException │ ├── ArgumentException │ │ └── ArgumentNullException │ ├── InvalidOperationException │ └── OverflowException ├── IOException │ └── FileNotFoundException └── ApplicationException ← Własne wyjątki (stare podejście)
Wyjątki tworzą hierarchię klas. catch (Exception) łapie wszystkie wyjątki, bo każdy dziedziczy po Exception.
Wiele bloków catch
Możesz obsłużyć różne typy błędów w różny sposób, używając wielu bloków catch.
Console.Write("Podaj dzielnik: "); string input = Console.ReadLine(); try { int dzielnik = int.Parse(input); int wynik = 100 / dzielnik; Console.WriteLine($"100 / {dzielnik} = {wynik}"); } catch (FormatException) { Console.WriteLine("❌ To nie jest liczba!"); } catch (DivideByZeroException) { Console.WriteLine("❌ Nie można dzielić przez zero!"); } catch (Exception ex) // Catch-all na końcu { Console.WriteLine($"❌ Nieznany błąd: {ex.Message}"); }
Bloki catch są sprawdzane od góry do dołu. Bardziej szczegółowe wyjątki muszą być PRZED ogólnymi!
❌ Źle – ogólny na początku
catch (Exception ex) // Łapie WSZYSTKO!
{
}
catch (FormatException) // Nigdy nie wykona!
{
}
✅ Dobrze – szczegółowy najpierw
catch (FormatException) // Specyficzny
{
}
catch (Exception ex) // Ogólny na końcu
{
}
Łączenie wyjątków (C# 6+)
try { // kod } // Łączenie typów (C# 6+) catch (Exception ex) when (ex is FormatException or OverflowException) { Console.WriteLine("Problem z liczbą!"); } // Filtr when catch (Exception ex) when (ex.Message.Contains("timeout")) { Console.WriteLine("Przekroczono czas!"); }
Blok finally
Blok finally wykonuje się ZAWSZE – niezależnie czy wystąpił błąd czy nie. Służy do sprzątania zasobów.
StreamReader reader = null; try { reader = new StreamReader("dane.txt"); string content = reader.ReadToEnd(); Console.WriteLine(content); } catch (FileNotFoundException) { Console.WriteLine("Plik nie istnieje!"); } finally { // ZAWSZE się wykona – zamknij plik! if (reader != null) { reader.Close(); Console.WriteLine("Plik zamknięty."); } }
- Zamykanie plików
- Zamykanie połączeń z bazą danych
- Zwalnianie zasobów sieciowych
- Czyszczenie obiektów tymczasowych
Przepływ z finally
try/finally bez catch
// Można użyć try/finally BEZ catch // Wyjątek poleci wyżej, ale finally się wykona try { // Otwórz zasób // Użyj zasobu (może rzucić wyjątek) } finally { // Zamknij zasób (ZAWSZE) }
W C# lepszą praktyką jest używanie using zamiast try/finally dla zasobów:
using (StreamReader reader = new StreamReader("plik.txt"))
{
// reader zostanie automatycznie zamknięty
}
Rzucanie wyjątków (throw)
Możesz sam rzucić wyjątek używając throw. To sposób sygnalizowania błędu w swoim kodzie.
public class Osoba { private int _wiek; public int Wiek { get { return _wiek; } set { if (value < 0) { throw new ArgumentException("Wiek nie może być ujemny!"); } if (value > 150) { throw new ArgumentException("Wiek nie może przekraczać 150!"); } _wiek = value; } } } // Użycie try { Osoba o = new Osoba(); o.Wiek = -5; // 💥 ArgumentException! } catch (ArgumentException ex) { Console.WriteLine(ex.Message); // "Wiek nie może być ujemny!" }
Popularne wyjątki do rzucania
| Wyjątek | Kiedy rzucać | Przykład |
|---|---|---|
ArgumentException | Nieprawidłowy argument | Wiek ujemny |
ArgumentNullException | Argument jest null | Nazwa = null |
ArgumentOutOfRangeException | Argument poza zakresem | Indeks < 0 |
InvalidOperationException | Nieprawidłowy stan | Metoda wywołana w złym momencie |
NotImplementedException | Metoda niezaimplementowana | TODO |
NotSupportedException | Operacja niewspierana | Tylko do odczytu |
public class Konto { public decimal Saldo { get; private set; } public void Wplac(decimal kwota) { if (kwota <= 0) throw new ArgumentException("Kwota musi być dodatnia", nameof(kwota)); Saldo += kwota; } public void Wyplac(decimal kwota) { if (kwota <= 0) throw new ArgumentException("Kwota musi być dodatnia", nameof(kwota)); if (kwota > Saldo) throw new InvalidOperationException("Brak wystarczających środków"); Saldo -= kwota; } }
Ponowne rzucanie (rethrow)
Możesz przechwycić wyjątek, wykonać akcję (np. logowanie) i rzucić go dalej.
public void PrzetworzPlik(string sciezka) { try { string content = File.ReadAllText(sciezka); // przetwarzanie... } catch (Exception ex) { // Zaloguj błąd Console.WriteLine($"[LOG] Błąd: {ex.Message}"); // Rzuć dalej (zachowuje stack trace!) throw; } }
❌ Źle – gubi stack trace
catch (Exception ex)
{
throw ex; // Resetuje stack trace!
}
✅ Dobrze – zachowuje stack trace
catch (Exception ex)
{
throw; // Zachowuje oryginalny stack trace
}
Opakowywanie wyjątku
public void ZapiszDane(string dane) { try { File.WriteAllText("dane.txt", dane); } catch (IOException ex) { // Opakuj w własny wyjątek z dodatkowym kontekstem throw new InvalidOperationException( "Nie udało się zapisać danych", ex); // ex = InnerException } }
Własne wyjątki
Możesz tworzyć własne klasy wyjątków dziedziczące po Exception. Daje to precyzyjniejszą obsługę błędów.
// Własny wyjątek public class BrakSrodkowException : Exception { public decimal KwotaBrakujaca { get; } public BrakSrodkowException(decimal brakuje) : base($"Brakuje {brakuje:C} na koncie") { KwotaBrakujaca = brakuje; } } // Użycie w klasie public class Konto { public decimal Saldo { get; private set; } = 100; public void Wyplac(decimal kwota) { if (kwota > Saldo) { decimal brakuje = kwota - Saldo; throw new BrakSrodkowException(brakuje); } Saldo -= kwota; } } // Obsługa try { Konto k = new Konto(); k.Wyplac(500); } catch (BrakSrodkowException ex) { Console.WriteLine(ex.Message); Console.WriteLine($"Doładuj konto o: {ex.KwotaBrakujaca:C}"); }
Nazwa własnego wyjątku powinna kończyć się na Exception:
BrakSrodkowExceptionNieznanyUzytkownikExceptionLimitPrzekroczonyException
Pełna implementacja (z konstruktorami)
public class WalidacjaException : Exception { public string NazwaPola { get; } // Konstruktor domyślny public WalidacjaException() : base() { } // Z komunikatem public WalidacjaException(string message) : base(message) { } // Z komunikatem i nazwą pola public WalidacjaException(string message, string nazwaPola) : base(message) { NazwaPola = nazwaPola; } // Z inner exception public WalidacjaException(string message, Exception inner) : base(message, inner) { } }
Wzorce użycia
Wzorzec 1: Pętla z ponawianiem
int liczba; bool poprawne = false; while (!poprawne) { Console.Write("Podaj liczbę (1-100): "); try { liczba = int.Parse(Console.ReadLine()); if (liczba < 1 || liczba > 100) throw new ArgumentOutOfRangeException(); poprawne = true; } catch (FormatException) { Console.WriteLine("To nie jest liczba!"); } catch (ArgumentOutOfRangeException) { Console.WriteLine("Liczba musi być z zakresu 1-100!"); } } Console.WriteLine($"Dziękuję! Wybrałeś: {liczba}");
Wzorzec 2: TryParse zamiast wyjątku
// ❌ Wolniejsze – wyjątek jest kosztowny try { int x = int.Parse(input); } catch (FormatException) { } // ✅ Szybsze – bez wyjątku if (int.TryParse(input, out int x)) { Console.WriteLine($"Liczba: {x}"); } else { Console.WriteLine("To nie jest liczba"); }
int.TryParse()– zwraca true/false, nie rzuca wyjątkuint.Parse()– rzuca FormatException przy błędzie- Używaj TryParse gdy błąd jest oczekiwany (dane od użytkownika)
- Używaj Parse gdy błąd jest wyjątkowy
Wzorzec 3: Obsługa plików
public static string OdczytajBezpiecznie(string sciezka) { try { return File.ReadAllText(sciezka); } catch (FileNotFoundException) { Console.WriteLine($"Plik nie istnieje: {sciezka}"); return null; } catch (UnauthorizedAccessException) { Console.WriteLine($"Brak dostępu do pliku: {sciezka}"); return null; } catch (IOException ex) { Console.WriteLine($"Błąd odczytu: {ex.Message}"); return null; } }
Dobre praktyki
| ✅ Rób | ❌ Nie rób |
|---|---|
| Łap konkretne wyjątki | Łapać wszystko przez catch (Exception) |
| Używaj TryParse dla danych użytkownika | Używać wyjątków do kontroli przepływu |
| Loguj szczegóły wyjątku | Ignorować wyjątki (pusty catch) |
Używaj throw; do ponownego rzucenia |
Pisać throw ex; – gubi stack trace |
| Zwalniaj zasoby w finally lub using | Zostawiać otwarte pliki/połączenia |
| Twórz sensowne komunikaty | Pisać „Wystąpił błąd” |
Rzucanie wyjątku jest wolne – wymaga budowania stack trace. Nie używaj wyjątków do normalnego przepływu programu!
// ❌ ZŁE – wyjątek do kontroli przepływu
try {
int x = lista[index];
} catch (IndexOutOfRangeException) { }
// ✅ DOBRE – sprawdzenie warunku
if (index >= 0 && index < lista.Count) {
int x = lista[index];
}
Częste błędy
❌ Błąd 1: Pusty catch
❌ Źle – połykanie błędu
try
{
// kod
}
catch
{
// Nic nie robimy = ZŁO!
}
✅ Dobrze – reakcja na błąd
try
{
// kod
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
// lub logowanie
}
❌ Błąd 2: Zbyt ogólny catch
❌ Źle
try
{
// wiele operacji
}
catch (Exception)
{
Console.WriteLine("Błąd!");
// Który? Nie wiadomo!
}
✅ Dobrze
try
{
// operacje
}
catch (FileNotFoundException)
{
Console.WriteLine("Brak pliku");
}
catch (FormatException)
{
Console.WriteLine("Zły format");
}
❌ Błąd 3: throw ex zamiast throw
❌ Źle – resetuje stack trace
catch (Exception ex)
{
Log(ex);
throw ex; // ŹRÓDŁO BŁĘDU ZGUBIONE!
}
✅ Dobrze – zachowuje stack trace
catch (Exception ex)
{
Log(ex);
throw; // Stack trace zachowany
}
❌ Błąd 4: Wyjątek zamiast if
❌ Źle – wolne!
try
{
int x = tablica[index];
}
catch (IndexOutOfRangeException)
{
// obsługa
}
✅ Dobrze – szybkie
if (index >= 0 && index < tablica.Length)
{
int x = tablica[index];
}
else
{
// obsługa
}
Podsumowanie
- Exception – obiekt reprezentujący błąd
- try – kod który może rzucić wyjątek
- catch – obsługa wyjątku
- finally – wykonuje się ZAWSZE (sprzątanie)
- throw – rzucanie wyjątku
Schemat składni
// Pełna składnia try/catch/finally try { // Kod który może rzucić wyjątek } catch (SpecyficznyWyjatek ex) { // Obsługa konkretnego typu } catch (Exception ex) { // Catch-all (ostatni) } finally { // Zawsze się wykonuje } // Rzucanie wyjątku throw new ArgumentException("komunikat"); // Własny wyjątek public class MojWyjatek : Exception { public MojWyjatek(string msg) : base(msg) { } }
| Wyjątek | Przyczyna |
|---|---|
FormatException | Nieprawidłowy format (Parse) |
DivideByZeroException | Dzielenie przez zero |
NullReferenceException | Użycie null |
IndexOutOfRangeException | Indeks poza tablicą |
FileNotFoundException | Brak pliku |
ArgumentException | Nieprawidłowy argument |
InvalidOperationException | Zły stan obiektu |
Zadania praktyczne
📝 Zadanie 1: Kalkulator bezpieczny
Napisz prosty kalkulator (dodawanie, odejmowanie, mnożenie, dzielenie) który:
- Pobiera dwie liczby od użytkownika
- Obsługuje błędy: niepoprawny format, dzielenie przez zero
- Pozwala użytkownikowi spróbować ponownie
📝 Zadanie 2: Odczyt pliku konfiguracji
Napisz metodę LoadConfig(string path) która:
- Odczytuje plik tekstowy
- Obsługuje FileNotFoundException, UnauthorizedAccessException
- Zwraca null przy błędzie lub zawartość pliku
📝 Zadanie 3: Walidacja formularza
Utwórz klasę Formularz z metodą Waliduj(string email, int wiek):
- Email musi zawierać "@" i "." – inaczej ArgumentException
- Wiek musi być 0-150 – inaczej ArgumentOutOfRangeException
- Zwraca true jeśli dane są poprawne
⭐ Zadanie 4: Własny wyjątek
Utwórz klasę KontoBankowe z:
- Własnością Saldo
- Metodami Wplac() i Wyplac()
- Własnym wyjątkiem
NiewystarczajaceSrodkiException
💡 Wyjątek powinien zawierać właściwość KwotaBrakujaca
⭐⭐ Zadanie 5: Przetwarzanie listy liczb
Napisz program który:
- Wczytuje liczby od użytkownika (kończy na "koniec")
- Ignoruje niepoprawne wpisy (z komunikatem)
- Na końcu wyświetla: sumę, średnią, min, max
- Obsługuje przypadek pustej listy