Eine gute Lösung, um in try / catch / finally zu warten?

92

Ich muss eine asyncMethode in einem catchBlock aufrufen , bevor ich die Ausnahme (mit ihrer Stapelverfolgung) erneut wie folgt auslösen kann:

try
{
    // Do something
}
catch
{
    // <- Clean things here with async methods
    throw;
}

Aber leider kann man nicht awaitin einem catchoder finallyBlock verwenden. Ich habe gelernt, dass der Compiler keine Möglichkeit hat, in einen catchBlock zurückzukehren, um das auszuführen, was nach Ihrer awaitAnweisung oder so ist ...

Ich habe versucht zu Task.Wait()ersetzen awaitund ich habe einen Deadlock. Ich habe im Web gesucht, wie ich dies vermeiden kann, und diese Website gefunden .

Da ich die asyncMethoden nicht ändern kann und auch nicht weiß, ob sie verwendet werden ConfigureAwait(false), habe ich diese Methoden erstellt Func<Task>, die eine asynchrone Methode starten, sobald wir uns in einem anderen Thread befinden (um einen Deadlock zu vermeiden) und auf deren Abschluss warten:

public static void AwaitTaskSync(Func<Task> action)
{
    Task.Run(async () => await action().ConfigureAwait(false)).Wait();
}

public static TResult AwaitTaskSync<TResult>(Func<Task<TResult>> action)
{
    return Task.Run(async () => await action().ConfigureAwait(false)).Result;
}

public static void AwaitSync(Func<IAsyncAction> action)
{
    AwaitTaskSync(() => action().AsTask());
}

public static TResult AwaitSync<TResult>(Func<IAsyncOperation<TResult>> action)
{
    return AwaitTaskSync(() => action().AsTask());
}

Meine Fragen sind also: Glaubst du, dieser Code ist in Ordnung?

Natürlich höre ich zu, wenn Sie einige Verbesserungen haben oder einen besseren Ansatz kennen! :) :)

user2397050
quelle
2
Die Verwendung awaitin einem Catch-Block ist seit C # 6.0 tatsächlich zulässig (siehe meine Antwort unten)
Adi Lester
3
Verwandte C # 5.0- Fehlermeldungen: CS1985 : Kann im Hauptteil einer catch-Klausel nicht warten. CS1984 : Kann im
DavidRR

Antworten:

172

Sie können die Logik außerhalb des catchBlocks verschieben und die Ausnahme nach Bedarf erneut auslösen, indem Sie verwenden ExceptionDispatchInfo.

static async Task f()
{
    ExceptionDispatchInfo capturedException = null;
    try
    {
        await TaskThatFails();
    }
    catch (MyException ex)
    {
        capturedException = ExceptionDispatchInfo.Capture(ex);
    }

    if (capturedException != null)
    {
        await ExceptionHandler();

        capturedException.Throw();
    }
}

Auf diese Weise zeichnet der Aufrufer beim Überprüfen der Ausnahmeeigenschaft StackTraceimmer noch auf, wo TaskThatFailssie ausgelöst wurde.

Sam Harwell
quelle
1
Was ist der Vorteil des Behaltens ExceptionDispatchInfostatt Exception(wie in der Antwort von Stephen Cleary)?
Varvara Kalinina
2
Ich kann mir vorstellen Exception, dass Sie alle vorherigen verlieren , wenn Sie sich entscheiden, das erneut zu werfen StackTrace?
Varvara Kalinina
1
@ VarvaraKalinina Genau.
54

Sie sollten wissen, dass es seit C # 6.0 möglich ist, awaitin catchund zu finallyblockieren, also können Sie dies tatsächlich tun:

try
{
    // Do something
}
catch (Exception ex)
{
    await DoCleanupAsync();
    throw;
}

Die neuen C # 6.0-Funktionen, einschließlich der gerade erwähnten, werden hier oder als Video hier aufgelistet .

Adi Lester
quelle
Die Unterstützung für das Warten in catch / finally-Blöcken in C # 6.0 ist auch auf Wikipedia angegeben.
DavidRR
4
@ DavidRR. Die Wikipedia ist nicht maßgeblich. Diesbezüglich ist es nur eine weitere Website unter Millionen.
user34660
Während dies für C # 6 gilt, wurde die Frage von Anfang an mit C # 5 markiert. Ich frage mich daher, ob diese Antwort hier verwirrend ist oder ob wir in diesen Fällen nur das spezifische Versions-Tag entfernen sollten.
Julealgon
16

Wenn Sie asyncFehlerbehandlungsroutinen verwenden müssen, würde ich Folgendes empfehlen:

Exception exception = null;
try
{
  ...
}
catch (Exception ex)
{
  exception = ex;
}

if (exception != null)
{
  ...
}

Das Problem beim synchronen Blockieren von asyncCode (unabhängig davon, auf welchem ​​Thread er ausgeführt wird) besteht darin, dass Sie synchron blockieren. In den meisten Szenarien ist es besser zu verwenden await.

Update: Da Sie neu werfen müssen, können Sie verwenden ExceptionDispatchInfo.

Stephen Cleary
quelle
1
Danke, aber leider kenne ich diese Methode schon. Das mache ich normalerweise, aber das kann ich hier nicht. Wenn ich einfach throw exception;in der ifAnweisung verwende, geht die Stapelverfolgung verloren.
user2397050
3

Wir haben die großartige Antwort von hvd auf die folgende wiederverwendbare Dienstprogrammklasse in unserem Projekt extrahiert :

public static class TryWithAwaitInCatch
{
    public static async Task ExecuteAndHandleErrorAsync(Func<Task> actionAsync,
        Func<Exception, Task<bool>> errorHandlerAsync)
    {
        ExceptionDispatchInfo capturedException = null;
        try
        {
            await actionAsync().ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            capturedException = ExceptionDispatchInfo.Capture(ex);
        }

        if (capturedException != null)
        {
            bool needsThrow = await errorHandlerAsync(capturedException.SourceException).ConfigureAwait(false);
            if (needsThrow)
            {
                capturedException.Throw();
            }
        }
    }
}

Man würde es wie folgt verwenden:

    public async Task OnDoSomething()
    {
        await TryWithAwaitInCatch.ExecuteAndHandleErrorAsync(
            async () => await DoSomethingAsync(),
            async (ex) => { await ShowMessageAsync("Error: " + ex.Message); return false; }
        );
    }

Fühlen Sie sich frei, die Benennung zu verbessern, wir haben sie absichtlich ausführlich gehalten. Beachten Sie, dass der Kontext im Wrapper nicht erfasst werden muss, da er bereits auf der Aufrufsite erfasst wurde ConfigureAwait(false).

mrts
quelle