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 null referencí (NullReferenceException)
  • Přístup k neexistujícímu souboru (FileNotFoundException)

Typy výjimek

Výjimky lze rozdělit na dva základní typy:

  1. Systémově vyhozené výjimky
  2. 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í DivideByZeroException

2. 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);
}