Mengapa Java / C # tidak bisa mengimplementasikan RAII?

29

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?

mike30
sumber
3
Itu tidak praktis, tidak mungkin . Standar C # tidak menjamin destruktor / Disposes pernah dijalankan, terlepas dari bagaimana mereka dipicu. Menambahkan penghancuran implisit di akhir ruang lingkup tidak akan membantu itu.
Telastyn
20
@ Telastyn Huh? Apa yang dikatakan standar C # sekarang tidak ada relevansinya, karena kami sedang mendiskusikan untuk mengubah dokumen itu. Satu-satunya masalah adalah apakah ini praktis untuk dilakukan, dan untuk itu satu-satunya hal yang menarik tentang kurangnya jaminan saat ini adalah alasan untuk kurangnya jaminan ini. Perhatikan bahwa untuk usingpelaksanaan Dispose yang dijamin (baik, mendiskontokan proses tiba-tiba mati tanpa pengecualian yang dilemparkan, di mana titik semua pembersihan mungkin menjadi diperdebatkan).
4
duplikat dari Apakah pengembang Java secara sadar meninggalkan RAII? , meskipun jawaban yang diterima sama sekali salah. Jawaban singkatnya adalah Java menggunakan semantik referensi (heap) daripada semantik value (stack) , jadi finalisasi deterministik tidak terlalu berguna / mungkin. C # memang memiliki nilai-semantik ( struct), tetapi mereka biasanya dihindari kecuali dalam kasus yang sangat khusus. Lihat juga .
BlueRaja - Danny Pflughoeft
2
Ini mirip, bukan duplikat persis.
Maniero
3
blogs.msdn.com/b/oldnewthing/archive/2010/08/10/10048150.aspx adalah halaman yang relevan dengan pertanyaan ini.
Maniero

Jawaban:

17

Perpanjangan bahasa seperti itu akan jauh lebih rumit dan invasif daripada yang Anda kira. Anda tidak bisa menambahkan begitu saja

jika masa hidup variabel tipe stack-bound berakhir, panggil Disposeobjek yang dimaksud

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):

File openSavegame(string id) {
    string path = ... id ...;
    File f = new File(path);
    // do something, perhaps logging
    return f;
} // f goes out of scope, caller receives a closed file

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 structs, 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
Mengapa Anda perlu menyalin / memindahkan konstruktor? File masih berupa tipe referensi. Dalam situasi itu, f yang merupakan pointer disalin ke pemanggil dan bertanggung jawab untuk membuang sumber daya (kompiler secara implisit akan meletakkan pola coba-akhirnya-buang di pemanggil sebagai gantinya)
Maniero
1
@Bigown Jika Anda memperlakukan setiap referensi dengan Filecara ini, tidak ada yang berubah dan Disposetidak pernah dipanggil. Jika Anda selalu menelepon Dispose, 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.
Saya gagal melihat apa yang Anda katakan sekarang (saya tidak mengatakan Anda salah). Objek memiliki sumber daya, bukan referensi.
Maniero
Pemahaman saya, mengubah contoh Anda menjadi hanya pengembalian, adalah bahwa kompiler akan memasukkan percobaan sebelum akuisisi sumber daya (baris 3 pada contoh Anda) dan blok akhirnya-buang tepat sebelum akhir ruang lingkup (baris 6). Tidak masalah di sini, setuju? Kembali ke contoh Anda. Kompiler melihat transfer, ia tidak dapat memasukkan try-akhirnya di sini tetapi pemanggil akan menerima objek (pointer ke) File dan menganggap pemanggil tidak mentransfer objek ini lagi, kompiler akan memasukkan pola try-akhirnya di sana. Dengan kata lain setiap objek IDisposable yang tidak ditransfer perlu menerapkan pola coba-akhirnya.
Maniero
1
@ Bigown Dengan kata lain, jangan panggil Disposejika 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);), harus f.Disposedipanggil di akhir ruang lingkup? Jika ya, Anda memutuskan penelepon yang menyimpan funtuk digunakan nanti. Jika tidak, Anda membocorkan sumber daya jika penelepon tidak menyimpan f. 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.
26

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:

