Pertanyaan: Mengapa Java / C # tidak bisa mengimplementasikan RAII?
Klarifikasi: Saya tahu pengumpul sampah tidak deterministik. Jadi dengan fitur bahasa saat ini, metode Buang () objek tidak mungkin dipanggil secara otomatis pada scope exit. Tetapi dapatkah fitur deterministik seperti itu ditambahkan?
Pemahaman saya:
Saya merasa implementasi RAII harus memenuhi dua persyaratan:
1. Masa pakai sumber daya harus terikat pada ruang lingkup.
2. Tersirat. Pembebasan sumber daya harus terjadi tanpa pernyataan eksplisit oleh programmer. Dianalogikan dengan pengumpul sampah yang membebaskan memori tanpa pernyataan eksplisit. "Implisit" hanya perlu terjadi pada saat penggunaan kelas. Pembuat perpustakaan kelas tentu saja harus secara eksplisit menerapkan metode destruktor atau Buang ().
Java / C # memuaskan poin 1. Dalam C # sumber daya mengimplementasikan IDisposable dapat terikat ke lingkup "menggunakan":
void test()
{
using(Resource r = new Resource())
{
r.foo();
}//resource released on scope exit
}
Ini tidak memenuhi poin 2. Programmer harus secara eksplisit mengikat objek ke lingkup "menggunakan" khusus. Pemrogram dapat (dan memang) lupa untuk secara eksplisit mengikat sumber daya ke ruang lingkup, menciptakan kebocoran.
Faktanya, blok "using" dikonversi menjadi kode try-akhirnya-buang () oleh kompiler. Ini memiliki sifat eksplisit yang sama dari pola coba-akhirnya-buang (). Tanpa pelepasan implisit, kait ke lingkup adalah gula sintaksis.
void test()
{
//Programmer forgot (or was not aware of the need) to explicitly
//bind Resource to a scope.
Resource r = new Resource();
r.foo();
}//resource leaked!!!
Saya pikir layak membuat fitur bahasa di Java / C # yang memungkinkan objek khusus yang terhubung ke stack melalui smart-pointer. Fitur ini akan memungkinkan Anda untuk menandai kelas sebagai lingkup-terikat, sehingga selalu dibuat dengan kait ke tumpukan. Mungkin ada opsi untuk berbagai jenis pointer cerdas.
class Resource - ScopeBound
{
/* class details */
void Dispose()
{
//free resource
}
}
void test()
{
//class Resource was flagged as ScopeBound so the tie to the stack is implicit.
Resource r = new Resource(); //r is a smart-pointer
r.foo();
}//resource released on scope exit.
Saya pikir implisit adalah "sepadan". Sama seperti implikasi dari pengumpulan sampah adalah "sepadan". Eksplisit menggunakan blok menyegarkan di mata, tetapi tidak menawarkan keuntungan semantik dari coba-akhirnya buang ()
Apakah tidak praktis untuk mengimplementasikan fitur seperti itu ke dalam bahasa Java / C #? Bisakah itu diperkenalkan tanpa melanggar kode lama?
Dispose
s pernah dijalankan, terlepas dari bagaimana mereka dipicu. Menambahkan penghancuran implisit di akhir ruang lingkup tidak akan membantu itu.using
pelaksanaanDispose
yang dijamin (baik, mendiskontokan proses tiba-tiba mati tanpa pengecualian yang dilemparkan, di mana titik semua pembersihan mungkin menjadi diperdebatkan).struct
), tetapi mereka biasanya dihindari kecuali dalam kasus yang sangat khusus. Lihat juga .Jawaban:
Perpanjangan bahasa seperti itu akan jauh lebih rumit dan invasif daripada yang Anda kira. Anda tidak bisa menambahkan begitu saja
ke bagian yang relevan dari spesifikasi bahasa dan dilakukan. Saya akan mengabaikan masalah nilai sementara (
new Resource().doSomething()
) yang dapat diselesaikan dengan kata-kata yang sedikit lebih umum, ini bukan masalah yang paling serius. Sebagai contoh, kode ini akan rusak (dan hal semacam ini mungkin menjadi mustahil dilakukan secara umum):Sekarang Anda perlu menyalin konstruktor yang ditentukan pengguna (atau memindahkan konstruktor) dan mulai memohonnya di mana-mana. Ini tidak hanya membawa implikasi kinerja, itu juga membuat hal-hal ini secara efektif menilai tipe, sedangkan hampir semua objek lainnya adalah tipe referensi. Dalam kasus Jawa, ini adalah penyimpangan radikal dari cara kerja benda. Dalam C # kurang begitu (sudah memiliki
struct
s, tetapi tidak ada konstruktor salinan yang ditetapkan pengguna untuk mereka AFAIK), tetapi masih membuat objek RAII ini lebih istimewa. Atau, versi terbatas dari tipe linier (lih. Karat) juga dapat menyelesaikan masalah, dengan biaya melarang aliasing termasuk melewati parameter (kecuali jika Anda ingin memperkenalkan lebih banyak kompleksitas dengan mengadopsi referensi yang dipinjam seperti Rust dan pemeriksa pinjaman).Hal ini dapat dilakukan secara teknis, tetapi Anda berakhir dengan kategori hal-hal yang sangat berbeda dari semua hal lain dalam bahasa. Ini hampir selalu merupakan ide yang buruk, dengan konsekuensi bagi pelaksana (lebih banyak kasus tepi, lebih banyak waktu / biaya di setiap departemen) dan pengguna (lebih banyak konsep untuk dipelajari, lebih banyak kemungkinan bug). Ini tidak sebanding dengan kenyamanan tambahan.
sumber
File
cara ini, tidak ada yang berubah danDispose
tidak pernah dipanggil. Jika Anda selalu meneleponDispose
, Anda tidak dapat melakukan apa pun dengan benda sekali pakai. Atau Anda mengusulkan beberapa skema untuk kadang-kadang membuang dan kadang-kadang tidak? Jika demikian, tolong jelaskan secara rinci dan saya akan memberi tahu Anda situasi di mana ia gagal.Dispose
jika referensi lolos? Analisis melarikan diri adalah masalah lama dan sulit, ini tidak akan selalu berhasil tanpa perubahan lebih lanjut ke bahasa. Ketika referensi dilewatkan ke metode (virtual) lain (something.EatFile(f);
), harusf.Dispose
dipanggil di akhir ruang lingkup? Jika ya, Anda memutuskan penelepon yang menyimpanf
untuk digunakan nanti. Jika tidak, Anda membocorkan sumber daya jika penelepon tidak menyimpanf
. Satu-satunya cara yang agak sederhana untuk menghapus ini adalah sistem tipe linear, yang (seperti yang sudah saya bahas nanti dalam jawaban saya) malah memperkenalkan banyak komplikasi lain.Kesulitan terbesar dalam mengimplementasikan sesuatu seperti ini untuk Java atau C # adalah menentukan cara kerja transfer sumber daya. Anda perlu beberapa cara untuk memperpanjang umur sumber daya di luar ruang lingkup. Mempertimbangkan:
Yang lebih buruk adalah bahwa hal ini mungkin tidak jelas bagi pelaksana
IWrapAResource
:Sesuatu seperti
using
pernyataan C # mungkin sedekat Anda akan memiliki semantik RAII tanpa menggunakan referensi sumber penghitungan atau memaksa semantik nilai di mana saja seperti C atau C ++. Karena Java dan C # memiliki pembagian sumber daya secara implisit yang dikelola oleh seorang pengumpul sampah, minimum yang harus dapat dilakukan oleh seorang programmer adalah memilih ruang lingkup yang terikat oleh suatu sumber daya, yang persis seperti yangusing
sudah dilakukan.sumber
using
pernyataan itu.IWrapSomething
untuk membuangT
. Siapa pun yang diciptakanT
perlu khawatir tentang itu, apakah menggunakanusing
, menjadiIDisposable
dirinya sendiri, atau memiliki beberapa skema siklus sumber daya ad-hoc.Alasan mengapa RAII tidak dapat bekerja dalam bahasa seperti C #, tetapi bekerja di C ++, adalah karena dalam C ++ Anda dapat memutuskan apakah suatu objek benar-benar sementara (dengan mengalokasikannya pada tumpukan) atau apakah itu berumur panjang (oleh mengalokasikannya di heap menggunakan
new
dan menggunakan pointer).Jadi, di C ++, Anda dapat melakukan sesuatu seperti ini:
Dalam C #, Anda tidak dapat membedakan antara dua kasus, sehingga kompiler tidak akan tahu apakah akan menyelesaikan objek atau tidak.
Apa yang dapat Anda lakukan adalah memperkenalkan beberapa jenis variabel lokal khusus, yang tidak dapat Anda masukkan ke dalam kolom, dll. * Dan itu akan secara otomatis dibuang ketika keluar dari ruang lingkup. Itulah yang dilakukan oleh C ++ / CLI. Di C ++ / CLI, Anda menulis kode seperti ini:
Ini mengkompilasi IL pada dasarnya sama dengan C # berikut:
Untuk menyimpulkan, jika saya menebak mengapa para perancang C # tidak menambahkan RAII, itu karena mereka berpikir bahwa memiliki dua jenis variabel lokal tidak sepadan, sebagian besar karena dalam bahasa dengan GC, finalisasi deterministik tidak berguna sehingga sering.
* Bukan tanpa yang setara dengan
&
operator, yang di C ++ / CLI adalah%
. Meskipun melakukannya adalah "tidak aman" dalam arti bahwa setelah metode berakhir, bidang akan merujuk objek yang dibuang.sumber
struct
tipe seperti D.Jika yang membuat Anda
using
gelisah adalah kesederhanaan mereka, mungkin kita bisa mengambil langkah kecil menuju yang kurang jelas, daripada mengubah spesifikasi C # itu sendiri. Pertimbangkan kode ini:Lihat
local
kata kunci yang saya tambahkan? Semua hal ini adalah menambahkan sedikit gula lebih sintaksis, sepertiusing
, memerintahkan compiler untuk memanggilDispose
dalamfinally
blok pada akhir lingkup variabel ini. Itu semuanya. Ini sama dengan:tetapi dengan ruang lingkup implisit, bukan yang eksplisit. Ini lebih sederhana daripada saran lain karena saya tidak harus memiliki kelas yang didefinisikan sebagai lingkup-terikat. Gula sintaksis yang lebih bersih dan lebih implisit.
Mungkin ada masalah di sini dengan cakupan yang sulit untuk diselesaikan, meskipun saya tidak bisa melihatnya sekarang, dan saya akan menghargai siapa pun yang dapat menemukannya.
sumber
using
kata kunci, kami dapat menjaga perilaku yang ada dan menggunakannya juga, untuk kasus-kasus di mana kami tidak memerlukan cakupan spesifik. Memiliki brace-lessusing
default untuk lingkup saat ini.Untuk contoh bagaimana RAII bekerja dalam bahasa yang dikumpulkan sampah, periksa
with
kata kunci dengan Python . Alih-alih mengandalkan objek yang hancur secara deterministik, Anda dapat mengasosiasikan__enter__()
dan__exit__()
metode ke lingkup leksikal tertentu. Contoh umum adalah:Seperti halnya gaya RAII C ++, file akan ditutup saat keluar dari blok itu, tidak peduli apakah itu jalan keluar 'normal', a
break
, langsungreturn
atau pengecualian.Perhatikan bahwa
open()
panggilan adalah fungsi pembukaan file yang biasa. untuk membuat ini berfungsi, objek file yang dikembalikan mencakup dua metode:Ini adalah ungkapan umum dalam Python: objek yang dikaitkan dengan sumber daya biasanya mencakup dua metode ini.
Perhatikan bahwa objek file masih dapat tetap dialokasikan setelah
__exit__()
panggilan, yang penting adalah bahwa itu ditutup.sumber
with
di Python hampir persis sepertiusing
di C #, dan karena itu tidak RAII sejauh pertanyaan ini diperhatikan.defer
dalam bahasa Go).