Sdílet prostřednictvím


Osvědčené postupy pro výjimky

Správné zpracování výjimek je nezbytné pro spolehlivost aplikací. Očekávané výjimky můžete záměrně zpracovat, abyste zabránili chybovému ukončení aplikace. Aplikace s chybovým ukončením je ale spolehlivější a diagnostikovatelná než aplikace s nedefinovaným chováním.

Tento článek popisuje osvědčené postupy pro zpracování a vytváření výjimek.

Zpracování výjimek

Následující osvědčené postupy se týkají způsobu zpracování výjimek:

Použijte bloky try/catch/finally k zotavení se z chyb nebo uvolnění prostředků.

Pro kód, který může potenciálně vygenerovat výjimku, a když se vaše aplikace může z této výjimky zotavit, použijte try/catch bloků kolem kódu. V catch blocích vždy uspořádejte výjimky od nejvíce odvozených k nejméně odvozeným. (Všechny výjimky jsou odvozeny z Exception třídy. Další odvozené výjimky nejsou zpracovávány klauzulí catch, která předchází klauzuli catch pro základní třídu výjimky.) Pokud se váš kód nemůže obnovit z výjimky, nezachyťte tuto výjimku. Povolte metody vyšší úrovně ve volacím zásobníku, aby se mohly v případě potřeby obnovit.

Vyčistěte prostředky, které byly přiděleny buď pomocí příkazů using, nebo bloků finally. Upřednostněte příkazy using k automatickému vyčištění prostředků při vyvolání výjimek. Pomocí bloků finally vyčistěte prostředky, které neimplementují IDisposable. Kód v klauzuli finally se téměř vždy spustí i v případě, že dojde k vyvolání výjimek.

Zpracování běžných podmínek, aby nedocházelo k výjimkám

U podmínek, u kterých je pravděpodobné, že dojde k výjimce, ale mohou vyvolat výjimku, zvažte jejich zpracování způsobem, který se této výjimce zabrání. Pokud se například pokusíte zavřít připojení, které už je zavřené, zobrazí se InvalidOperationException. Tomu se můžete vyhnout pomocí příkazu if, abyste před pokusem o jeho zavření zkontrolovali stav připojení.

if (conn.State != ConnectionState.Closed)
{
    conn.Close();
}
If conn.State <> ConnectionState.Closed Then
    conn.Close()
End IF

Pokud před zavřením nekontrolujete stav připojení, můžete zachytit výjimku InvalidOperationException.

try
{
    conn.Close();
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.GetType().FullName);
    Console.WriteLine(ex.Message);
}
Try
    conn.Close()
Catch ex As InvalidOperationException
    Console.WriteLine(ex.GetType().FullName)
    Console.WriteLine(ex.Message)
End Try

Způsob výběru závisí na tom, jak často očekáváte výskyt události.

  • Zpracování výjimek použijte, pokud k události často nedojde, to znamená, že pokud je událost skutečně výjimečná a značí chybu, například neočekávaný konec souboru. Při použití zpracování výjimek se v normálních podmínkách spustí méně kódu.

  • Zkontrolujte chybové stavy v kódu, pokud se událost děje rutinně, a je možné ji považovat za součást normálního spuštění. Při kontrole běžných chybových podmínek se spustí méně kódu, protože se vyhnete výjimkám.

    Poznámka

    Počáteční kontroly většinu času eliminují výjimky. Můžou však existovat podmínky souběhu, kdy se chráněný stav změní mezi kontrolou a operací, a v takovém případě může dojít k výjimce.

Zavolejte metody Try*, aby se zabránilo výjimkám.

Pokud jsou náklady na výkon výjimek neúnosně vysoké, některé metody knihovny .NET poskytují alternativní formy zpracování chyb. Například Int32.Parse vyvolá OverflowException, pokud je hodnota, kterou chcete analyzovat, příliš velká, aby byla reprezentována Int32. Int32.TryParse však tuto výjimku nevyvolá. Místo toho vrátí logickou hodnotu a má parametr out, který při úspěchu obsahuje analyzované platné celé číslo. Dictionary<TKey,TValue>.TryGetValue má podobné chování při pokusu o získání hodnoty ze slovníku.

Zachytávání zrušení a asynchronních výjimek

