Výjimky slouží k signalizaci a zpracování chybových stavů, které mohou nastat při běhu programu. Umožňují oddělit běžnou logiku od ošetření chyb, což přispívá k vyšší čitelnosti a udržitelnosti kódu.
Výjimka (exception) je speciální objekt, který představuje chybový stav. Výjimky vznikají v situacích, kdy program není schopen dokončit operaci podle očekávání.
Například:
- Dělení nulou (
DivideByZeroException) - Přístup mimo rozsah pole (
IndexOutOfRangeException) - Pokus o práci se
nullreferencí (NullReferenceException) - Přístup k neexistujícímu souboru (
FileNotFoundException)
Typy výjimek
Výjimky lze rozdělit na dva základní typy:
- Systémově vyhozené výjimky
- Uživatelem vyhozené výjimky
1. Systémově vyhozené výjimky
Jedná se o výjimky, které vyhazuje samotný běhový systém CLR (Common Language Runtime) nebo knihovny .NET při chybových stavech. Např.:
int x = 10;
int y = 0;
int z = x / y; // Vyhodí DivideByZeroException2. Uživatelem vyhozené výjimky
Programátor může vlastní výjimku vytvořit a vyhodit ji pomocí klíčového slova throw. To se používá, pokud je potřeba definovat vlastní chybový stav, na který se chce někde v aplikaci reagovat.
if (age < 0)
{
throw new ArgumentException("Věk nemůže být záporný");
}Vytvoření vlastní výjimky
Vlastní výjimka se vytváří jako třída dědící ze základní třídy Exception nebo některé z jejích potomků (např. ArgumentException):
public class NegativeAgeException : Exception
{
public NegativeAgeException(string message) : base(message)
{
}
}Ukázka použití:
if (age < 0)
{
throw new NegativeAgeException("Věk nemůže být záporný.");
}Vlastní výjimky
Dle best practices by vývojáři měli tvořit vlastní (doménově specifické) výjimky a nevyhazovat obecné výjimky poskytované .NET runtime nebo knihovnami.
Co se děje při vyhození výjimky?
Když se v C# programu vyhodí výjimka (např. dělením nulou nebo příkazem throw), CLR (Common Language Runtime) okamžitě přeruší běžné provádění aktuální metody. Výjimka se stává objektem, který obsahuje podrobnosti o chybě (typ výjimky, zpráva, stack trace).
Propagace výjimky
Pokud v místě, kde výjimka vznikla, není obklopena blokem try-catch, začne výjimka propadat nahoru zásobníkem volání (call stack).
To znamená:
- metoda, ve které výjimka vznikla, je okamžitě přerušena
- řízení programu se přesune do metody, která ji zavolala
- pokud ani tam není
try-catch, výjimka putuje dál - pokračuje to až k metodě
Main
Například:
void MethodA()
{
MethodB();
}
void MethodB()
{
MethodC();
}
void MethodC()
{
int x = 10;
int y = 0;
int z = x / y; // Vyhodí DivideByZeroException
}Pokud žádná z metod A, B nebo C neobsahuje try-catch, výjimka se propaguje až ven z Main.
Co se stane, když výjimka „probublá“ mimo metodu Main?
Pokud výjimka projde až mimo metodu Main (tj. není odchycena vůbec), běhové prostředí ji zachytí jako neobslouženou výjimku:
- Konzolová aplikace zpravidla vypíše stack trace a ukončí program s chybovým návratovým kódem.
- GUI aplikace (např. WinForms, WPF) může vyvolat chybové okno nebo aplikaci ukončit.
- Webové aplikace skončí chybovým HTTP stavem 500.
Typický výpis v konzoli při neobsloužené výjimce:
Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
at Program.MethodC() in Program.cs:line 10
at Program.MethodB() in Program.cs:line 6
at Program.MethodA() in Program.cs:line 2
at Program.Main() in Program.cs:line 1
Tldr
Výjimky tedy vždy je třeba odchytit a správně vyřešit.
Ošetření výjimek
Pro ošetření výjimek slouží konstrukce try/catch a volitelně finally.
Význam
- try
- Obsahuje kód, který může vyvolat výjimku.
- Pokud výjimka nastane, běh se přeruší a pokračuje v
catch.
- catch
- Odchytí konkrétní typ výjimky.
- Může odchytávat více typů výjimek. Vždy musí být umisťovány od nejkonkrétnějších k nejobecnějším.
- Pokud není výjimka zachycena, propadá výš po stacku.
- finally
- Vždy se vykoná, ať už výjimka nastala nebo ne.
- Slouží k uvolnění prostředků (např. uzavření souborů).
string path = "data.txt";
StreamReader reader = null;
try
{
reader = new StreamReader(path);
string content = reader.ReadToEnd();
Console.WriteLine("Obsah souboru:");
Console.WriteLine(content);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("Chyba: Soubor nebyl nalezen.");
}
catch (IOException ex)
{
Console.WriteLine("Chyba při práci se souborem: " + ex.Message);
}
finally
{
if (reader != null)
{
reader.Close();
Console.WriteLine("Soubor byl uzavřen.");
}
}Alternativní přístupy k signalizaci chyb
Vyhazování výjimek a jejich následné odchytávání je násobně pomalejší než kontrola stavu přes if a práce s chybovými stavy. Takže pokud je důležitý výkon, tak je se upřednostňuje využití alternativních způsobů popsaných níže.
1. Návratová hodnota
bool TryParseAge(string input, out int age)
{
return int.TryParse(input, out age);
}- Výhoda: efektivita. Nevýhoda: nutnost kontroly návratových hodnot.
- Nevýhoda: chybí kontext chyby. Není možné zaznamenat dodatečné informace.
2. Result pattern
Použití návratového typu, který reprezentuje buď výsledek, nebo chybu (případně více variant):
public record Result<T>(T? Value, string? Error);
Result<int> ParseAge(string input)
{
if (!int.TryParse(input, out int age))
return new(0, "Neplatný vstup");
return new(age, null);
}