Kivételkezelési utasítások – throw, try-catch, try-finallyés try-catch-finally

A kivételekkel és try az throw utasításokkal dolgozhat. Az utasításthrow kivételt jelezhet. Az utasítással try elfoghatja és kezelheti a kódblokk végrehajtása során esetlegesen előforduló kivételeket.

A C# nyelv referenciadokumentuma a C# nyelv legújabb kiadású verzióját ismerteti. Emellett a közelgő nyelvi kiadás nyilvános előzetes verziójú funkcióinak kezdeti dokumentációját is tartalmazza.

A dokumentáció azonosítja azokat a funkciókat, amelyeket először a nyelv utolsó három verziójában vagy az aktuális nyilvános előzetes verziókban vezetnek be.

Jótanács

Ha meg szeretné tudni, hogy mikor jelent meg először egy funkció a C#-ban, tekintse meg a C# nyelvi verzióelőzményeiről szóló cikket.

Az throw utasítás

Az throw utasítás kivételt jelez:

if (shapeAmount <= 0)
{
    throw new ArgumentOutOfRangeException(nameof(shapeAmount), "Amount of shapes must be positive.");
}

throw e; Egy utasításban a kifejezés e eredményének implicit módon konvertálhatónak System.Exceptionkell lennie.

Használhatja a beépített kivételosztályokat, például ArgumentOutOfRangeException vagy InvalidOperationException. A .NET a következő segédmetenek is biztosítja, hogy bizonyos feltételek mellett kivételeket adjon ki: ArgumentNullException.ThrowIfNull és ArgumentException.ThrowIfNullOrEmpty. Saját kivételosztályokat is meghatározhat, amelyekből származik System.Exception. További információ: Kivételek létrehozása és kivetése.

catch Egy blokkon belül egy throw; utasítással újra kiírhatja a blokk által kezelt kivételtcatch:

try
{
    ProcessShapes(shapeAmount);
}
catch (Exception e)
{
    LogError(e, "Shape processing failed.");
    throw;
}

Feljegyzés

throw; Megőrzi a kivétel eredeti veremnyomát, amely a Exception.StackTrace tulajdonságban van tárolva. Ezzel szemben throw e; frissíti a StackTrace tulajdonságot e.

Kivétel esetén a közös nyelvi futtatókörnyezet (CLR) megkeresi azt a blokkotcatch kivételt. Ha a jelenleg végrehajtott metódus nem tartalmaz ilyen blokkot catch , a CLR megvizsgálja az aktuális metódust nevesítő metódust, és így tovább a hívási vermet. Ha nincs kompatibilis catch blokk, a CLR leállítja a végrehajtó szálat. További információkért tekintse meg a C#-nyelv specifikációjának a kivételek kezelésévelfoglalkozó szakaszát.

A throw kifejezés

Kifejezésként is használható throw . Ez a megközelítés több esetben is kényelmes lehet, például:

  • a feltételes operátor. Az alábbi példa egy throw kifejezést használ, amikor ArgumentException az átadott tömb args üres:

    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    
  • a null-szenesítő operátor. Az alábbi példa egy throw kifejezés használatával ad vissza egy ArgumentNullException értéket, amikor a tulajdonsághoz rendelendő sztring a következő null:

    public string Name
    {
        get => name;
        set => name = value ??
            throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
    }
    
  • kifejezéssel testesített lambda vagy metódus. Az alábbi példa egy throw kifejezéssel InvalidCastException jelzi, hogy az értékké alakítás DateTime nem támogatott:

    DateTime ToDateTime(IFormatProvider provider) =>
             throw new InvalidCastException("Conversion to a DateTime is not supported.");
    

Az try utasítás

Az utasítást a try következő űrlapok bármelyikében használhatja: try-catch - a kód blokkon belüli try végrehajtása során esetlegesen előforduló kivételek kezelésére, try-finally - a blokk elhagyásakor try futó kód megadására, valamint try-catch-finally - az előző két űrlap kombinációjaként.

Az try-catch utasítás

Az utasítással kezelheti a try-catch kódblokk végrehajtása során esetlegesen előforduló kivételeket. Helyezze el a kódot, ahol kivétel léphet fel egy blokkon try belül. A fogási záradék használatával adja meg a megfelelő catch blokkban kezelni kívánt kivételek alaptípusát:

