Dela via


Undantagshanteringsuttryck – throw, try-catch, try-finallyoch try-catch-finally

Du använder throw instruktionerna och try för att arbeta med undantag. Använd -instruktionen throw för att utlösa ett undantag. Använd -instruktionen try för att fånga och hantera undantag som kan inträffa under körningen av ett kodblock.

Instruktionen throw

Instruktionen throw utlöser ett undantag:

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

I en throw e; instruktion måste uttryckets e resultat implicit konverteras till System.Exception.

Du kan använda de inbyggda undantagsklasserna, till exempel ArgumentOutOfRangeException eller InvalidOperationException. .NET tillhandahåller också följande hjälpmetoder för att utlösa undantag under vissa villkor: ArgumentNullException.ThrowIfNull och ArgumentException.ThrowIfNullOrEmpty. Du kan också definiera dina egna undantagsklasser som härleds från System.Exception. Mer information finns i Skapa och utlösa undantag.

I ett catch block kan du använda en throw; -instruktion för att återskapa undantaget som hanteras av catch blocket:

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

Kommentar

throw; bevarar den ursprungliga stackspårningen av undantaget, som lagras i Exception.StackTrace egenskapen. I motsats till det throw e; uppdaterar egenskapen StackTraceför e .

När ett undantag utlöses letar CLR (Common Language Runtime) efter det catch block som kan hantera det här undantaget. Om den metod som körs för närvarande inte innehåller något catch sådant block tittar CLR på metoden som anropade den aktuella metoden och så vidare upp i anropsstacken. Om inget catch block hittas avslutar CLR den körande tråden. Mer information finns i avsnittet Hur undantag hanteras i C#-språkspecifikationen.

Uttrycket throw

Du kan också använda throw som ett uttryck. Detta kan vara praktiskt i ett antal fall, bland annat:

  • villkorsoperatorn. I följande exempel används ett throw uttryck för att utlösa en ArgumentException när den skickade matrisen args är tom:

    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    
  • operatorn null-coalescing. I följande exempel används ett throw uttryck för att utlösa en ArgumentNullException när strängen som ska tilldelas till en egenskap är null:

    public string Name
    {
        get => name;
        set => name = value ??
            throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
    }
    
  • en uttrycksfyllig lambda eller metod. I följande exempel används ett throw uttryck för att utlösa ett InvalidCastException för att indikera att en konvertering till ett DateTime värde inte stöds:

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

Instruktionen try

Du kan använda -instruktionen try i något av följande formulär: try-catch - för att hantera undantag som kan inträffa under körningen av koden i ett try block, try-finally - för att ange den kod som körs när kontrollen lämnar try blocket och try-catch-finally - som en kombination av de föregående två formulären.

Instruktionen try-catch

Använd -instruktionen try-catch för att hantera undantag som kan inträffa under körningen av ett kodblock. Placera koden där ett undantag kan inträffa i ett try block. Använd en catch-sats för att ange den bastyp av undantag som du vill hantera i motsvarande catch block:

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

Du kan ange flera catch-satser:

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

När ett undantag inträffar granskas catch-satser i den angivna ordningen, uppifrån och ned. Maximalt körs endast ett catch block för undantag som genereras. Som föregående exempel också visar kan du utelämna deklarationen av en undantagsvariabel och endast ange undantagstypen i en catch-sats. En catch-sats utan någon angiven undantagstyp matchar alla undantag och måste, om det finns, vara den sista catch-satsen.

Om du vill återskapa ett undantag som fångas använder du -instruktionenthrow, som följande exempel visar:

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

Kommentar

throw; bevarar den ursprungliga stackspårningen av undantaget, som lagras i Exception.StackTrace egenskapen. I motsats till det throw e; uppdaterar egenskapen StackTraceför e .

Ett when undantagsfilter

Tillsammans med en undantagstyp kan du också ange ett undantagsfilter som ytterligare undersöker ett undantag och avgör om motsvarande catch block hanterar undantaget. Ett undantagsfilter är ett booleskt uttryck som följer nyckelordet when , vilket visas i följande exempel:

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}");
}

I föregående exempel används ett undantagsfilter för att tillhandahålla ett enda catch block för att hantera undantag av två angivna typer.

Du kan ange flera catch satser för samma undantagstyp om de skiljer sig från undantagsfilter. En av dessa satser kanske inte har något undantagsfilter. Om en sådan sats finns måste den vara den sista av de satser som anger den undantagstypen.

