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.
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.CloseAsync
bahwa saya perlu mengambil tindakan pencegahan ekstra untuk membuatnya berjalan. Jika saya hanya meletakkannya di akhirusing
-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 mengapaAsyncDispose
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 keCloseAsync
: mereka adalah yang dilarang oleh banyak standar pengkodean.Jawaban:
Ada pengecualian yang ingin Anda kemukakan (mengganggu permintaan saat ini, atau menghentikan proses), dan ada pengecualian bahwa desain Anda mengharapkan akan terjadi kadang-kadang dan Anda dapat menanganinya (misalnya coba lagi dan lanjutkan).
Tetapi membedakan antara kedua jenis ini tergantung pada penelepon utama kode - ini adalah inti dari semua pengecualian, untuk menyerahkan keputusan kepada penelepon.
Kadang - kadang penelepon akan menempatkan prioritas lebih besar pada memunculkan pengecualian dari blok kode asli, dan kadang - kadang pengecualian dari
Dispose
. Tidak ada aturan umum untuk memutuskan mana yang harus diprioritaskan. CLR setidaknya konsisten (seperti yang telah Anda catat) antara perilaku sinkronisasi dan non-asinkron.Sangat disayangkan bahwa sekarang kita harus
AggregateException
mewakili beberapa pengecualian, tidak dapat dipasang untuk menyelesaikan ini. yaitu jika pengecualian sudah dalam penerbangan, dan yang lain dilemparkan, mereka digabungkan menjadiAggregateException
. Thecatch
mekanisme dapat dimodifikasi sehingga jika Anda menuliscatch (MyException)
maka akan menangkap setiapAggregateException
yang mencakup pengecualian dari jenisMyException
. Ada berbagai komplikasi lain yang berasal dari ide ini, dan mungkin terlalu berisiko untuk memodifikasi sesuatu yang begitu mendasar sekarang.Anda dapat meningkatkan Anda
UsingAsync
untuk mendukung pengembalian nilai lebih awal:sumber
await using
dapat digunakan (ini adalah tempat DisposeAsync tidak akan melempar dalam kasus non-fatal), dan penolong sepertiUsingAsync
lebih tepat (jika DisposeAsync cenderung membuang) ? (Tentu saja, saya perlu memodifikasiUsingAsync
agar tidak membabi buta menangkap segalanya, tetapi hanya non-fatal (dan tidak bertulang dalam penggunaan Eric Lippert ).)Mungkin Anda sudah mengerti mengapa ini terjadi, tetapi perlu diuraikan. Perilaku ini tidak spesifik untuk
await using
. Itu akan terjadi denganusing
balok polos juga. Jadi sementara saya katakan diDispose()
sini, itu semua berlaku untukDisposeAsync()
juga.Satu
using
blok hanyalah gula sintaksis untuk atry
/finally
blok, seperti yang dikatakan di bagian dokumentasi . Apa yang Anda lihat terjadi karenafinally
blok selalu berjalan, bahkan setelah pengecualian. Jadi jika pengecualian terjadi, dan tidak adacatch
blok, pengecualian ditunda hinggafinally
blok berjalan, dan kemudian pengecualian dilemparkan. Tetapi jika pengecualian terjadi difinally
, Anda tidak akan pernah melihat pengecualian lama.Anda dapat melihat ini dengan contoh ini:
Tidak masalah apakah
Dispose()
atauDisposeAsync()
disebut di dalamfinally
. Tingkah lakunya sama.Pikiran pertama saya adalah: jangan menyerah
Dispose()
. Tetapi setelah meninjau beberapa kode Microsoft sendiri, saya pikir itu tergantung.Lihatlah implementasi mereka
FileStream
, misalnya. BaikDispose()
metode sinkron , danDisposeAsync()
sebenarnya bisa melempar pengecualian. SinkronDispose()
tidak mengabaikan beberapa pengecualian sengaja, tapi tidak semua.Tetapi saya pikir penting untuk mempertimbangkan sifat kelas Anda. Dalam
FileStream
, misalnya,Dispose()
akan menyiram buffer ke sistem file. Itu adalah tugas yang sangat penting dan Anda perlu tahu apakah itu gagal . Anda tidak bisa mengabaikannya begitu saja.Namun, pada jenis objek lainnya, saat Anda menelepon
Dispose()
, Anda benar-benar tidak lagi menggunakan objek tersebut. MemanggilDispose()
benar-benar hanya berarti "objek ini mati bagiku". Mungkin membersihkan beberapa memori yang dialokasikan, tetapi gagal tidak mempengaruhi pengoperasian aplikasi Anda dengan cara apa pun. Jika demikian, Anda mungkin memutuskan untuk mengabaikan pengecualian di dalamDispose()
.Tetapi bagaimanapun juga, jika Anda ingin membedakan antara pengecualian di dalam
using
atau pengecualian yang berasalDispose()
, maka Anda memerlukantry
/catch
blok baik di dalam maupun di luarusing
blok Anda :Atau Anda tidak bisa menggunakannya
using
. Tuliskantry
/catch
/finally
blokir diri Anda sendiri, di mana Anda mendapatkan pengecualian difinally
:sumber
catch
dalam satuusing
blok tidak akan membantu karena biasanya pengecualian penanganan dilakukan di suatu tempat jauh dariusing
blok itu sendiri, sehingga penanganan di dalamusing
biasanya tidak terlalu praktis. Tentang tidak menggunakanusing
- apakah ini benar-benar lebih baik daripada solusi yang diusulkan?try
/catch
/finally
blok karena akan segera jelas apa yang dilakukannya tanpa harus membaca apa yangAsyncUsing
sedang dilakukan. Anda juga menyimpan opsi untuk melakukan pengembalian lebih awal. Anda juga akan dikenakan biaya CPU tambahanAwaitUsing
. Itu akan kecil, tapi ada di sana.Dispose()
tidak boleh melempar karena dipanggil lebih dari sekali. Implementasi Microsoft sendiri dapat membuang pengecualian, dan untuk alasan yang baik, seperti yang saya tunjukkan dalam jawaban ini. Namun, saya setuju bahwa Anda harus menghindarinya jika mungkin karena tidak ada yang biasanya berharap untuk membuangnya.menggunakan secara efektif Kode Penanganan Pengecualian (gula sintaks untuk dicoba ... akhirnya ... Buang ()).
Jika pengecualian Anda menangani kode melempar Pengecualian, ada sesuatu yang rusak secara meriah.
Apa pun yang terjadi bahkan membawa Anda ke sana, tidak benar-benar mater lagi. Kode penanganan Pengecualian yang salah akan menyembunyikan semua kemungkinan pengecualian, dengan satu atau lain cara. Kode penanganan pengecualian harus diperbaiki, yang memiliki prioritas absolut. Tanpa itu, Anda tidak akan pernah mendapatkan data debug yang cukup untuk masalah sebenarnya. Saya melihat kesalahan itu sering dilakukan secara ekstrem. Ini tentang kesalahan yang mudah, seperti menangani pointer telanjang. Begitu sering, ada dua Artikel tentang tautan saya tematik, yang dapat membantu Anda dengan konsep desain yang salah:
Bergantung pada klasifikasi Pengecualian, inilah yang perlu Anda lakukan jika kode Penanganan / Dipose Pengecualian Anda mengeluarkan suatu Pengecualian:
Untuk Fatal, Boneheaded dan Vexing solusinya sama.
Pengecualian eksogen, harus dihindari bahkan dengan biaya serius. Ada alasan mengapa kami masih menggunakan log file daripada mencatat basis data untuk mencatat pengecualian - DB Opeartions adalah cara mudah untuk mengalami masalah eksogen. File log adalah satu-satunya kasus, di mana saya bahkan tidak keberatan jika Anda menyimpan File Handle. Buka seluruh Runtime.
Jika Anda harus menutup koneksi, jangan terlalu khawatir tentang ujung yang lain. Tangani seperti yang dilakukan UDP: "Saya akan mengirim informasi, tetapi saya tidak peduli jika pihak lain yang mendapatkannya." Membuang adalah tentang membersihkan sumber daya di sisi klien / sisi yang Anda kerjakan.
Saya dapat mencoba memberi tahu mereka. Tapi membersihkan barang-barang di Sisi Server / FS? Itulah yang mereka timeout dan mereka penanganan eksepsi bertanggung jawab untuk.
sumber
NativeMethods.UnmapViewOfFile();
danNativeMethods.CloseHandle()
. Tapi itu diimpor dari luar. Tidak ada pemeriksaan nilai pengembalian atau apa pun yang dapat digunakan untuk mendapatkan .NET Exception yang tepat di sekitar apa pun yang mungkin ditemui keduanya. Jadi saya sangat berasumsi, SqlConnection.Dispose (bool) tidak peduli. | Tutup jauh lebih baik, sebenarnya memberitahu server. Sebelum itu panggilan buang.Anda dapat mencoba menggunakan AggregateException dan mengubah sesuatu kode Anda seperti ini:
https://docs.microsoft.com/ru-ru/dotnet/api/system.aggregateexception?view=netframework-4.8
https://docs.microsoft.com/ru-ru/dotnet/standard/parallel-programming/exception-handling-task-parallel-library
sumber