Selama beralih ke .NET Core 3 yang baru IAsynsDisposable
, saya menemukan masalah berikut.
Inti dari masalah: jika DisposeAsync
melempar pengecualian, pengecualian ini menyembunyikan setiap pengecualian yang dilemparkan ke dalam await using
-block.
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");
}
}
Yang tertangkap adalah AsyncDispose
-eksepsi jika dilempar, dan pengecualian dari dalam await using
hanya jika AsyncDispose
tidak melempar.
Namun saya lebih suka sebaliknya: mendapatkan pengecualian dari await using
blok jika memungkinkan, dan DisposeAsync
-kecualian hanya jika await using
blok selesai dengan sukses.
Dasar Pemikiran: Bayangkan bahwa kelas saya D
bekerja dengan beberapa sumber daya jaringan dan berlangganan untuk beberapa notifikasi jarak jauh. Kode di dalam await using
dapat melakukan sesuatu yang salah dan gagal saluran komunikasi, setelah itu kode dalam Buang yang mencoba menutup komunikasi dengan anggun (misalnya, berhenti berlangganan pemberitahuan) akan gagal juga. Tetapi pengecualian pertama memberi saya informasi nyata tentang masalah tersebut, dan yang kedua hanyalah masalah sekunder.
Dalam kasus lain ketika bagian utama dijalankan dan pembuangan gagal, masalah sebenarnya ada di dalam DisposeAsync
, jadi pengecualian dari DisposeAsync
adalah yang relevan. Ini berarti bahwa hanya menekan semua pengecualian di dalam DisposeAsync
seharusnya bukan ide yang baik.
Saya tahu bahwa ada masalah yang sama dengan kasus non-async: pengecualian dalam finally
mengabaikan pengecualian try
, itu sebabnya tidak disarankan untuk melempar Dispose()
. Tetapi dengan kelas yang mengakses jaringan menekan pengecualian dalam metode penutupan tidak terlihat bagus sama sekali.
Dimungkinkan untuk mengatasi masalah dengan pembantu berikut:
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 { }
}
}
}
dan menggunakannya seperti
await new D().UsingAsync(d =>
{
throw new ArgumentException("I'm inside using");
});
yang agak jelek (dan melarang hal-hal seperti pengembalian awal di dalam blok menggunakan).
Apakah ada solusi kanonik yang baik, dengan await using
jika memungkinkan? Pencarian saya di internet bahkan tidak membahas masalah ini.
CloseAsync
bahwa saya perlu mengambil tindakan pencegahan ekstra untuk membuatnya berjalan. Jika saya hanya meletakkannya di akhir using
-block, itu akan dilewati pada pengembalian awal dll (ini adalah apa yang kita inginkan terjadi) dan pengecualian (ini adalah apa yang kita inginkan terjadi). Tapi idenya terlihat menjanjikan.
Dispose
selalu menjadi "Ada yang salah: lakukan saja yang terbaik untuk memperbaiki situasi, tetapi jangan membuatnya lebih buruk", dan saya tidak melihat mengapa AsyncDispose
harus ada perbedaan.
DisposeAsync
melakukan yang terbaik untuk merapikan tetapi tidak melempar adalah hal yang benar untuk dilakukan. Anda berbicara tentang pengembalian awal yang disengaja , di mana pengembalian awal yang disengaja dapat secara tidak sengaja mem-bypass panggilan ke CloseAsync
: mereka adalah yang dilarang oleh banyak standar pengkodean.
Close
metode terpisah karena alasan ini. Mungkin bijaksana untuk melakukan hal yang sama:CloseAsync
upaya untuk menutup semuanya dengan baik dan gagal.DisposeAsync
hanya melakukan yang terbaik, dan gagal diam-diam.