Om en catch sats har ett undantagsfilter kan den ange den undantagstyp som är samma som eller mindre härledd än en undantagstyp för en catch sats som visas efter den. Om det till exempel finns ett undantagsfilter behöver en catch (Exception e) sats inte vara den sista satsen.

Undantagsfilter jämfört med traditionell undantagshantering

Undantagsfilter ger betydande fördelar jämfört med traditionella metoder för undantagshantering. Den viktigaste skillnaden är när logiken för undantagshantering utvärderas:

  • Undantagsfilter (when): Filteruttrycket utvärderas innan stacken tas bort. Det innebär att den ursprungliga anropsstacken och alla lokala variabler förblir intakta under filterutvärderingen.
  • Traditionella catch block: Catch-blocket körs efter att stacken har avslutats, vilket kan förlora värdefull felsökningsinformation.

Här är en jämförelse som visar skillnaden:

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}");
}

Fördelar med undantagsfilter

  • Bättre felsökningsupplevelse: Eftersom stacken inte har lösts upp förrän ett filter matchar kan felsökarna visa den ursprungliga felpunkten med alla lokala variabler intakta.
  • Prestandafördelar: Om inget filter matchar fortsätter undantaget att spridas utan att stacken varvar ned och återställs.
  • Renare kod: Flera filter kan hantera olika villkor av samma undantagstyp utan att behöva kapslade if-else-instruktioner.
  • Loggning och diagnostik: Du kan undersöka och logga undantagsinformation innan du bestämmer dig för om undantaget ska hanteras:
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");
}

När undantagsfilter ska användas

Använd undantagsfilter när du behöver:

  • Hantera undantag baserat på specifika villkor eller egenskaper.
  • Bevara den ursprungliga anropsstacken för felsökning.
  • Logga eller granska undantag innan du bestämmer dig för om de ska hanteras.
  • Hantera samma undantagstyp på olika sätt baserat på kontext.
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");
}

Bevarande av stackspårning

Undantagsfilter bevarar den ursprungliga ex.StackTrace egenskapen. Om en catch sats inte kan bearbeta undantaget och återkastas går den ursprungliga stackinformationen förlorad. when Filtret varvar inte ned stacken, så om ett when filter är falseändras inte den ursprungliga stackspårningen.

Undantagsfiltermetoden är värdefull i program där bevarande av felsökningsinformation är avgörande för att diagnostisera problem.

Undantag i asynkrona metoder och iteratormetoder

Om ett undantag inträffar i en asynkron funktion sprids det till funktionens anropare när du väntar på resultatet av funktionen, som följande exempel visar:

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;
}

Om ett undantag inträffar i en iteratormetod sprids det endast till anroparen när iteratorn avancerar till nästa element.

Instruktionen try-finally

I en try-finally instruktion finally körs blocket när kontrollen lämnar try blocket. Kontrollen kan lämna try blocket på grund av

  • normal körning,
  • körning av en jump-instruktion (dvs. , returnbreak, continueeller goto) eller
  • spridning av ett undantag från try blocket.

I följande exempel används finally blocket för att återställa tillståndet för ett objekt innan kontrollen lämnar metoden:

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

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

Du kan också använda finally blocket för att rensa allokerade resurser som används i try blocket.

Kommentar

När typen av en resurs implementerar gränssnittet eller bör du överväga -instruktionenIDisposable.IAsyncDisposableusing -instruktionen using säkerställer att förvärvade resurser tas bort när kontrollen lämnar -instruktionen using . Kompilatorn omvandlar en using instruktion till en try-finally -instruktion.

Körningen finally av blocket beror på om operativsystemet väljer att utlösa en undantagsåtgärd. De enda fall där finally block inte körs omfattar omedelbar avslutning av ett program. En sådan avslutning kan till exempel inträffa på grund av anropet Environment.FailFast eller ett OverflowException undantag InvalidProgramException . De flesta operativsystem utför en rimlig resursrensning som en del av att stoppa och ta bort processen.

Instruktionen try-catch-finally

Du använder en try-catch-finally instruktion både för att hantera undantag som kan inträffa under körningen try av blocket och ange den kod som måste köras när kontrollen lämnar instruktionen try :

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;
    }

}

När ett undantag hanteras av ett catch block finally körs blocket efter körningen av blocket catch (även om ett annat undantag inträffar under körningen catch av blocket). Information om catch och block finns i finally respektive try-catch.

Språkspecifikation för C#

Mer information finns i följande avsnitt i C#-språkspecifikationen:

Se även