Dobre rozwiązanie na czekanie w try / catch / w końcu?

94

Muszę wywołać asyncmetodę w catchbloku przed ponownym rzuceniem wyjątku (z jego śladem stosu) w następujący sposób:

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

Ale niestety nie możesz używać awaitw a catchor finallyblock. Dowiedziałem się, że to dlatego, że kompilator nie ma sposobu, aby wrócić w catchbloku, aby wykonać to, co jest po twojej awaitinstrukcji lub coś w tym rodzaju ...

Próbowałem użyć Task.Wait()do wymiany awaiti dostałem impasu. Szukałem w Internecie, jak mogę tego uniknąć i znalazłem tę witrynę .

Ponieważ nie mogę zmienić asyncmetod ani nie wiem, czy używają ConfigureAwait(false), stworzyłem te metody, które pobierają a, Func<Task>która uruchamia metodę asynchroniczną, gdy jesteśmy w innym wątku (aby uniknąć impasu) i czeka na jej zakończenie:

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

Więc moje pytania brzmi: czy uważasz, że ten kod jest w porządku?

Oczywiście, jeśli masz jakieś ulepszenia lub znasz lepsze podejście, słucham! :)

user2397050
źródło
2
Korzystanie awaitw bloku catch jest faktycznie dozwolony od C # 6.0 (patrz moją odpowiedź poniżej)
ADI Lester
3
Powiązane komunikaty o błędach C # 5.0 : CS1985 : nie można czekać w treści klauzuli catch. CS1984 : Nie można czekać w treści klauzuli
DavidRR

Odpowiedzi:

174

Możesz przenieść logikę poza catchblok i ponownie zgłosić wyjątek po, jeśli to konieczne, za pomocą 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();
    }
}

W ten sposób, gdy wywołujący sprawdza właściwość wyjątku StackTrace, nadal rejestruje, gdzie do środka TaskThatFailszostał rzucony.

Sam Harwell
źródło
1
Jaka jest korzyść z trzymania ExceptionDispatchInfozamiast Exception(jak w odpowiedzi Stephena Cleary'ego)?
Varvara Kalinina
2
Domyślam się, że jeśli zdecydujesz się rzucić ponownie Exception, stracisz wszystkie poprzednie StackTrace?
Varvara Kalinina
1
@VarvaraKalinina Dokładnie.
54

Powinieneś wiedzieć, że od C # 6.0 możliwe jest użycie awaitin catchi finallybloki, więc faktycznie możesz to zrobić:

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

Nowe funkcje C # 6.0, w tym ta, o której wspomniałem, są wymienione tutaj lub jako wideo tutaj .

Adi Lester
źródło
Obsługa await w blokach catch / final w języku C # 6.0 jest również wskazana w Wikipedii.
DavidRR
4
@DavidRR. Wikipedia nie jest autorytatywna. Jeśli chodzi o to, jest to po prostu kolejna witryna internetowa wśród milionów.
user34660
Chociaż dotyczy to języka C # 6, pytanie zostało oznaczone tagiem C # 5 od samego początku. To sprawia, że ​​zastanawiam się, czy ta odpowiedź jest myląca, czy też powinniśmy po prostu usunąć tag konkretnej wersji w takich przypadkach.
julealgon
16

Jeśli potrzebujesz obsługi asyncbłędów, polecam coś takiego:

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

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

Problem z synchronicznym blokowaniem asynckodu (niezależnie od tego, w którym wątku działa) polega na tym, że blokujesz synchronicznie. W większości scenariuszy lepiej jest użyć await.

Aktualizacja: ponieważ musisz rzucić ponownie, możesz użyć ExceptionDispatchInfo.

Stephen Cleary
źródło
1
Dziękuję, ale niestety już znam tę metodę. To jest to, co robię normalnie, ale nie mogę tego zrobić tutaj. Jeśli po prostu użyję throw exception;w ifinstrukcji, ślad stosu zostanie utracony.
user2397050
3

W naszym projekcie wyodrębniliśmy świetną odpowiedź hvd na następującą klasę narzędzi wielokrotnego użytku:

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

Można by go użyć w następujący sposób:

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

Nie krępuj się ulepszyć nazewnictwo, celowo pozostawiliśmy je rozwlekłe. Zauważ, że nie ma potrzeby przechwytywania kontekstu wewnątrz opakowania, ponieważ jest on już przechwycony w witrynie wywołania, stąd ConfigureAwait(false).

mrts
źródło