try
{
    var result = Process(-3, 4);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (ArgumentException e)
{
    Console.WriteLine($"Processing failed: {e.Message}");
}

Több fogási záradékot is megadhat:

try
{
    var result = await ProcessAsync(-3, 4, cancellationToken);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (ArgumentException e)
{
    Console.WriteLine($"Processing failed: {e.Message}");
}
catch (OperationCanceledException)
{
    Console.WriteLine("Processing is cancelled.");
}

Kivétel esetén a futtatókörnyezet felülről lefelé haladva ellenőrzi a fogási záradékokat a megadott sorrendben. Legfeljebb egy catch blokk fut minden kidobott kivétel esetén. Ahogy az előző példa is mutatja, kihagyhatja egy kivételváltozó deklarációját, és csak a kivételtípust adhatja meg egy fogási záradékban. A megadott kivételtípus nélküli fogási záradék minden kivételnek megfelel, és ha van ilyen, akkor az utolsó fogási záradéknak kell lennie.

A kifogott kivétel újbóli eldobásához használja az throw utasítást, ahogy az alábbi példa is mutatja:

try
{
    var result = Process(-3, 4);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (Exception e)
{
    LogError(e, "Processing failed.");
    throw;
}

Feljegyzés

throw; Megőrzi a kivétel eredeti veremnyomát, amely a Exception.StackTrace tulajdonságban van tárolva. Ezzel szemben throw e; frissíti a StackTrace tulajdonságot e.

Kivételszűrő when

A kivételtípus mellett megadhat egy kivételszűrőt is, amely tovább vizsgálja a kivételt, és eldönti, hogy a megfelelő catch blokk kezeli-e ezt a kivételt. A kivételszűrő egy logikai kifejezés, amely a when kulcsszót követi, ahogy az alábbi példa is mutatja:

try
{
    var result = Process(-3, 4);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (Exception e) when (e is ArgumentException || e is DivideByZeroException)
{
    Console.WriteLine($"Processing failed: {e.Message}");
}

Az előző példa egy kivételszűrővel egyetlen blokkot catch biztosít két megadott típus kivételeinek kezeléséhez.

Több záradékot is megadhat catch ugyanahhoz a kivételtípushoz, ha kivételszűrőkkel különböztetik meg őket. Előfordulhat, hogy az egyik záradék kivételszűrővel nem rendelkezik. Ha létezik ilyen záradék, akkor a kivételtípust meghatározó záradékok közül az utolsónak kell lennie.

Ha egy catch záradék kivételszűrővel rendelkezik, megadhatja azt a kivételtípust, amely megegyezik a utána megjelenő záradék kivételtípusával catch vagy kevésbé származtatott típusával. Ha például egy kivételszűrő van jelen, catch (Exception e) a záradéknak nem kell az utolsó záradéknak lennie.

Kivételszűrők és hagyományos kivételkezelés

A kivételszűrők jelentős előnyöket biztosítanak a hagyományos kivételkezelési megközelítéssel szemben. A fő különbség a kivételkezelési logika kiértékelése:

  • Kivételszűrők (when): A rendszer a verem feloldása előtt kiértékeli a szűrőkifejezést. Ez azt jelenti, hogy az eredeti hívásverem és az összes helyi változó érintetlen marad a szűrő kiértékelése során.
  • Hagyományos catch blokkok: A fogási blokk a verem feloldása után fut, ami értékes hibakeresési információk elvesztését eredményezheti.

Íme egy összehasonlítás, amely a különbséget mutatja:

public static void DemonstrateStackUnwindingDifference()
{
    var localVariable = "Important debugging info";
    
    try
    {
        ProcessWithExceptionFilter(localVariable);
    }
    catch (InvalidOperationException ex) when (ex.Message.Contains("filter"))
    {
        // Exception filter: Stack not unwound yet.
        // localVariable is still accessible in debugger.
        // Call stack shows original throwing location.
        Console.WriteLine($"Caught with filter: {ex.Message}");
        Console.WriteLine($"Local variable accessible: {localVariable}");
    }
    
    try
    {
        ProcessWithTraditionalCatch(localVariable);
    }
    catch (InvalidOperationException ex)
    {
        // Traditional catch: Stack already unwound.
        // Some debugging information may be lost.
        if (ex.Message.Contains("traditional"))
        {
            Console.WriteLine($"Caught with if: {ex.Message}");
            Console.WriteLine($"Local variable accessible: {localVariable}");
        }
        else
        {
            throw; // Re-throws and further modifies stack trace.
        }
    }
}

private static void ProcessWithExceptionFilter(string context)
{
    throw new InvalidOperationException($"Exception for filter demo: {context}");
}

private static void ProcessWithTraditionalCatch(string context)
{
    throw new InvalidOperationException($"Exception for traditional demo: {context}");
}

A kivételszűrők előnyei

  • Jobb hibakeresési élmény: Mivel a verem nem oldható fel, amíg egy szűrő nem egyezik, a hibakeresők az eredeti hibapontot az összes helyi változó érintetlen állapotában jeleníthetik meg.
  • Teljesítménybeli előnyök: Ha nem egyezik a szűrő, a kivétel továbbra is propagálást folytat a verem feloldása és helyreállítása nélkül.
  • Tisztább kód: Több szűrő is képes kezelni az azonos kivételtípusú különböző feltételeket anélkül, hogy beágyazott if-else utasításokat kellene megadnia.
  • Naplózás és diagnosztika: Megvizsgálhatja és naplózhatja a kivétel részleteit, mielőtt eldöntené, hogy kezeli-e a kivételt:
public static void DemonstrateDebuggingAdvantage()
{
    var contextData = new Dictionary<string, object>
    {
        ["RequestId"] = Guid.NewGuid(),
        ["UserId"] = "user123",
        ["Timestamp"] = DateTime.Now
    };

    try
    {
        // Simulate a deep call stack.
        Level1Method(contextData);
    }
    catch (Exception ex) when (LogAndFilter(ex, contextData))
    {
        // This catch block may never execute if LogAndFilter returns false.
        // But LogAndFilter can examine the exception while the stack is intact.
        Console.WriteLine("Exception handled after logging");
    }
}

private static void Level1Method(Dictionary<string, object> context)
{
    Level2Method(context);
}

private static void Level2Method(Dictionary<string, object> context)
{
    Level3Method(context);
}

private static void Level3Method(Dictionary<string, object> context)
{
    throw new InvalidOperationException("Error in deep call stack");
}

private static bool LogAndFilter(Exception ex, Dictionary<string, object> context)
{
    // This method runs before stack unwinding.
    // Full call stack and local variables are still available.
    Console.WriteLine($"Exception occurred: {ex.Message}");
    Console.WriteLine($"Request ID: {context["RequestId"]}");
    Console.WriteLine($"Full stack trace preserved: {ex.StackTrace}");
    
    // Return true to handle the exception, false to continue search.
    return ex.Message.Contains("deep call stack");
}

Mikor érdemes kivételszűrőket használni?

A következő esetekben használjon kivételszűrőket:

  • A kivételek kezelése adott feltételek vagy tulajdonságok alapján.
  • A hibakereséshez őrizze meg az eredeti hívásvermet.
  • Mielőtt eldöntené, hogy kezeli-e a kivételeket, naplózza vagy vizsgálja meg a kivételeket.
  • Ugyanazt a kivételtípust a környezet alapján eltérően kezelheti.
public static void HandleFileOperations(string filePath)
{
    try
    {
        // Simulate file operation that might fail.
        ProcessFile(filePath);
    }
    catch (IOException ex) when (ex.Message.Contains("access denied"))
    {
        Console.WriteLine("File access denied. Check permissions.");
    }
    catch (IOException ex) when (ex.Message.Contains("not found"))
    {
        Console.WriteLine("File not found. Verify the path.");
    }
    catch (IOException ex) when (IsNetworkPath(filePath))
    {
        Console.WriteLine($"Network file operation failed: {ex.Message}");
    }
    catch (IOException)
    {
        Console.WriteLine("Other I/O error occurred.");
    }
}

private static void ProcessFile(string filePath)
{
    // Simulate different types of file exceptions.
    if (filePath.Contains("denied"))
        throw new IOException("File access denied");
    if (filePath.Contains("missing"))
        throw new IOException("File not found");
    if (IsNetworkPath(filePath))
        throw new IOException("Network timeout occurred");
}

private static bool IsNetworkPath(string path)
{
    return path.StartsWith(@"\\") || path.StartsWith("http");
}

Verem nyomkövetésének megőrzése

A kivételszűrők megőrzik az eredeti ex.StackTrace tulajdonságot. Ha egy catch záradék nem tudja feldolgozni a kivételt, és újradobja, az eredeti veremadatok elvesznek. A when szűrő nem oldja fel a vermet, ezért ha when van falseszűrő, az eredeti verem nyomkövetése nem változik.

A kivételszűrő megközelítés értékes azokban az alkalmazásokban, ahol a hibakeresési információk megőrzése kulcsfontosságú a kódhibák diagnosztizálásához.

Kivételek az aszinkron és iterátor metódusokban

Ha kivétel történik egy aszinkron függvényben, a kivétel a függvény hívójának propagálja, amikor a függvény eredményére vár , ahogy az alábbi példa mutatja:

public static async Task Run()
{
    try
    {
        Task<int> processing = ProcessAsync(-1);
        Console.WriteLine("Launched processing.");

        int result = await processing;
        Console.WriteLine($"Result: {result}.");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // Output:
    // Launched processing.
    // Processing failed: Input must be non-negative. (Parameter 'input')
}

private static async Task<int> ProcessAsync(int input)
{
    if (input < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(input), "Input must be non-negative.");
    }

    await Task.Delay(500);
    return input;
}

Ha egy iterátor metódusban kivétel történik, a kivétel csak akkor propagálódik a hívónak, ha az iterátor továbblép a következő elemre.

Az try-finally utasítás

try-finally Egy utasításban a blokk akkor fut, amikor a finally vezérlő elhagyja a try blokkot. A vezérlő elhagyhatja a try blokkot a következő miatt:

  • normál végrehajtás,
  • ugrási utasítás végrehajtása (vagyis return, , break, continuevagy ), vagy goto
  • kivétel propagálása a try blokkból.

Az alábbi példa egy objektum állapotának visszaállítására használja a finally blokkot, mielőtt a vezérlő elhagyja a metódust:

public async Task HandleRequest(int itemId, CancellationToken ct)
{
    Busy = true;

    try
    {
        await ProcessAsync(itemId, ct);
    }
    finally
    {
        Busy = false;
    }
}

A blokk használatával finally a blokkban try használt lefoglalt erőforrásokat is törölheti.

Feljegyzés

Ha egy erőforrás típusa implementálja a felületet, vegye figyelembe az utasítástIDisposableIAsyncDisposable.using Az using utasítás biztosítja, hogy a beszerzett erőforrások törlődnek, amikor a vezérlő elhagyja az utasítást using . A fordító utasítássá alakít át egy using utasítást try-finally .

A finally blokk végrehajtása attól függ, hogy az operációs rendszer kiváltja-e a kivétel kikapcsolási műveletét. Az egyetlen olyan eset, amikor finally a blokkok végrehajtása nem jár a program azonnali leállításával. Ilyen megszüntetés történhet például a Environment.FailFast hívás vagy egy OverflowExceptionInvalidProgramException kivétel miatt. A legtöbb operációs rendszer ésszerű erőforrás-tisztítást végez a folyamat leállítása és eltávolítása során.

Az try-catch-finally utasítás

try-catch-finally A blokk végrehajtása try során esetlegesen előforduló kivételek kezelésére és az utasítás elhagyásakor try futtatandó kód megadására egyaránt használjon utasítást:

public async Task ProcessRequest(int itemId, CancellationToken ct)
{
    Busy = true;

    try
    {
        await ProcessAsync(itemId, ct);
    }
    catch (Exception e) when (e is not OperationCanceledException)
    {
        LogError(e, $"Failed to process request for item ID {itemId}.");
        throw;
    }
    finally
    {
        Busy = false;
    }

}

Ha egy catch blokk kivételt kezel, a finally blokk a catch blokk befejeződése után fut (még akkor is, ha a blokk végrehajtása catch során egy másik kivétel történik). További információ catch és finally blokkok : Az try-catch utasítás és az try-finally Utasítás szakasz.

C# nyelvspecifikáció

További információt a C# nyelvspecifikációjának alábbi szakaszaiban talál:

Lásd még