Ini adalah terjemahan dari bagian pertama artikel. Artikel ini ditulis pada tahun 2008. Setelah 10 tahun, hampir kehilangan relevansinya.
Pelepasan Sumber Daya Deterministik - Suatu Kebutuhan
Selama lebih dari 20 tahun pengalaman pengkodean, saya terkadang mengembangkan bahasa saya sendiri untuk menyelesaikan masalah. Mereka berkisar dari bahasa sederhana, imperatif hingga ekspresi reguler khusus untuk pohon. Saat membuat bahasa, ada banyak rekomendasi dan beberapa aturan sederhana tidak boleh dilanggar. Salah satunya:
Jangan pernah membuat bahasa pengecualian di mana tidak ada rilis sumber daya deterministik.
Tebak rekomendasi apa yang tidak diikuti oleh .NET runtime, dan sebagai hasilnya, semua bahasa didasarkan padanya?
Alasan aturan ini ada adalah bahwa pelepasan sumber daya deterministik diperlukan untuk membuat program yang didukung . Pelepasan sumber daya yang ditentukan memberikan titik tertentu di mana programmer yakin bahwa sumber daya itu dibebaskan. Ada dua cara untuk menulis program yang andal: pendekatan tradisional adalah melepaskan sumber daya sedini mungkin dan pendekatan modern adalah melepaskan sumber daya untuk waktu yang tidak ditentukan. Keuntungan dari pendekatan modern adalah bahwa programmer tidak perlu secara eksplisit membebaskan sumber daya. Kerugiannya adalah bahwa jauh lebih sulit untuk menulis aplikasi yang dapat diandalkan, ada banyak kesalahan halus. Sayangnya, .NET runtime dibuat menggunakan pendekatan modern.
.NET mendukung pelepasan sumber daya non-deterministik menggunakan metode Finalize
, yang memiliki arti khusus. Untuk rilis sumber daya deterministik, Microsoft juga menambahkan antarmuka IDisposable
(dan kelas lainnya, yang akan kita bahas nanti). Namun demikian, untuk runtime IDisposable
adalah antarmuka normal, seperti yang lainnya. Status "kelas dua" ini menciptakan beberapa kesulitan.
Dalam C #, "rilis deterministik untuk orang miskin" dapat diimplementasikan menggunakan try
dan finally
try
atau using
(yang merupakan hal yang hampir sama). Microsoft telah berdiskusi untuk waktu yang lama apakah akan menghitung jumlah tautan atau tidak, dan menurut saya keputusan yang salah telah dibuat. Akibatnya, untuk pelepasan sumber daya yang deterministik, Anda harus using
konstruksi yang canggung finally
\ using
atau panggilan langsung ke IDisposable.Dispose
, yang penuh dengan kesalahan. Untuk seorang programmer C ++ yang terbiasa menggunakan shared_ptr<T>
kedua opsi tidak menarik. (Kalimat terakhir memperjelas di mana penulis memiliki hubungan seperti itu - kira - kira.
IDisposable
IDisposable
adalah solusi untuk pelepasan sumber daya deterministik yang ditawarkan oleh Misoftro. Salah satunya adalah untuk kasus-kasus berikut:
- Jenis apa pun yang memiliki sumber daya yang dikelola (
IDisposable
). Suatu tipe harus tentu memiliki , yaitu, mengatur waktu hidup, sumber daya, dan tidak hanya merujuk kepada mereka. - Jenis apa pun yang memiliki sumber daya yang tidak dikelola.
- Jenis apa pun yang memiliki sumber daya yang dikelola dan tidak dikelola.
- Jenis apa pun yang diwarisi dari kelas yang mengimplementasikan
IDisposable
. Saya tidak merekomendasikan mewarisi dari kelas yang memiliki sumber daya yang tidak dikelola. Lebih baik gunakan lampiran.
IDisposable
membantu membebaskan sumber daya secara deterministik, tetapi memiliki masalah sendiri.
Kesulitan IDisposable - Kegunaan
Objek IDisposable
digunakan cukup rumit. Menggunakan objek harus dibungkus using
konstruksi. Berita buruknya adalah bahwa C # tidak mengizinkan using
dengan tipe yang tidak menerapkan IDisposable
. Oleh karena itu, programmer harus merujuk pada dokumentasi setiap kali untuk memahami apakah perlu menulis using
, atau hanya menulis using
mana using
mana, dan kemudian menghapus di mana compiler bersumpah.
Managed C ++ jauh lebih baik dalam hal ini. Ini mendukung semantik tumpukan untuk jenis referensi , yang berfungsi hanya using
untuk jenis yang diperlukan. C # dapat mengambil manfaat dari kemampuan menulis using
jenis apa pun.
Masalah ini dapat diatasi dengan. alat analisis kode. Untuk membuat keadaan menjadi lebih buruk, jika Anda lupa menggunakan, program dapat lulus tes, tetapi crash saat bekerja "di ladang."
Alih-alih menghitung tautan, IDisposable
memiliki masalah lain - menentukan pemilik. Ketika di C ++ salinan terakhir dari shared_ptr<T>
keluar dari ruang lingkup, sumber daya dibebaskan segera, tidak perlu berpikir siapa yang harus bebas. IDisposable
sebaliknya, memaksa programmer untuk menentukan siapa yang "memiliki" objek dan bertanggung jawab untuk melepaskannya. Terkadang kepemilikan jelas: ketika satu objek merangkum objek lain dan itu sendiri mengimplementasikan IDisposable
, oleh karena itu bertanggung jawab atas pelepasan objek anak. Terkadang umur suatu objek ditentukan oleh satu blok kode, dan programmer hanya menggunakan di sekitar blok ini. Namun demikian, ada banyak kasus di mana suatu objek dapat digunakan di beberapa tempat dan masa hidupnya sulit untuk ditentukan (walaupun dalam kasus ini jumlah referensi akan baik-baik saja).
IDisposable Kesulitan - Kompatibilitas Mundur
Menambahkan IDisposable
ke kelas dan menghapus IDisposable
dari daftar antarmuka yang diimplementasikan merupakan perubahan besar. Kode klien yang tidak mengharapkan IDisposable
tidak akan membebaskan sumber daya jika Anda menambahkan IDisposable
ke salah satu kelas Anda yang diteruskan dengan referensi ke antarmuka atau kelas dasar.
Microsoft sendiri mengalami masalah ini. IEnumerator
tidak diwarisi dari IDisposable
, dan IEnumerator<T>
diwarisi. Jika Anda meneruskan IEnumerator<T>
kode yang menerima IEnumerator
, Dispose
tidak akan dipanggil.
Ini bukan akhir dunia, tetapi memberikan beberapa esensi sekunder IDisposable
.
Kesulitan IDisposable - Merancang Hirarki Kelas
Kelemahan terbesar yang disebabkan oleh IDisposable
di bidang desain hierarki adalah bahwa setiap kelas dan antarmuka harus memprediksi apakah IDisposable
akan dibutuhkan oleh turunannya.
Jika antarmuka tidak mewarisi IDisposable
, tetapi kelas yang mengimplementasikan antarmuka juga menerapkan IDisposable
, maka kode akhir akan mengabaikan rilis deterministik, atau harus memeriksa apakah objek mengimplementasikan antarmuka IDisposable
. Tetapi untuk ini, tidak mungkin menggunakan konstruk penggunaan dan Anda harus menulis try
jelek dan finally
.
Singkatnya, IDisposable
mempersulit pengembangan perangkat lunak yang dapat digunakan kembali. Alasan utamanya adalah pelanggaran terhadap salah satu prinsip desain berorientasi objek - pemisahan antarmuka dan implementasi. Rilis sumber daya harus merupakan detail implementasi. Microsoft memutuskan untuk membuat rilis sumber daya deterministik antarmuka kelas dua.
Salah satu solusi yang tidak begitu indah adalah membuat semua kelas mengimplementasikan IDisposable
, tetapi di sebagian besar kelas, IDisposable.Dispose
tidak akan melakukan apa pun. Tapi ini tidak terlalu cantik.
Kesulitan lain dengan IDisposable
adalah koleksi. Beberapa koleksi "memiliki" objek di dalamnya, dan beberapa tidak. Namun, koleksi itu sendiri tidak menerapkan IDisposable
. Programmer harus ingat untuk memanggil IDisposable.Dispose
objek dalam koleksi, atau buat turunannya sendiri dari kelas koleksi yang mengimplementasikan IDisposable
berarti kepemilikan.
IDisposable Kesulitan - tambahan status "salah"
IDisposable
dapat dipanggil secara eksplisit kapan saja, terlepas dari masa pakai objek. Artinya, keadaan "dirilis" ditambahkan ke setiap objek, di mana dianjurkan untuk melempar ObjectDisposedException
. Memeriksa status dan melempar pengecualian adalah biaya tambahan.
Alih-alih memeriksa setiap bersin, lebih baik mempertimbangkan mengakses objek dalam keadaan "bebas" sebagai "perilaku tidak terdefinisi" sebagai panggilan ke memori yang dibebaskan.
Kesulitan IDisposable - tidak ada jaminan
IDisposable
hanyalah sebuah antarmuka. Kelas yang mengimplementasikan IDisposable
mendukung rilis deterministik, tetapi tidak menjaminnya . Untuk kode klien, tidak masalah untuk tidak memanggil Dispose
. Oleh karena itu, kelas yang mengimplementasikan IDisposable
harus mendukung rilis deterministik dan non-deterministik.
Complexities IDisposable - Implementasi Kompleks
Microsoft menawarkan pola penerapan IDisposable
. (Sebelumnya, ada pola yang secara umum mengerikan, tetapi relatif baru-baru ini, setelah kemunculan .NET 4, dokumentasinya diperbaiki, termasuk di bawah pengaruh artikel ini. Dalam edisi lama buku .NET, Anda dapat menemukan versi yang lama. - perkiraan )
IDisposable.Dispose
mungkin tidak dipanggil sama sekali, sehingga kelas harus menyertakan finalizer ke sumber daya gratis.IDisposable.Dispose
bisa dipanggil beberapa kali dan harus bekerja tanpa efek samping yang terlihat. Oleh karena itu, perlu untuk menambahkan verifikasi apakah metode tersebut telah dipanggil atau belum.- Finalizers dipanggil dalam utas terpisah dan dapat dipanggil sebelum
IDisposable.Dispose
. IDisposable.Dispose
. Penggunaan GC.SuppressFinalize
untuk menghindari "balapan" tersebut.
Selain itu:
- Finalizers dipanggil, termasuk untuk objek yang melempar pengecualian pada konstruktor. Oleh karena itu, kode rilis harus bekerja dengan objek yang diinisialisasi sebagian.
- Menerapkan
IDisposable
di kelas yang diwarisi dari CriticalFinalizerObject
membutuhkan konstruksi non-sepele. void Dispose(bool disposing)
adalah metode viral dan harus dieksekusi di Wilayah Eksekusi Terbatas , yang mengharuskan panggilan ke RuntimeHelpers.PrepareMethod
.
Kesulitan IDisposable - Tidak Cocok untuk Penyelesaian Logika
Mematikan suatu objek - sering terjadi dalam program-program dalam urutan paralel atau asinkron. Misalnya, kelas menggunakan utas terpisah dan ingin melengkapinya menggunakan ManualResetEvent
. Ini dapat dilakukan di IDisposable.Dispose
, tetapi dapat menyebabkan kesalahan jika kode dipanggil di finalizer.
Untuk memahami batasan dalam finalizer, Anda perlu memahami cara kerja pemulung. Di bawah ini adalah diagram yang disederhanakan di mana banyak detail terkait dengan generasi, tautan lemah, pemulihan objek, pengumpulan sampah latar belakang, dll. Dihilangkan.
Pengumpul sampah .NET menggunakan algoritma mark-and-sweep. Secara umum, logikanya terlihat seperti ini:
- Jeda semua utas.
- Ambil semua objek root: variabel pada stack, bidang statis, objek
GCHandle
, antrian finalisasi. Dalam kasus pembongkaran domain aplikasi (penghentian program), dianggap bahwa variabel dalam stack dan bidang statis bukanlah root. - Secara rekursif menelusuri semua tautan dari objek dan menandainya sebagai "terjangkau".
- Pergi melalui semua objek lain yang memiliki destruktor (finalizer), menyatakan mereka dapat dijangkau, dan menempatkan mereka dalam antrian finalisasi (
GC.SuppressFinalize
memberitahu GC untuk tidak melakukan ini). Objek diantrekan dalam urutan yang tidak terduga.
Di latar belakang, aliran (atau beberapa) finalisasi berfungsi:
- Mengambil objek dari antrian dan mulai menyelesaikannya. Dimungkinkan untuk menjalankan beberapa finalizer dari objek yang berbeda secara bersamaan.
- Objek dihapus dari antrian, dan jika tidak ada orang lain yang merujuknya, itu akan dihapus pada pengumpulan sampah berikutnya.
Sekarang harus jelas mengapa tidak mungkin untuk mengakses sumber daya yang dikelola dari finalizer - Anda tidak tahu dalam urutan apa disebut finalizer. Bahkan memanggil IDisposable.Dispose
objek lain dari finalizer dapat menyebabkan kesalahan, karena kode rilis sumber daya dapat bekerja di utas lainnya.
Ada beberapa pengecualian ketika Anda dapat mengakses sumber daya yang dikelola dari finalizer:
- Finalisasi objek yang diwarisi dari
CriticalFinalizerObject
dilakukan setelah finalisasi objek yang tidak diwarisi dari kelas ini. Ini berarti Anda bisa memanggil ManualResetEvent
dari finalizer hingga kelas diwarisi dari CriticalFinalizerObject
- Beberapa objek dan metode khusus, seperti Konsol dan beberapa metode Thread. Mereka dapat dipanggil dari finalizers bahkan jika program berakhir.
Dalam kasus umum, lebih baik tidak mengakses sumber daya yang dikelola dari finalizer. Namun demikian, logika penyelesaian diperlukan untuk perangkat lunak non-sepele. Pada Windows.Forms
berisi logika penyelesaian dalam metode Application.Exit
. Saat Anda mengembangkan pustaka komponen Anda, hal terbaik yang harus dilakukan adalah menyelesaikan logika penyelesaian dengan IDisposable
. Pengakhiran normal jika terjadi panggilan IDisposable.Dispose
dan darurat jika tidak.
Microsoft juga mengalami masalah ini. Kelas StreamWriter
memiliki objek Stream
(tergantung pada parameter konstruktor di versi terbaru - sekitar Per. ). StreamWriter.Close
flushes buffer dan panggilan Stream.Close
(juga terjadi jika dibungkus using
- kira - kira Per. ). Jika StreamWriter
tidak ditutup, buffer tidak memerah dan obrolan data hilang. Microsoft tidak mendefinisikan ulang finalizer, sehingga "menyelesaikan" masalah penyelesaian. Contoh yang bagus tentang perlunya logika penyelesaian.
Saya sarankan membaca
Banyak informasi tentang .NET internal dalam artikel ini berasal dari CLR Jeffrey Richter via C #. Jika Anda belum memilikinya, maka belilah . Serius. Ini adalah pengetahuan yang diperlukan untuk setiap programmer C #.
Kesimpulan dari penerjemah
Sebagian besar .NET programmer tidak akan pernah menemui masalah yang dijelaskan dalam artikel ini. .NET akan berevolusi untuk meningkatkan tingkat abstraksi dan mengurangi kebutuhan akan "juggling" sumber daya yang tidak dikelola. Namun demikian, artikel ini bermanfaat karena menjelaskan detail mendalam dari hal-hal sederhana dan dampaknya pada desain kode.
Bagian selanjutnya akan menjadi diskusi terperinci tentang cara bekerja dengan sumber daya yang dikelola dan tidak dikelola di .NET dengan banyak contoh.