Une bonne solution pour wait dans try / catch / finally?

93

J'ai besoin d'appeler une asyncméthode dans un catchbloc avant de lancer à nouveau 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 awaitdans un bloc catchou finally. J'ai appris que c'est parce que le compilateur n'a aucun moyen de revenir en arrière dans un catchbloc pour exécuter ce qui se trouve après votre awaitinstruction ou quelque chose comme ça ...

J'ai essayé d'utiliser Task.Wait()pour remplacer awaitet j'ai eu une impasse. J'ai cherché sur le Web comment éviter cela et j'ai trouvé ce site .

Comme je ne peux pas changer les asyncméthodes et que je ne sais pas si elles utilisent ConfigureAwait(false), j'ai créé ces méthodes qui prennent un Func<Task>qui démarre une méthode asynchrone une fois que nous sommes sur un thread différent (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());
}

Ma question est donc 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! :)

user2397050
la source
2
L'utilisation awaitdans un bloc catch est en fait autorisée depuis C # 6.0 (voir ma réponse ci-dessous)
Adi Lester
3
Messages d'erreur C # 5.0 associés : CS1985 : Impossible d'attendre dans le corps d'une clause catch. CS1984 : Impossible d'attendre dans le corps d'une clause finally.
DavidRR

Réponses:

173

Vous pouvez déplacer la logique en dehors du catchbloc et relancer 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();
    }
}

De cette façon, lorsque l'appelant inspecte la StackTracepropriété de l'exception , il enregistre toujours où à l'intérieur TaskThatFailselle a été lancée.

Sam Harwell
la source
1
Quel est l'avantage de conserver ExceptionDispatchInfoau lieu de Exception(comme dans la réponse de Stephen Cleary)?
Varvara Kalinina
2
Je peux deviner que si vous décidez de relancer le Exception, vous perdez tout le précédent StackTrace?
Varvara Kalinina
1
@VarvaraKalinina Exactement.
54

Vous devez savoir que depuis C # 6.0, il est possible d'utiliser awaitin catchand finallyblocks, donc vous pouvez 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 .

Adi Lester
la source
La prise en charge de wait dans les blocs catch / finally en C # 6.0 est également indiquée sur Wikipedia.
DavidRR
4
@DavidRR. le Wikipedia n'est pas autoritaire. C'est juste un autre site Web parmi des millions en ce qui concerne cela.
user34660
Bien que cela soit valable pour C # 6, la question a été étiquetée C # 5 depuis le tout début. Cela me fait me demander si cette réponse est déroutante ou si nous devrions simplement supprimer la balise de version spécifique dans ces cas.
julealgon
16

Si vous avez besoin d'utiliser asyncdes gestionnaires d'erreurs, je vous recommande 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 asynccode (quel que soit le thread sur lequel il s'exécute) 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 relancer, vous pouvez utiliser ExceptionDispatchInfo.

Stephen Cleary
la source
1
Merci mais malheureusement je connais déjà cette méthode. C'est ce que je fais normalement, mais je ne peux pas faire ça ici. Si j'utilise simplement throw exception;dans l' ifinstruction, la trace de la pile sera perdue.
user2397050
3

Nous avons extrait l'excellente réponse de hvd à la classe utilitaire réutilisable suivante dans 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 le nommage, nous l'avons gardé intentionnellement verbeux. 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'appel, par conséquent ConfigureAwait(false).

mrts
la source