Je lepší zachytit OperationCanceledException místo TaskCanceledException, která je odvozena z OperationCanceledException, při volání asynchronní metody. Mnoho asynchronních metod vyvolá výjimku OperationCanceledException, pokud je požadováno zrušení. Tyto výjimky umožňují efektivní zastavení provádění a rozvinout zásobník volání, jakmile je zaznamenán požadavek na zrušení.

Asynchronní metody ukládají výjimky, které jsou vyvolány během provádění, do úkolu, který vrací. Pokud je výjimka uložena do vráceného úkolu, tato výjimka bude vyvolána při čekání na úkol. Výjimky použití, například ArgumentException, jsou stále vyvolány synchronně. Další informace naleznete v sekci Asynchronní výjimky.

Navrhujte třídy tak, aby se dalo vyhnout výjimkám

Třída může poskytovat metody nebo vlastnosti, které umožňují vyhnout se volání, které by aktivovalo výjimku. Například třída FileStream poskytuje metody, které pomáhají určit, zda byl dosažen konec souboru. Tyto metody můžete volat, abyste se vyhnuli výjimce, která je vyvolána, pokud čtete za koncem souboru. Následující příklad ukazuje, jak číst na konec souboru bez aktivace výjimky:

class FileRead
{
    public static void ReadAll(FileStream fileToRead)
    {
        ArgumentNullException.ThrowIfNull(fileToRead);

        int b;

        // Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin);

        // Read each byte to the end of the file.
        for (int i = 0; i < fileToRead.Length; i++)
        {
            b = fileToRead.ReadByte();
            Console.Write(b.ToString());
            // Or do something else with the byte.
        }
    }
}
Class FileRead
    Public Sub ReadAll(fileToRead As FileStream)
        ' This if statement is optional
        ' as it is very unlikely that
        ' the stream would ever be null.
        If fileToRead Is Nothing Then
            Throw New System.ArgumentNullException()
        End If

        Dim b As Integer

        ' Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin)

        ' Read each byte to the end of the file.
        For i As Integer = 0 To fileToRead.Length - 1
            b = fileToRead.ReadByte()
            Console.Write(b.ToString())
            ' Or do something else with the byte.
        Next i
    End Sub
End Class

Dalším způsobem, jak se vyhnout výjimkám, je vrátit null (nebo výchozí) pro nejběžnější případy chyb místo vyvolání výjimky. Běžný případ chyby lze považovat za normální tok řízení. Vrácením null (nebo výchozí) v těchto případech minimalizujete dopad na výkon aplikace.

U typů hodnot zvažte, jestli jako indikátor chyby aplikace použít Nullable<T> nebo default. Pomocí Nullable<Guid>se default místo nullstává Guid.Empty . Někdy může přidání Nullable<T> pomoci jasněji určit, zda je hodnota přítomná, nebo chybí. Jindy může přidání Nullable<T> vytvořit další případy pro kontrolu, které nejsou nezbytné a slouží pouze k vytvoření potenciálních zdrojů chyb.

Obnovení stavu v případech, kdy se metody nedokončí kvůli výjimkám

Volající by měli být schopni předpokládat, že neexistují žádné vedlejší účinky, pokud je hozena výjimka z metody. Pokud máte například kód, který převádí peníze stažením z jednoho účtu a vkladem na jiný účet a při provádění vkladu dojde k výjimce, nechcete, aby výběr zůstal v platnosti.

public void TransferFunds(Account from, Account to, decimal amount)
{
    from.Withdrawal(amount);
    // If the deposit fails, the withdrawal shouldn't remain in effect.
    to.Deposit(amount);
}
Public Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    from.Withdrawal(amount)
    ' If the deposit fails, the withdrawal shouldn't remain in effect.
    [to].Deposit(amount)
End Sub

Předchozí metoda přímo nevyvolá žádné výjimky. Je však nutné napsat metodu tak, aby se výběr peněz v případě selhání operace vkladu vrátil zpět.

Jedním ze způsobů, jak tuto situaci vyřešit, je zachytit všechny výjimky vyvolané transakcí vkladu a vrátit výběr zpět.

private static void TransferFunds(Account from, Account to, decimal amount)
{
    string withdrawalTrxID = from.Withdrawal(amount);
    try
    {
        to.Deposit(amount);
    }
    catch
    {
        from.RollbackTransaction(withdrawalTrxID);
        throw;
    }
}
Private Shared Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    Dim withdrawalTrxID As String = from.Withdrawal(amount)
    Try
        [to].Deposit(amount)
    Catch
        from.RollbackTransaction(withdrawalTrxID)
        Throw
    End Try
