web-dev-qa-db-fra.com

Une bonne solution à attendre dans try / catch / finally?

J'ai besoin d'appeler une méthode async dans un bloc catch avant de relancer l'exception (avec sa trace de pile) comme ceci:

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

Mais malheureusement, vous ne pouvez pas utiliser await dans un bloc catch ou finally. J'ai appris que c'est parce que le compilateur n'a aucun moyen de revenir dans un bloc catch pour exécuter ce qui se trouve après votre instruction await ou quelque chose comme ça ...

J'ai essayé d'utiliser Task.Wait() pour remplacer await et je me suis retrouvé dans une impasse. J'ai cherché sur le Web comment je pouvais éviter cela et trouvé ce site .

Comme je ne peux pas changer les méthodes async ni savoir s'ils utilisent ConfigureAwait(false), j'ai créé ces méthodes qui prennent un Func<Task> Qui démarre une méthode async une fois que nous sommes sur un autre thread (pour éviter un blocage) et attend son achèvement:

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

Donc, ma question est la suivante: pensez-vous que ce code est correct?

Bien sûr, si vous avez des améliorations ou connaissez une meilleure approche, je vous écoute! :)

89
user2397050

Vous pouvez déplacer la logique en dehors du bloc catch et rediffuser l'exception après, si nécessaire, en utilisant 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();
    }
}

Ainsi, lorsque l'appelant inspecte la propriété StackTrace de l'exception, il enregistre quand même où il a été jeté dans TaskThatFails.

168
user743382

Vous devriez savoir que depuis C # 6.0, il est possible d'utiliser await dans les blocs catch et finally afin que vous puissiez en fait faire ceci:

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

Les nouvelles fonctionnalités de C # 6.0, y compris celle que je viens de mentionner sont répertoriées ici ou sous forme de vidéo ici .

53
Adi Lester

Si vous devez utiliser les gestionnaires d’erreur async, je vous conseille quelque chose comme ceci:

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

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

Le problème avec le blocage synchrone sur le code async (quel que soit le thread sur lequel il est exécuté) est que vous bloquez de manière synchrone. Dans la plupart des scénarios, il est préférable d'utiliser await.

Mise à jour: Puisque vous devez rediffuser, vous pouvez utiliser ExceptionDispatchInfo.

15
Stephen Cleary

Nous avons extrait la bonne réponse de hvd à la classe d'utilitaires réutilisable suivante de notre projet:

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

On l'utiliserait comme suit:

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

N'hésitez pas à améliorer la dénomination, nous l'avons délibérément conservée. Notez qu'il n'est pas nécessaire de capturer le contexte à l'intérieur du wrapper car il est déjà capturé dans le site d'appels, d'où ConfigureAwait(false).

3
mrts