Durante il passaggio ai nuovi .NET Core 3 IAsynsDisposable, mi sono imbattuto nel seguente problema.
Il nocciolo del problema: se DisposeAsyncgenera un'eccezione, questa eccezione nasconde tutte le eccezioni generate all'interno del await usingblocco.
class Program
{
static async Task Main()
{
try
{
await using (var d = new D())
{
throw new ArgumentException("I'm inside using");
}
}
catch (Exception e)
{
Console.WriteLine(e.Message); // prints I'm inside dispose
}
}
}
class D : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await Task.Delay(1);
throw new Exception("I'm inside dispose");
}
}
Ciò che viene catturato è l' AsyncDisposeeccezione se viene generata e l'eccezione dall'interno await usingsolo se AsyncDisposenon viene generata .
Preferirei comunque il contrario: ottenere l'eccezione dal await usingblocco, se possibile, e DisposeAsync-eccezione solo se il await usingblocco è stato completato correttamente.
Motivazione: Immagina che la mia classe Dlavori con alcune risorse di rete e si iscriva per alcune notifiche remote. Il codice all'interno await usingpuò fare qualcosa di sbagliato e fallire il canale di comunicazione, dopodiché anche il codice in Dispose che tenta di chiudere con garbo la comunicazione (ad esempio, annullare l'iscrizione alle notifiche) fallirebbe. Ma la prima eccezione mi dà le informazioni reali sul problema, e la seconda è solo un problema secondario.
Nell'altro caso quando la parte principale è passata e lo smaltimento non è riuscito, il vero problema è all'interno DisposeAsync, quindi l'eccezione DisposeAsyncè quella rilevante. Ciò significa che sopprimere tutte le eccezioni al suo interno DisposeAsyncnon dovrebbe essere una buona idea.
So che c'è lo stesso problema con il caso non asincrono: l'eccezione finallysovrascrive l'eccezione try, ecco perché non è consigliabile lanciare Dispose(). Ma con le classi che accedono alla rete, sopprimere le eccezioni nei metodi di chiusura non sembra affatto buono.
È possibile aggirare il problema con il seguente aiuto:
static class AsyncTools
{
public static async Task UsingAsync<T>(this T disposable, Func<T, Task> task)
where T : IAsyncDisposable
{
bool trySucceeded = false;
try
{
await task(disposable);
trySucceeded = true;
}
finally
{
if (trySucceeded)
await disposable.DisposeAsync();
else // must suppress exceptions
try { await disposable.DisposeAsync(); } catch { }
}
}
}
e usalo come
await new D().UsingAsync(d =>
{
throw new ArgumentException("I'm inside using");
});
che è un po 'brutto (e non consente cose come i primi ritorni all'interno del blocco using).
Esiste una buona soluzione canonica, await usingse possibile? La mia ricerca su Internet non ha trovato nemmeno discutere questo problema.
CloseAsyncmezzo separato devo prendere delle precauzioni extra per farlo funzionare. Se lo metto solo alla fine di using-block, verrà ignorato sui rendimenti iniziali ecc. (Questo è ciò che vorremmo che accadesse) ed eccezioni (questo è ciò che vorremmo che accadesse). Ma l'idea sembra promettente.
Disposeè sempre stato "Le cose potrebbero essere andate male: fai del tuo meglio per migliorare la situazione, ma non peggiorare", e non vedo perché AsyncDisposedovrebbe essere diverso.
DisposeAsynccosa migliore per riordinare ma non buttare è la cosa giusta da fare. Stavi parlando di ritorni anticipati intenzionali , in cui un ritorno anticipato intenzionale potrebbe erroneamente aggirare una chiamata a CloseAsync: quelli sono quelli proibiti da molti standard di codifica.
Closemetodo separato proprio per questo motivo. Probabilmente è saggio fare lo stesso:CloseAsynctenta di chiudere bene le cose e genera un fallimento.DisposeAsyncfa solo del suo meglio e fallisce in silenzio.