End Sub

Tento příklad ukazuje použití throw k opětovnému vytvoření původní výjimky, což usnadňuje volajícím vidět skutečnou příčinu problému, aniž by museli zkoumat vlastnost InnerException. Alternativou je vyvolání nové výjimky a zahrnutí původní výjimky jako vnitřní výjimky.

catch (Exception ex)
{
    from.RollbackTransaction(withdrawalTrxID);
    throw new TransferFundsException("Withdrawal failed.", innerException: ex)
    {
        From = from,
        To = to,
        Amount = amount
    };
}
Catch ex As Exception
    from.RollbackTransaction(withdrawalTrxID)
    Throw New TransferFundsException("Withdrawal failed.", innerException:=ex) With
    {
        .From = from,
        .[To] = [to],
        .Amount = amount
    }
End Try

Správně zachytávat a znovu vyvolat výjimky

Po vyvolání výjimky je součástí informací, které přenáší, trasování zásobníku. Trasování zásobníku je seznam hierarchie volání metody, která začíná metodou, která vyvolá výjimku a končí metodou, která zachytí výjimku. Pokud znovu vyvoláte výjimku zadáním výjimky v příkazu throw, například throw e, sledování zásobníku se restartuje v aktuální metodě a seznam volání metod mezi původní metodou, která vyvolala výjimku, a aktuální metodou se ztratí. Pokud chcete zachovat původní informace o trasování zásobníku s výjimkou, existují dvě možnosti, které závisí na tom, odkud výjimku znovu vyvoláváte:

  • Pokud znovu vyvoláte výjimku z bloku obslužné rutiny (catch), který zachytil instanci výjimky, použijte příkaz throw bez zadání výjimky. Pravidlo analýzy kódu CA2200 pomáhá najít místa v kódu, kde byste neúmyslně ztratili informace o trasování zásobníku.
  • Pokud přehazujete výjimku z jiného místa než z obslužné rutiny (catch blok), použijte ExceptionDispatchInfo.Capture(Exception) k zachycení výjimky v obslužné rutině a ExceptionDispatchInfo.Throw(), když ji chcete znovu přehodit. K kontrole zachycené výjimky můžete použít vlastnost ExceptionDispatchInfo.SourceException.

Následující příklad ukazuje, jak lze použít třídu ExceptionDispatchInfo a jak může výstup vypadat.

ExceptionDispatchInfo? edi = null;
try
{
    var txt = File.ReadAllText(@"C:\temp\file.txt");
}
catch (FileNotFoundException e)
{
    edi = ExceptionDispatchInfo.Capture(e);
}

// ...

Console.WriteLine("I was here.");

if (edi is not null)
    edi.Throw();

Pokud soubor v ukázkovém kódu neexistuje, vytvoří se následující výstup:

I was here.
Unhandled exception. System.IO.FileNotFoundException: Could not find file 'C:\temp\file.txt'.
File name: 'C:\temp\file.txt'
   at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)
   at System.IO.File.ReadAllText(String path, Encoding encoding)
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 12
--- End of stack trace from previous location ---
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 24

Vyvolání výjimek

Následující osvědčené postupy se týkají způsobu vyvolání výjimek:

Použití předdefinovaných typů výjimek

Zavést novou třídu výjimek pouze v případech, kdy se nepoužívá předdefinovaná třída. Například:

Poznámka

I když je to možné, nejlepší je použít předdefinované typy výjimek, neměli byste vyvolat některé rezervované typy výjimek, jako jsou AccessViolationException, IndexOutOfRangeException, NullReferenceException a StackOverflowException. Další informace naleznete v tématu CA2201: Nevyvolávejte rezervované typy výjimek.

Použití metod tvůrce výjimek

Je běžné, že třída ve své implementaci vyvolává stejnou výjimku z různých míst. Pokud se chcete vyhnout nadměrnému kódu, vytvořte pomocnou metodu, která vytvoří výjimku a vrátí ji. Například:

class FileReader
{
    private readonly string _fileName;

    public FileReader(string path)
    {
        _fileName = path;
    }

    public byte[] Read(int bytes)
    {
        byte[] results = FileUtils.ReadFromFile(_fileName, bytes) ?? throw NewFileIOException();
        return results;
    }