class IWrapAResource
{
    private readonly Resource resource;
    public IWrapAResource()
    {
        // Where Resource is scope bound
        Resource builder = new Resource(args, args, args);

        this.resource = builder;
    } // Uh oh, resource is destroyed
} // Crap, there's no scope for IWrapAResource we can bind to!

Yang lebih buruk adalah bahwa hal ini mungkin tidak jelas bagi pelaksana IWrapAResource:

class IWrapSomething<T>
{
    private readonly T resource; // What happens if T is Resource?
    public IWrapSomething(T input)
    {
        this.resource = input;
    }
}

Sesuatu seperti usingpernyataan 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 yang usingsudah dilakukan.

Billy ONeal
sumber
Dengan asumsi Anda tidak perlu merujuk ke variabel setelah itu keluar dari ruang lingkup (dan benar-benar tidak boleh ada kebutuhan seperti itu), saya mengklaim bahwa Anda masih dapat membuat objek sendiri dengan menulis finalizer untuk itu . Finalizer dipanggil tepat sebelum objeknya adalah sampah yang dikumpulkan. Lihat msdn.microsoft.com/en-us/library/0s71x931.aspx
Robert Harvey
8
@Robert: Program yang ditulis dengan benar tidak dapat mengasumsikan finalizers pernah dijalankan blogs.msdn.com/b/oldnewthing/archive/2010/08/09/10047586.aspx
Billy ONeal
1
Hm Mungkin itu sebabnya mereka membuat usingpernyataan itu.
Robert Harvey
2
Persis. Ini adalah sumber besar bug pemula di C ++, dan juga akan ada di Java / C #. Java / C # tidak menghilangkan kemampuan untuk membocorkan referensi ke sumber daya yang akan dihancurkan, tetapi dengan membuatnya secara eksplisit dan opsional mereka mengingatkan programmer dan memberinya pilihan sadar apa yang harus dilakukan.
Aleksandr Dubinsky
1
@vick Ini bukan IWrapSomethinguntuk membuang T. Siapa pun yang diciptakan Tperlu khawatir tentang itu, apakah menggunakan using, menjadi IDisposabledirinya sendiri, atau memiliki beberapa skema siklus sumber daya ad-hoc.
Aleksandr Dubinsky
13

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 newdan menggunakan pointer).

Jadi, di C ++, Anda dapat melakukan sesuatu seperti ini:

void f()
{
    Foo f1;
    Foo* f2 = new Foo();
    Foo::someStaticField = f2;

    // f1 is destroyed here, the object pointed to by f2 isn't
}

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:

void f()
{
    Foo f1;
    Foo^ f2 = gcnew Foo();
    Foo::someStaticField = f2;

    // f1 is disposed here, the object pointed to by f2 isn't
}

Ini mengkompilasi IL pada dasarnya sama dengan C # berikut:

void f()
{
    using (Foo f1 = new Foo())
    {
        Foo f2 = new Foo();
        Foo.someStaticField = f2;
    }
    // f1 is disposed here, the object pointed to by f2 isn't
}

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.

svick
sumber
1
C # bisa dengan mudah melakukan RAII jika memungkinkan destruktor untuk structtipe seperti D.
Jan Hudec
6

Jika yang membuat Anda usinggelisah adalah kesederhanaan mereka, mungkin kita bisa mengambil langkah kecil menuju yang kurang jelas, daripada mengubah spesifikasi C # itu sendiri. Pertimbangkan kode ini:

public void ReadFile ()
{
  string filename = "myFile.dat";
  local Stream file = File.Open(filename);
  file.Read(blah blah blah);
}

Lihat localkata kunci yang saya tambahkan? Semua hal ini adalah menambahkan sedikit gula lebih sintaksis, seperti using, memerintahkan compiler untuk memanggil Disposedalam finallyblok pada akhir lingkup variabel ini. Itu semuanya. Ini sama dengan:

public void ReadFile ()
{
  string filename = "myFile.dat";
  using (Stream file = File.Open(filename))
  {
      file.Read(blah blah blah);
  }
}

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.

Avner Shahar-Kashtan
sumber
1
@ mike30 tetapi memindahkannya ke definisi tipe akan membawa Anda tepat ke masalah yang didaftarkan orang lain - apa yang terjadi jika Anda meneruskan pointer ke metode yang berbeda atau mengembalikannya dari fungsi? Dengan cara ini pelingkupan dinyatakan dalam ruang lingkup, bukan di tempat lain. Suatu tipe mungkin Disposable, tetapi tidak bisa memanggil Buang.
Avner Shahar-Kashtan
3
@ mike30: Meh. Semua sintaks ini dilakukan adalah menghapus kawat gigi dan, dengan ekstensi, kontrol pelingkupan yang mereka berikan.
Robert Harvey
1
@RobertHarvey Tepat. Ini mengorbankan beberapa fleksibilitas untuk kode yang lebih bersih dan lebih sedikit bersarang. Jika kami menerima saran @ delnan dan menggunakan kembali usingkata kunci, kami dapat menjaga perilaku yang ada dan menggunakannya juga, untuk kasus-kasus di mana kami tidak memerlukan cakupan spesifik. Memiliki brace-less usingdefault untuk lingkup saat ini.
Avner Shahar-Kashtan
1
Saya tidak punya masalah dengan latihan semi-praktis dalam desain bahasa.
Avner Shahar-Kashtan
1
@RobertHarvey. Anda tampaknya memiliki bias terhadap apa pun yang saat ini tidak diterapkan di C #. Kami tidak akan memiliki obat generik, LINQ, blok-penggunaan, tipe ipmlicit, dll jika kami puas dengan C # 1.0. Sintaks ini tidak menyelesaikan masalah implisit, tetapi merupakan gula yang baik untuk mengikat ke lingkup saat ini.
mike30
1

Untuk contoh bagaimana RAII bekerja dalam bahasa yang dikumpulkan sampah, periksa withkata 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:

with open('output.txt', 'w') as f:
    f.write('Hi there!')

Seperti halnya gaya RAII C ++, file akan ditutup saat keluar dari blok itu, tidak peduli apakah itu jalan keluar 'normal', a break, langsung returnatau pengecualian.

Perhatikan bahwa open()panggilan adalah fungsi pembukaan file yang biasa. untuk membuat ini berfungsi, objek file yang dikembalikan mencakup dua metode:

def __enter__(self):
  return self
def __exit__(self):
  self.close()

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.

Javier
sumber
7
withdi Python hampir persis seperti usingdi C #, dan karena itu tidak RAII sejauh pertanyaan ini diperhatikan.
1
"With" Python adalah manajemen sumber daya lingkup-terikat tetapi tidak ada bukti dari smart-pointer. Tindakan mendeklarasikan pointer sebagai smart dapat dianggap "eksplisit", tetapi jika kompiler memaksakan smartness sebagai bagian dari tipe objek, itu akan condong ke arah "implisit".
mike30
AFAICT, tujuan RAII adalah menetapkan pelingkupan yang ketat terhadap sumber daya. jika Anda hanya tertarik untuk melakukan deallocating objek, maka tidak ada, bahasa yang dikumpulkan sampah tidak bisa melakukannya. jika Anda tertarik untuk secara konsisten melepaskan sumber daya, maka ini adalah cara untuk melakukannya (yang lain adalah deferdalam bahasa Go).
Javier
1
Sebenarnya, saya pikir itu adil untuk mengatakan bahwa Java dan C # sangat mendukung konstruksi eksplisit. Kalau tidak, mengapa repot-repot dengan semua upacara yang melekat dalam menggunakan Antarmuka dan warisan?
Robert Harvey
1
@delnan, Go memang memiliki antarmuka 'implisit'.
Javier