Durante il passaggio ai nuovi .NET Core 3 IAsynsDisposable
, mi sono imbattuto nel seguente problema.
Il nocciolo del problema: se DisposeAsync
genera un'eccezione, questa eccezione nasconde tutte le eccezioni generate all'interno del await using
blocco.
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' AsyncDispose
eccezione se viene generata e l'eccezione dall'interno await using
solo se AsyncDispose
non viene generata .
Preferirei comunque il contrario: ottenere l'eccezione dal await using
blocco, se possibile, e DisposeAsync
-eccezione solo se il await using
blocco è stato completato correttamente.
Motivazione: Immagina che la mia classe D
lavori con alcune risorse di rete e si iscriva per alcune notifiche remote. Il codice all'interno await using
può 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 DisposeAsync
non dovrebbe essere una buona idea.
So che c'è lo stesso problema con il caso non asincrono: l'eccezione finally
sovrascrive 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 using
se possibile? La mia ricerca su Internet non ha trovato nemmeno discutere questo problema.
CloseAsync
mezzo 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é AsyncDispose
dovrebbe essere diverso.
DisposeAsync
cosa 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.
Close
metodo separato proprio per questo motivo. Probabilmente è saggio fare lo stesso:CloseAsync
tenta di chiudere bene le cose e genera un fallimento.DisposeAsync
fa solo del suo meglio e fallisce in silenzio.