    static FileReaderException NewFileIOException()
    {
        string description = "My NewFileIOException Description";

        return new FileReaderException(description);
    }
}
Class FileReader
    Private fileName As String


    Public Sub New(path As String)
        fileName = path
    End Sub

    Public Function Read(bytes As Integer) As Byte()
        Dim results() As Byte = FileUtils.ReadFromFile(fileName, bytes)
        If results Is Nothing
            Throw NewFileIOException()
        End If
        Return results
    End Function

    Function NewFileIOException() As FileReaderException
        Dim description As String = "My NewFileIOException Description"

        Return New FileReaderException(description)
    End Function
End Class

Některé klíčové typy výjimek v .NET mají statické pomocné metody throw, které přidělují a vyvolávají výjimku. Místo vytváření a vyvolání odpovídajícího typu výjimky byste měli volat tyto metody:

Spropitné

Následující pravidla analýzy kódu vám pomůžou najít místa v kódu, kde můžete využít tyto statické throw pomocné rutiny: CA1510, CA1511, CA1512a CA1513.

Pokud implementujete asynchronní metodu, volejte CancellationToken.ThrowIfCancellationRequested() místo kontroly, zda bylo požadováno zrušení a následného vytvoření a vyvolání OperationCanceledException. Pro více informací viz CA2250.

Zahrňte lokalizovanou textovou zprávu

Chybová zpráva, kterou uživatel uvidí, je odvozena z Exception.Message vlastnosti výjimky, která byla vyvolána, a ne z názvu třídy výjimky. Obvykle můžete přiřadit hodnotu vlastnosti Exception.Message předáním textu zprávy message do argumentu konstruktoru Exception.

U lokalizovaných aplikací byste měli zadat lokalizovaný řetězec zprávy pro každou výjimku, kterou může aplikace vyvolat. Používáte soubory prostředků k poskytování lokalizovaných chybových zpráv. Informace o lokalizaci aplikací a načítání lokalizovaných řetězců najdete v následujících článcích:

Použití správné gramatiky

Napište jasné věty a zahrňte koncovou interpunkci. Každá věta v řetězci přiřazenému k vlastnosti Exception.Message by měla končit tečkou. Například věta "Tabulka protokolu přetekla." používá správnou gramatiku a interpunkci.

Dobře umístit příkazy throw

Umístěte příkazy throw tam, kde trasování zásobníku pomůže. Trasování zásobníku začíná příkazem, kde je vyvolána výjimka, a končí u příkazu catch, který zachytí výjimku.

Nevyvolávejte výjimky v klauzulích finally

Nevyvolávejte výjimky v klauzulích finally. Další informace naleznete v tématu pravidla analýzy kódu CA2219.

Nevyvolávejte výjimky z neočekávaných míst

Některé metody, jako jsou Equals, GetHashCodea ToString metody, statické konstruktory a operátory rovnosti, by neměly vyvolat výjimky. Další informace naleznete v tématu pravidla analýzy kódu CA1065.

Synchronní vyvolání výjimek ověření argumentu

V metodách vracejících úlohy byste měli před zadáním asynchronní části metody ověřit argumenty a vyvolat všechny odpovídající výjimky, jako jsou ArgumentException a ArgumentNullException. Výjimky, které jsou vyvolány v asynchronní části metody, jsou uloženy ve vrácené úloze a neobjeví se, dokud například úloha nebude očekávána. Další informace naleznete v tématu Výjimky v metodách vracejících úlohy.

Vlastní typy výjimek

Následující osvědčené postupy se týkají vlastních typů výjimek:

Ukončete názvy tříd výjimek pomocí Exception

Pokud je potřeba vlastní výjimka, pojmenujte ji odpovídajícím způsobem a odvozujte ji z třídy Exception. Například:

public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
    Inherits Exception
End Class

Zahrnout tři konstruktory

Při vytváření vlastních tříd výjimek použijte alespoň tři běžné konstruktory: konstruktor bez parametrů, konstruktor, který přebírá řetězcovou zprávu, a konstruktor, který přebírá řetězcovou zprávu a vnitřní výjimku.

Příklad najdete v tématu Postupy: Vytváření uživatelem definovaných výjimek.

Podle potřeby zadejte další vlastnosti.

Uveďte další vlastnosti výjimky (kromě vlastního řetězce zprávy) jenom v případě, že existuje programový scénář, ve kterém jsou další informace užitečné. Například FileNotFoundException poskytuje vlastnost FileName.

Viz také