Una buona soluzione per attendere in prova / cattura / finalmente?


92

Ho bisogno di chiamare un asyncmetodo in un catchblocco prima di lanciare di nuovo l'eccezione (con la sua traccia dello stack) in questo modo:

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

Ma purtroppo non è possibile utilizzare awaitin una catcho di finallyblocco. Ho imparato che è perché il compilatore non ha alcun modo per tornare indietro in un catchblocco per eseguire ciò che è dopo la tua awaitistruzione o qualcosa del genere ...

Ho provato a utilizzare Task.Wait()per sostituire awaite ho ottenuto un deadlock. Ho cercato sul Web come avrei potuto evitarlo e ho trovato questo sito .

Dato che non posso cambiare i asyncmetodi né so se usano ConfigureAwait(false), ho creato questi metodi che prendono un Func<Task>metodo che avvia un metodo asincrono una volta che siamo su un thread diverso (per evitare un deadlock) e attende il suo completamento:

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

Quindi la mia domanda è: pensi che questo codice vada bene?

Ovviamente, se hai qualche miglioramento o conosci un approccio migliore, ti sto ascoltando! :)


2
L'utilizzo awaitin un blocco catch è effettivamente consentito da C # 6.0 (vedere la mia risposta di seguito)
Adi Lester

3
Messaggi di errore correlati di C # 5.0 : CS1985 : Impossibile attendere nel corpo di una clausola catch. CS1984 : Non posso attendere nel corpo di una clausola finalmente.
DavidRR

Risposte:


172

È possibile spostare la logica all'esterno del catchblocco e lanciare nuovamente l'eccezione dopo, se necessario, utilizzando 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();
    }
}

In questo modo, quando il chiamante ispeziona la StackTraceproprietà dell'eccezione , registra ancora dove TaskThatFailsè stata lanciata all'interno .


1
Qual è il vantaggio di mantenere ExceptionDispatchInfoinvece di Exception(come nella risposta di Stephen Cleary)?
Varvara Kalinina

2
Posso immaginare che se decidi di rilanciare il Exception, perdi tutto il precedente StackTrace?
Varvara Kalinina

1
@VarvaraKalinina Esattamente.

54

Dovresti sapere che a partire da C # 6.0 è possibile usare awaitin catche finallyblocks, quindi potresti in effetti fare questo:

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

Le nuove funzionalità di C # 6.0, inclusa quella appena citata, sono elencate qui o come video qui .


Su Wikipedia è indicato anche il supporto per i blocchi di wait in catch / latest in C # 6.0.
DavidRR

4
@DavidRR. la Wikipedia non è autorevole. È solo un altro sito web tra milioni per quanto riguarda questo aspetto.
user34660

Sebbene ciò sia valido per C # 6, la domanda è stata contrassegnata con C # 5 sin dall'inizio. Questo mi fa chiedere se avere questa risposta qui è fonte di confusione o se dovremmo semplicemente rimuovere il tag di versione specifico in questi casi.
julealgon

16

Se hai bisogno di usare asyncgestori di errori, ti consiglio qualcosa di simile:

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

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

Il problema con il blocco sincrono sul asynccodice (indipendentemente dal thread su cui è in esecuzione) è che stai bloccando in modo sincrono. Nella maggior parte degli scenari, è meglio usare await.

Aggiornamento: poiché è necessario rilanciare, è possibile utilizzare ExceptionDispatchInfo.


1
Grazie ma purtroppo conosco già questo metodo. Questo è quello che faccio normalmente, ma non posso farlo qui. Se ho semplicemente uso throw exception;nella ifdichiarazione sarà persa la traccia dello stack.
user2397050

3

Abbiamo estratto l'ottima risposta di hvd alla seguente classe di utilità riutilizzabile nel nostro progetto:

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

Si userebbe come segue:

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

Sentiti libero di migliorare la denominazione, l'abbiamo mantenuta intenzionalmente dettagliata. Si noti che non è necessario acquisire il contesto all'interno del wrapper poiché è già acquisito nel sito della chiamata, quindi ConfigureAwait(false).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.