Apakah para pengembang Java secara sadar meninggalkan RAII?

82

Sebagai seorang programmer C # lama, saya baru-baru ini datang untuk belajar lebih banyak tentang keuntungan dari Resource Acquisition Is Inisialisasi (RAII). Secara khusus, saya telah menemukan bahwa idiom C #:

using (var dbConn = new DbConnection(connStr)) {
    // do stuff with dbConn
}

memiliki setara C ++:

{
    DbConnection dbConn(connStr);
    // do stuff with dbConn
}

artinya, mengingat untuk menyertakan penggunaan sumber daya seperti DbConnectiondalam sebuah usingblok tidak perlu dalam C ++! Ini tampaknya merupakan keunggulan utama C ++. Ini bahkan lebih meyakinkan ketika Anda mempertimbangkan kelas yang memiliki anggota tipe instance, DbConnectionmisalnya

class Foo {
    DbConnection dbConn;

    // ...
}

Dalam C # saya perlu menerapkan Foo IDisposableseperti:

class Foo : IDisposable {
    DbConnection dbConn;

    public void Dispose()
    {       
        dbConn.Dispose();
    }
}

dan yang lebih buruk, setiap pengguna Fooharus ingat untuk melampirkan Foodi usingblok, seperti:

   using (var foo = new Foo()) {
       // do stuff with "foo"
   }

Sekarang melihat C # dan akar Java-nya, saya bertanya-tanya ... apakah pengembang Jawa sepenuhnya menghargai apa yang mereka berikan ketika mereka meninggalkan tumpukan demi tumpukan, sehingga meninggalkan RAII?

(Demikian pula, apakah Stroustrup sepenuhnya menghargai pentingnya RAII?)

JoelFan
sumber
5
Saya tidak yakin apa yang Anda bicarakan dengan tidak menyertakan sumber daya di C ++. Objek DBConnection mungkin menangani penutupan semua sumber daya di destruktornya.
maple_shaft
16
@maple_shaft, tepatnya maksud saya! Itulah keunggulan C ++ yang saya bahas dalam pertanyaan ini. Dalam C # Anda harus menyertakan sumber daya dalam "menggunakan" ... di C ++ Anda tidak.
JoelFan
12
Pemahaman saya adalah bahwa RAII, sebagai strategi, hanya dipahami setelah kompiler C ++ cukup baik untuk benar-benar menggunakan templating canggih, yang baik setelah Java. C ++ yang sebenarnya tersedia untuk digunakan ketika Java dibuat adalah gaya "C with class" yang sangat primitif, dengan templat dasar yang mungkin , jika Anda beruntung.
Sean McMillan
6
"Pemahaman saya adalah bahwa RAII, sebagai strategi, hanya dipahami setelah kompiler C ++ cukup baik untuk benar-benar menggunakan templating canggih, yang baik setelah Java." - Itu tidak sepenuhnya benar. Konstruktor dan destruktor telah menjadi fitur inti dari C ++ sejak hari pertama, jauh sebelum penggunaan template yang luas dan jauh sebelum Jawa.
Jim In Texas
8
@JimInTexas: Saya pikir Sean memiliki benih kebenaran di sana (Meskipun bukan templat tetapi pengecualian adalah intinya). Konstruktor / Destructors ada di sana dari awal, tetapi ada pentingnya dan konsep RAII awalnya tidak (apa kata yang saya cari) direalisasikan. Butuh beberapa tahun dan beberapa waktu untuk penyusun untuk menjadi baik sebelum kami menyadari betapa pentingnya seluruh RAII.
Martin York

Jawaban:

38

Sekarang melihat C # dan akar Java-nya, saya bertanya-tanya ... apakah pengembang Jawa sepenuhnya menghargai apa yang mereka berikan ketika mereka meninggalkan tumpukan demi tumpukan, sehingga meninggalkan RAII?

(Demikian pula, apakah Stroustrup sepenuhnya menghargai pentingnya RAII?)

Saya cukup yakin Gosling tidak mendapatkan arti penting RAII pada saat ia mendesain Java. Dalam wawancaranya, dia sering berbicara tentang alasan untuk tidak menggunakan obat generik dan overloading operator, tetapi tidak pernah menyebutkan destruktor deterministik dan RAII.

Cukup lucu, bahkan Stroustrup tidak menyadari pentingnya destruktor deterministik pada saat ia merancang mereka. Saya tidak dapat menemukan kutipan, tetapi jika Anda benar-benar tertarik, Anda dapat menemukannya di antara wawancaranya di sini: http://www.stroustrup.com/interviews.html

Nemanja Trifunovic
sumber
11
@maple_shaft: Singkatnya, itu tidak mungkin. Kecuali jika Anda menemukan cara untuk memiliki pengumpulan sampah deterministik (yang tampaknya tidak mungkin secara umum, dan membatalkan semua optimasi GC dekade terakhir), Anda harus memperkenalkan objek yang dialokasikan tumpukan, tetapi itu membuka beberapa kaleng worms: objek-objek ini membutuhkan semantik, "mengiris masalah" dengan subtyping (dan karenanya TANPA polimorfisme), menggantung pointer kecuali mungkin jika Anda menempatkan batasan yang signifikan di atasnya atau membuat perubahan sistem tipe masif yang tidak kompatibel. Dan itu baru saja keluar dari kepala saya.
13
@DeadMG: Jadi Anda sarankan kami kembali ke manajemen memori manual. Itu pendekatan yang valid untuk pemrograman secara umum, dan tentu saja memungkinkan kerusakan deterministik. Tapi itu tidak menjawab pertanyaan ini, yang menyangkut dirinya dengan pengaturan hanya GC yang ingin memberikan keamanan memori dan perilaku yang terdefinisi dengan baik bahkan jika kita semua bertindak seperti orang bodoh. Itu membutuhkan GC untuk semuanya dan tidak ada cara untuk memulai penghancuran objek secara manual (dan semua kode Java yang ada bergantung setidaknya pada yang sebelumnya), sehingga Anda membuat GC deterministik atau Anda kurang beruntung.
26
@delan. Saya tidak akan memanggil manualmanajemen memori pointer pintar C ++ . Mereka lebih seperti pengumpul sampah terkendali butir halus terkendali. Jika digunakan dengan benar, smart pointer adalah lutut lebah.
Martin York
10
@LokiAstari: Ya, saya katakan mereka sedikit kurang otomatis daripada GC penuh (Anda harus berpikir tentang jenis kecerdasan yang Anda inginkan) dan mengimplementasikannya sebagai perpustakaan membutuhkan pointer mentah (dan karenanya manajemen memori manual) untuk membangun . Juga, saya tidak mengetahui adanya penunjuk pintar selain menangani referensi siklik secara otomatis, yang merupakan persyaratan ketat untuk pengumpulan sampah di buku saya. Pointer pintar tentu sangat keren dan berguna, tetapi Anda harus menghadapi mereka tidak dapat memberikan jaminan (apakah Anda menganggapnya berguna atau tidak) dari bahasa GC'd sepenuhnya dan eksklusif.
11
@elan: Saya harus tidak setuju di sana. Saya pikir mereka lebih otomatis daripada GC karena mereka deterministik. BAIK. Agar efisien, Anda perlu memastikan Anda menggunakan yang benar (saya akan berikan kepada Anda). std :: lemah_ptr menangani siklus dengan sangat baik. Siklus selalu ditelusuri tetapi dalam kenyataannya itu hampir tidak pernah menjadi masalah karena objek dasar biasanya berdasarkan tumpukan dan ketika ini berjalan akan merapikan sisanya. Untuk kasus yang jarang terjadi itu bisa menjadi masalah std :: lemah_ptr.
Martin York
60

Ya, para perancang C # (dan, saya yakin, Java) secara khusus memutuskan menentang finalisasi deterministik. Saya bertanya kepada Anders Hejlsberg tentang hal ini beberapa kali sekitar tahun 1999-2002.

Pertama, gagasan semantik yang berbeda untuk suatu objek berdasarkan apakah berbasis stack atau heap tentu saja bertentangan dengan tujuan desain pemersatu dari kedua bahasa, yang untuk meringankan programmer dari masalah yang persis seperti itu.

Kedua, bahkan jika Anda mengakui bahwa ada keuntungan, ada kompleksitas implementasi yang signifikan dan inefisiensi yang terlibat dalam pembukuan. Anda tidak dapat benar - benar meletakkan objek seperti tumpukan di tumpukan dalam bahasa yang dikelola. Anda dibiarkan mengatakan "semantik mirip tumpukan," dan berkomitmen untuk pekerjaan yang signifikan (tipe nilai sudah cukup keras, pikirkan tentang objek yang merupakan turunan dari kelas kompleks, dengan referensi yang masuk dan kembali ke memori yang dikelola).

Karena itu, Anda tidak ingin finalisasi deterministik pada setiap objek dalam sistem pemrograman di mana "(hampir) semuanya adalah objek." Jadi Anda jangan harus memperkenalkan beberapa jenis sintaks programmer yang dikendalikan untuk memisahkan objek biasanya-dilacak dari satu yang memiliki finalisasi deterministik.

Di C #, Anda memiliki usingkata kunci, yang masuk agak terlambat dalam desain apa yang menjadi C # 1.0. Seluruh IDisposablehal yang cukup celaka, dan satu keajaiban apakah itu akan lebih elegan untuk memiliki usingpekerjaan dengan sintaks destructor C ++ ~menandai kelas-kelas dimana boiler-piring IDisposablepola bisa diterapkan secara otomatis?

Larry OBrien
sumber
2
Bagaimana dengan apa yang telah dilakukan C ++ / CLI (.NET), di mana objek pada heap yang dikelola juga memiliki "pegangan" berbasis stack, yang menyediakan RIAA?
JoelFan
3
C ++ / CLI memiliki serangkaian keputusan dan kendala desain yang sangat berbeda. Beberapa dari keputusan itu berarti bahwa Anda dapat menuntut lebih banyak pemikiran tentang alokasi memori dan implikasi kinerja dari programmer: keseluruhan "memberi mereka cukup tali untuk menggantung diri" trade-off. Dan saya membayangkan bahwa kompiler C ++ / CLI jauh lebih kompleks daripada C # (terutama pada generasi awal).
Larry OBrien
5
+1 ini adalah satu-satunya jawaban yang benar sejauh ini - itu karena Java sengaja tidak memiliki objek berbasis stack (non-primitif).
BlueRaja - Danny Pflughoeft
8
@ Peter Taylor - benar. Tetapi saya merasa bahwa destruktor non-deterministik C # bernilai sangat sedikit, karena Anda tidak dapat mengandalkannya untuk mengelola segala jenis sumber daya terbatas. Jadi, menurut pendapat saya, mungkin lebih baik menggunakan ~sintaksis untuk menjadi gula sintaksis untukIDisposable.Dispose()
Larry OBrien
3
@Larry: Saya setuju. C ++ / CLI memang digunakan ~sebagai gula sintaksis IDisposable.Dispose(), dan jauh lebih nyaman daripada sintaksis C #.
dan04
41

Perlu diingat bahwa Java dikembangkan pada 1991-1995 ketika C ++ adalah bahasa yang jauh berbeda. Pengecualian (yang membuat RAII penting ) dan templat (yang membuatnya lebih mudah untuk mengimplementasikan smart pointer) adalah fitur "baru-ketinggalan jaman". Kebanyakan programmer C ++ berasal dari C dan digunakan untuk melakukan manajemen memori manual.

Jadi saya ragu bahwa pengembang Java sengaja memutuskan untuk meninggalkan RAII. Namun, itu adalah keputusan yang disengaja untuk Jawa untuk lebih memilih semantik referensi daripada semantik nilai. Kerusakan deterministik sulit diimplementasikan dalam bahasa referensi-semantik.

Jadi mengapa menggunakan semantik referensi alih-alih nilai semantik?

Karena itu membuat bahasanya jauh lebih sederhana.

  • Tidak perlu ada perbedaan sintaksis antara Foodan Foo*atau antara foo.bardan foo->bar.
  • Tidak perlu penugasan berlebih, ketika semua tugas dilakukan adalah menyalin pointer.
  • Tidak perlu menyalin konstruktor. ( Kadang - kadang ada kebutuhan untuk fungsi penyalinan eksplisit seperti clone(). Banyak objek tidak perlu disalin. Misalnya, tidak bisa diubah.)
  • Tidak perlu mendeklarasikan privatecopy constructor dan operator=membuat kelas tidak dapat di-copy. Jika Anda tidak ingin objek kelas disalin, Anda tidak perlu menulis fungsi untuk menyalinnya.
  • Tidak perlu swapfungsi. (Kecuali jika Anda menulis semacam rutinitas.)
  • Tidak perlu untuk referensi nilai rujukan C ++ 0x-style.
  • Tidak perlu untuk (N) RVO.
  • Tidak ada masalah mengiris.
  • Lebih mudah bagi kompiler untuk menentukan tata letak objek, karena referensi memiliki ukuran tetap.

Kelemahan utama dari referensi semantik adalah bahwa ketika setiap objek berpotensi memiliki banyak referensi, menjadi sulit untuk mengetahui kapan harus menghapusnya. Anda harus memiliki manajemen memori otomatis.

Java memilih untuk menggunakan pengumpul sampah yang tidak menentukan.

Tidak bisakah GC menjadi deterministik?

Ya bisa. Misalnya, implementasi C dari Python menggunakan penghitungan referensi. Dan kemudian menambahkan tracing GC untuk menangani sampah siklik di mana penghitungan ulang gagal.

Tetapi penghitungan ulang sangat tidak efisien. Banyak siklus CPU dihabiskan untuk memperbarui hitungan. Lebih buruk lagi di lingkungan multi-threaded (seperti jenis Java yang dirancang untuk) di mana pembaruan tersebut perlu disinkronkan. Jauh lebih baik untuk menggunakan pengumpul sampah nol hingga Anda perlu beralih ke yang lain.

Anda dapat mengatakan bahwa Java memilih untuk mengoptimalkan case umum (memori) dengan mengorbankan sumber daya yang tidak dapat dipertukarkan seperti file dan soket. Hari ini, mengingat adopsi RAII di C ++, ini mungkin pilihan yang salah. Tetapi ingat bahwa sebagian besar target audiens untuk Java adalah programmer C (atau "C dengan kelas") yang terbiasa menutup hal-hal ini secara eksplisit.

Tapi bagaimana dengan C ++ / CLI "stack objek"?

Itu hanya gula sintaksis untukDispose ( tautan asli ), seperti halnya C # using. Namun, itu tidak menyelesaikan masalah umum dari kerusakan deterministik, karena Anda dapat membuat anonim gcnew FileStream("filename.ext")dan C ++ / CLI tidak akan secara otomatis membuangnya.

dan04
sumber
3
Juga, tautan yang bagus (terutama yang pertama, yang sangat relevan dengan diskusi ini) .
BlueRaja - Danny Pflughoeft
The usingpernyataan menangani banyak masalah yang berhubungan dengan pembersihan baik, tetapi banyak orang lain tetap. Saya akan menyarankan bahwa pendekatan yang tepat untuk bahasa dan kerangka kerja adalah untuk secara deklaratif membedakan antara lokasi penyimpanan yang "memiliki" referensi IDisposabledari yang tidak; menimpa atau meninggalkan lokasi penyimpanan yang memiliki referensi IDisposableharus membuang target dengan tidak adanya arahan yang bertentangan.
supercat
1
"Tidak perlu copy constructor" kedengarannya bagus, tetapi dalam praktiknya gagal. java.util.Date dan Kalender mungkin adalah contoh yang paling terkenal. Tidak ada yang lebih indah dari new Date(oldDate.getTime()).
kevin cline
2
iow RAII bukan "ditinggalkan", itu sama sekali tidak ada untuk ditinggalkan :) Mengenai menyalin konstruktor, saya tidak pernah menyukai mereka, terlalu mudah untuk salah, mereka adalah sumber sakit kepala yang konstan ketika di suatu tempat jauh di dalam seseorang (lain) lupa membuat salinan yang dalam, menyebabkan sumber daya dibagi di antara salinan yang seharusnya tidak.
jwenting
20

Java7 memperkenalkan sesuatu yang mirip dengan C # using: Pernyataan coba-dengan-sumber daya

sebuah trypernyataan yang menyatakan satu atau lebih sumber daya. Sebuah sumber daya adalah sebagai objek yang harus ditutup setelah program ini selesai dengan itu. Pernyataan try-dengan-sumber daya memastikan bahwa setiap sumber daya ditutup pada akhir pernyataan. Setiap objek yang mengimplementasikan java.lang.AutoCloseable, yang mencakup semua objek yang mengimplementasikan java.io.Closeable, dapat digunakan sebagai sumber daya ...

Jadi saya kira mereka tidak secara sadar memilih untuk tidak mengimplementasikan RAII atau mereka berubah pikiran sementara itu.

Patrick
sumber
Menarik, tetapi sepertinya ini hanya berfungsi dengan objek yang diimplementasikan java.lang.AutoCloseable. Mungkin bukan masalah besar tapi saya tidak suka bagaimana ini terasa agak terkendala. Mungkin saya memiliki beberapa objek lain yang harus dilepaskan secara otomatis, tetapi sangat aneh untuk membuatnya mengimplementasikan AutoCloseable...
FrustratedWithFormsDesigner
9
@ Patrick: Er, jadi? usingtidak sama dengan RAII - dalam satu kasus penelepon khawatir tentang membuang sumber daya, dalam kasus lain callee menanganinya.
BlueRaja - Danny Pflughoeft
1
+1 Saya tidak tahu tentang coba-dengan-sumber daya; itu harus berguna dalam membuang lebih banyak boilerplate.
jprete
3
-1 untuk using/ coba-dengan-sumber daya tidak sama dengan RAII.
Sean McMillan
4
@Sean: Setuju. usingdan sejenisnya tidak ada di dekat RAII.
DeadMG
18

Java sengaja tidak memiliki objek berbasis tumpukan (alias nilai-objek). Ini diperlukan agar objek secara otomatis dihancurkan pada akhir metode seperti itu.

Karena ini dan fakta bahwa Jawa adalah pengumpulan sampah, finalisasi deterministik lebih-atau-kurang mustahil (mis. Bagaimana jika objek "lokal" saya menjadi rujukan di tempat lain? Kemudian ketika metode ini berakhir, kami tidak ingin merusaknya. ) .

Namun, ini tidak masalah bagi kebanyakan dari kita, karena hampir tidak pernah ada kebutuhan untuk finalisasi deterministik, kecuali ketika berinteraksi dengan sumber daya asli (C ++)!


Mengapa Java tidak memiliki objek berbasis stack?

(Selain primitif ..)

Karena objek berbasis tumpukan memiliki semantik berbeda dari referensi berbasis tumpukan. Bayangkan kode berikut dalam C ++; apa fungsinya?

return myObject;
  • Jika myObjectobjek berbasis stack lokal, copy-constructor dipanggil (jika hasilnya ditugaskan untuk sesuatu).
  • Jika myObjectobjek berbasis stack lokal dan kami mengembalikan referensi, hasilnya tidak terdefinisi.
  • Jika myObjectadalah anggota / objek global, copy-constructor dipanggil (jika hasilnya ditugaskan untuk sesuatu).
  • Jika myObjectadalah anggota / objek global dan kami mengembalikan referensi, referensi dikembalikan.
  • Jika myObjectadalah pointer ke objek berbasis stack lokal, hasilnya tidak ditentukan.
  • Jika myObjectadalah penunjuk ke objek anggota / global, penunjuk itu dikembalikan.
  • Jika myObjectpointer ke objek berbasis heap, pointer itu dikembalikan.

Sekarang apa yang dilakukan kode yang sama di Java?

return myObject;
  • Referensi ke myObjectdikembalikan. Tidak masalah jika variabelnya lokal, anggota, atau global; dan tidak ada objek berbasis stack atau case pointer yang perlu dikhawatirkan.

Di atas menunjukkan mengapa objek berbasis stack adalah penyebab paling umum kesalahan pemrograman di C ++. Karena itu, desainer Jawa mengambilnya; dan tanpa mereka, tidak ada gunanya menggunakan RAII di Jawa.

BlueRaja - Danny Pflughoeft
sumber
6
Saya tidak tahu apa yang Anda maksud dengan "tidak ada gunanya di RAII" ... Saya pikir maksud Anda "tidak ada kemampuan untuk menyediakan RAII di Jawa" ... RAII tidak tergantung pada bahasa apa pun ... tidak ada gunanya menjadi "sia-sia" karena 1 bahasa tertentu tidak menyediakannya
JoelFan
4
Itu bukan alasan yang valid. Sebuah objek tidak harus benar-benar hidup di stack untuk menggunakan RAII berbasis stack. Jika ada yang namanya "referensi unik", destructor dapat ditembakkan begitu keluar dari ruang lingkup. Lihat misalnya, bagaimana cara kerjanya dengan bahasa pemrograman D: d-programming-language.org/exception-safe.html
Nemanja Trifunovic
3
@Nemanja: Sebuah objek tidak harus hidup di stack untuk memiliki semantik berbasis stack, dan saya tidak pernah mengatakan itu. Tapi bukan itu masalahnya; masalahnya, seperti yang saya sebutkan, adalah semantik berbasis stack itu sendiri.
BlueRaja - Danny Pflughoeft
4
@Aaronaught: Iblis ada di "hampir selalu" dan "sebagian besar waktu". Jika Anda tidak menutup koneksi db Anda dan membiarkannya ke GC untuk memicu finalizer, itu akan berfungsi dengan baik dengan unit-tes Anda dan rusak parah ketika digunakan dalam produksi. Pembersihan deterministik adalah penting terlepas dari bahasanya.
Nemanja Trifunovic
8
@NemanjaTrifunovic: Mengapa Anda menguji unit pada koneksi database langsung? Itu bukan tes unit. Tidak, maaf, saya tidak membelinya. Anda tidak seharusnya membuat koneksi DB di semua tempat, Anda harus meneruskannya melalui konstruktor atau properti, dan itu berarti Anda tidak ingin semantik penghancuran stack-like. Sangat sedikit objek yang bergantung pada koneksi database yang seharusnya memilikinya. Jika pembersihan non-deterministik menggigit Anda sesering itu, itu sulit, maka itu karena desain aplikasi yang buruk, bukan desain bahasa yang buruk.
Aaronaught
17

Uraian Anda tentang lubang usingtidak lengkap. Pertimbangkan masalah berikut:

interface Bar {
    ...
}
class Foo : Bar, IDisposable {
    ...
}

Bar b = new Foo();

// Where's the Dispose?

Menurut pendapat saya, tidak memiliki RAII dan GC adalah ide yang buruk. Ketika datang untuk menutup file di Jawa, itu malloc()dan free()di sana.

DeadMG
sumber
2
Saya setuju bahwa RAII adalah lutut lebah. Tetapi usingklausa ini merupakan langkah maju yang bagus untuk C # di Jawa. Ini memungkinkan perusakan deterministik dan dengan demikian memperbaiki manajemen sumber daya (ini tidak sebagus RAII seperti yang Anda perlu ingat untuk melakukannya, tetapi itu jelas merupakan ide yang baik).
Martin York
8
"Ketika datang untuk menutup file di Jawa, itu malloc () dan gratis () di sana." - Tentu saja.
Konrad Rudolph
9
@KonradRudolph: Ini lebih buruk daripada malloc dan gratis. Setidaknya dalam C Anda tidak memiliki pengecualian.
Nemanja Trifunovic
1
@Nemanja: Mari bersikap adil, Anda bisa free()di finally.
DeadMG
4
@ Loki: Masalah kelas dasar jauh lebih penting sebagai masalah. Misalnya, aslinya IEnumerabletidak mewarisi IDisposable, dan ada banyak iterator khusus yang tidak pernah dapat diimplementasikan sebagai hasilnya.
DeadMG
14

Saya cukup tua. Aku pernah ke sana dan melihatnya dan membenturkan kepalaku berkali-kali.

Saya berada di sebuah konferensi di Hursley Park di mana anak-anak IBM memberi tahu kami betapa indahnya bahasa Jawa yang baru ini, hanya ada yang bertanya ... mengapa tidak ada penghancur benda-benda ini. Dia tidak bermaksud hal yang kita tahu sebagai destruktor di C ++, tetapi tidak ada finalis juga (atau ada finalis tetapi mereka pada dasarnya tidak bekerja). Ini jauh di belakang, dan kami memutuskan bahwa Jawa sedikit bahasa mainan pada saat itu.

sekarang mereka menambahkan Finalisers ke spec bahasa dan Java melihat beberapa adopsi.

Tentu saja, nanti semua orang diberitahu untuk tidak menempatkan finalis pada objek mereka karena itu memperlambat GC. (karena harus tidak hanya mengunci tumpukan tetapi memindahkan objek yang akan diselesaikan ke area temp, karena metode ini tidak dapat dipanggil karena GC telah menghentikan aplikasi agar tidak berjalan. Sebaliknya mereka akan dipanggil segera sebelum yang berikutnya Siklus GC) (dan lebih buruk lagi, terkadang finaliser tidak akan pernah dipanggil sama sekali ketika aplikasi dimatikan. Bayangkan tidak ada file handle Anda yang ditutup, selamanya)

Kemudian kami memiliki C #, dan saya ingat forum diskusi di MSDN di mana kami diberitahu betapa indahnya bahasa C # baru ini. Seseorang bertanya mengapa tidak ada finalisasi deterministik dan anak-anak MS mengatakan kepada kami bagaimana kami tidak membutuhkan hal-hal seperti itu, kemudian mengatakan kepada kami bahwa kami perlu mengubah cara kami merancang aplikasi, kemudian memberi tahu kami betapa menakjubkannya GC dan bagaimana semua aplikasi lama kami sampah dan tidak pernah berhasil karena semua referensi melingkar. Kemudian mereka menyerah pada tekanan dan memberi tahu kami bahwa mereka telah menambahkan pola IDispose ini ke spek yang bisa kita gunakan. Saya pikir itu cukup banyak kembali ke manajemen memori manual untuk kami di aplikasi C # pada saat itu.

Tentu saja, anak-anak MS kemudian menemukan bahwa semua yang mereka katakan kepada kami adalah ... well, mereka membuat IDispose sedikit lebih dari sekadar antarmuka standar, dan kemudian menambahkan pernyataan menggunakan. W00t! Mereka menyadari bahwa finalisasi deterministik adalah sesuatu yang hilang dari bahasa. Tentu saja, Anda masih harus ingat untuk meletakkannya di mana-mana, jadi ini masih sedikit manual, tetapi lebih baik.

Jadi mengapa mereka melakukannya ketika mereka bisa menggunakan semantik gaya yang secara otomatis ditempatkan pada setiap blok lingkup sejak awal? Mungkin efisiensi, tetapi saya suka berpikir bahwa mereka tidak menyadarinya. Sama seperti akhirnya mereka menyadari bahwa Anda masih memerlukan pointer pintar di .NET (google SafeHandle) mereka berpikir bahwa GC benar-benar akan menyelesaikan semua masalah. Mereka lupa bahwa suatu objek lebih dari sekadar memori dan bahwa GC terutama dirancang untuk menangani manajemen memori. mereka terjebak dalam gagasan bahwa GC akan menangani ini, dan lupa bahwa Anda meletakkan barang-barang lain di sana, sebuah objek bukan hanya gumpalan memori yang tidak masalah jika Anda tidak menghapusnya untuk sementara waktu.

Tetapi saya juga berpikir bahwa kurangnya metode penyelesaian dalam Java asli memiliki sedikit lebih banyak - bahwa objek yang Anda buat adalah semua tentang memori, dan jika Anda ingin menghapus sesuatu yang lain (seperti pegangan DB atau soket atau apa pun ) maka Anda diharapkan melakukannya secara manual .

Ingat Java dirancang untuk lingkungan tertanam di mana orang terbiasa menulis kode C dengan banyak alokasi manual, jadi tidak memiliki bebas otomatis tidak banyak masalah - mereka tidak pernah melakukannya sebelumnya, jadi mengapa Anda membutuhkannya di Jawa? Masalahnya tidak ada hubungannya dengan utas, atau tumpukan / tumpukan, itu mungkin hanya ada untuk membuat alokasi memori (dan karenanya de-alokasi) sedikit lebih mudah. Secara keseluruhan, pernyataan try / akhirnya mungkin merupakan tempat yang lebih baik untuk menangani sumber daya non-memori.

Jadi IMHO, cara. NET hanya menyalin kelemahan terbesar Jawa adalah kelemahan terbesarnya. .NET seharusnya merupakan C ++ yang lebih baik, bukan Java yang lebih baik.

gbjbaanb
sumber
IMHO, hal-hal seperti 'menggunakan' blok adalah pendekatan yang tepat untuk pembersihan deterministik, tetapi beberapa hal lagi diperlukan juga: (1) sarana untuk memastikan bahwa benda-benda dibuang jika destruktor mereka mengeluarkan pengecualian; (2) sarana penghasil otomatis metode rutin untuk memanggil Disposesemua bidang yang ditandai dengan usingarahan, dan menentukan apakah IDisposable.Disposeharus secara otomatis menyebutnya; (3) arahan mirip dengan using, tetapi yang hanya akan memanggil Disposedalam kasus pengecualian; (4) variasi IDisposableyang akan mengambil Exceptionparameter, dan ...
supercat
... yang akan digunakan secara otomatis usingjika sesuai; parameternya adalah nulljika usingblok keluar secara normal, atau yang lain akan menunjukkan pengecualian apa yang tertunda jika keluar melalui pengecualian. Jika hal-hal seperti itu ada, akan jauh lebih mudah untuk mengelola sumber daya secara efektif dan menghindari kebocoran.
supercat
11

Bruce Eckel, penulis "Thinking in Java" dan "Thinking in C ++" dan anggota Komite Standar C ++, berpendapat bahwa, di banyak daerah (tidak hanya RAII), Gosling dan tim Java tidak melakukan pekerjaan rumah.

... Untuk memahami bagaimana bahasa dapat menjadi tidak menyenangkan dan rumit, dan dirancang dengan baik pada saat yang sama, Anda harus mengingat keputusan desain utama yang menjadi dasar segala sesuatu dalam C ++ digantung: kompatibilitas dengan C. Stroustrup memutuskan - dan benar sehingga , akan muncul - bahwa cara untuk mendapatkan massa programmer C untuk pindah ke objek adalah dengan membuat langkah transparan: untuk memungkinkan mereka mengkompilasi kode C mereka tidak berubah di bawah C ++. Ini adalah kendala besar, dan selalu menjadi kekuatan terbesar C ++ ... dan kutukannya. Itulah yang membuat C ++ sesukses dulu, dan serumit itu.

Itu juga menipu para desainer Java yang tidak mengerti C ++ dengan cukup baik. Sebagai contoh, mereka mengira operator overloading terlalu sulit untuk digunakan oleh programmer. Yang pada dasarnya benar dalam C ++, karena C ++ memiliki alokasi tumpukan dan alokasi tumpukan dan Anda harus membebani operator Anda untuk menangani semua situasi dan tidak menyebabkan kebocoran memori. Memang sulit. Java, bagaimanapun, memiliki mekanisme alokasi penyimpanan tunggal dan pengumpul sampah, yang membuat operator kelebihan beban sepele - seperti yang ditunjukkan dalam C # (tetapi sudah ditunjukkan dalam Python, yang mendahului Jawa). Tetapi selama bertahun-tahun, sebagian garis dari tim Jawa adalah "Overloading operator terlalu rumit." Ini dan banyak keputusan lain di mana seseorang jelas tidak

Ada banyak contoh lainnya. Primitif "harus dimasukkan untuk efisiensi." Jawaban yang tepat adalah tetap setia pada "semuanya adalah objek" dan memberikan pintu jebakan untuk melakukan aktivitas tingkat rendah ketika efisiensi diperlukan (ini juga akan memungkinkan teknologi hotspot untuk transparan membuat segalanya lebih efisien, karena mereka pada akhirnya akan memiliki). Oh, dan fakta bahwa Anda tidak dapat menggunakan prosesor floating point secara langsung untuk menghitung fungsi transendental (itu dilakukan dalam perangkat lunak sebagai gantinya). Saya telah menulis tentang masalah seperti ini sebanyak yang saya bisa, dan jawaban yang saya dengar selalu merupakan jawaban tautologis yang menyatakan bahwa "ini adalah cara Jawa."

Ketika saya menulis tentang bagaimana obat generik dirancang, saya mendapat respons yang sama, bersama dengan "kita harus kompatibel dengan keputusan sebelumnya (buruk) yang dibuat di Jawa." Akhir-akhir ini semakin banyak orang telah memperoleh pengalaman yang cukup dengan Generics untuk melihat bahwa mereka benar-benar sangat sulit digunakan - memang, template C ++ jauh lebih kuat dan konsisten (dan jauh lebih mudah digunakan sekarang karena pesan kesalahan kompiler dapat ditoleransi). Orang-orang bahkan telah menganggap serius reifikasi - sesuatu yang akan membantu tetapi tidak akan membuat banyak penyok dalam desain yang dilumpuhkan oleh kendala yang dipaksakan sendiri.

Daftar berlanjut ke titik di mana itu hanya membosankan ...

Gnawme
sumber
5
Ini terdengar seperti jawaban Java versus C ++, daripada berfokus pada RAII. Saya pikir C ++ dan Java adalah bahasa yang berbeda, masing-masing dengan kekuatan dan kelemahannya. Juga para perancang C ++ tidak mengerjakan pekerjaan rumahnya di banyak bidang (prinsip KISS tidak diterapkan, mekanisme impor sederhana untuk kelas yang hilang, dll). Tetapi fokus pertanyaannya adalah RAII: ini tidak ada di Jawa dan Anda harus memprogramnya secara manual.
Giorgio
4
@Iorgio: Inti dari artikel ini adalah, Jawa tampaknya telah ketinggalan perahu pada sejumlah masalah, beberapa di antaranya berhubungan langsung dengan RAII. Mengenai C ++ dan dampaknya pada Java, Eckels mencatat: "Anda harus mengingat keputusan desain utama yang menjadi dasar segala hal dalam C ++ digantung: kompatibilitas dengan C. Ini adalah kendala besar, dan selalu menjadi kekuatan terbesar C ++ ... dan itu kutukan. Itu juga menipu para desainer Java yang tidak mengerti C ++ dengan cukup baik. " Desain C ++ memengaruhi Java secara langsung, sementara C # memiliki kesempatan untuk belajar dari keduanya. (Apakah itu dilakukan adalah pertanyaan lain.)
Gnawme
2
@Giorgio Mempelajari bahasa yang ada dalam paradigma dan keluarga bahasa tertentu memang merupakan bagian dari pekerjaan rumah yang diperlukan untuk pengembangan bahasa baru. Ini adalah salah satu contoh di mana mereka hanya mengendusnya dengan Java. Mereka memiliki C ++ dan Smalltalk untuk dilihat. C ++ tidak memiliki Java untuk dilihat ketika dikembangkan.
Jeremy
1
@ Gnawme: "Java sepertinya ketinggalan perahu karena beberapa masalah, beberapa di antaranya berhubungan langsung dengan RAII": dapatkah Anda menyebutkan masalah ini? Artikel yang Anda posting tidak menyebutkan RAII.
Giorgio
2
@Giorgio Tentu, sudah ada inovasi sejak pengembangan C ++ yang mencakup banyak fitur yang Anda temukan kurang di sana. Apakah ada fitur-fitur yang seharusnya mereka temukan dengan melihat bahasa yang dibuat sebelum pengembangan C ++? Itu jenis pekerjaan rumah yang kita bicarakan dengan Java - tidak ada alasan bagi mereka untuk tidak mempertimbangkan setiap fitur C ++ di develpoment Java. Beberapa seperti warisan berganda yang sengaja mereka tinggalkan - yang lain seperti RAII yang tampaknya mereka abaikan.
Jeremy
10

Alasan terbaik jauh lebih sederhana daripada sebagian besar jawaban di sini.

Anda tidak dapat meneruskan tumpukan objek yang dialokasikan ke utas lainnya.

Berhentilah dan pikirkan itu. Teruslah berpikir .... Sekarang C ++ tidak memiliki utas ketika semua orang begitu tertarik pada RAII. Bahkan Erlang (tumpukan terpisah per utas) menjadi menjengkelkan ketika Anda melewati terlalu banyak objek di sekitar. C ++ hanya mendapatkan model memori di C ++ 2011; sekarang Anda hampir dapat beralasan tentang concurrency di C ++ tanpa harus merujuk ke "dokumentasi" kompiler Anda.

Java dirancang dari (hampir) hari pertama untuk beberapa utas.

Saya masih memiliki salinan lama "Bahasa Pemrograman C ++" di mana Stroustrup meyakinkan saya bahwa saya tidak akan memerlukan utas.

Alasan menyakitkan kedua adalah untuk menghindari mengiris.

Tim Williscroft
sumber
1
Java sedang dirancang untuk banyak utas juga menjelaskan mengapa GC tidak didasarkan pada penghitungan referensi.
dan04
4
@NemanjaTrifunovic: Anda tidak dapat membandingkan C ++ / CLI dengan Java atau C #, itu dirancang hampir untuk tujuan yang jelas untuk beroperasi dengan kode C / C ++ yang tidak dikelola; itu lebih seperti bahasa yang tidak dikelola yang terjadi untuk memberikan akses ke .NET framework daripada sebaliknya.
Aaronaught
3
@NemanjaTrifunovic: Ya, C ++ / CLI adalah salah satu contoh bagaimana hal itu dapat dilakukan dengan cara yang sama sekali tidak pantas untuk aplikasi normal . Ini hanya berguna untuk C / C ++ interop. Tidak hanya jika pengembang normal tidak perlu dibebani dengan keputusan "tumpukan atau tumpukan" yang sama sekali tidak relevan, tetapi jika Anda pernah mencoba untuk refactor maka itu mudah untuk secara tidak sengaja membuat null pointer / kesalahan referensi dan / atau kebocoran memori. Maaf, tapi saya harus bertanya-tanya apakah Anda pernah benar-benar diprogram dalam Java atau C #, karena saya tidak berpikir siapa pun yang benar-benar menginginkan semantik yang digunakan dalam C ++ / CLI.
Aaronaught
2
@Aaronaught: Saya sudah memprogram dengan Java (sedikit) dan C # (banyak) dan proyek saya saat ini hampir semuanya C #. Percayalah, saya tahu apa yang saya bicarakan, dan itu tidak ada hubungannya dengan "tumpukan vs tumpukan" - itu semua ada hubungannya dengan memastikan bahwa semua sumber daya Anda dilepaskan segera setelah Anda tidak membutuhkannya. Secara otomatis. Jika tidak - Anda akan mendapat masalah.
Nemanja Trifunovic
4
@NemanjaTrifunovic: Itu hebat, sangat hebat, tetapi baik C # dan C ++ / CLI mengharuskan Anda untuk secara eksplisit menyatakan kapan Anda ingin ini terjadi, mereka hanya menggunakan sintaks yang berbeda. Tidak ada yang memperdebatkan poin penting yang saat ini Anda bicarakan (bahwa "sumber daya dilepaskan segera setelah Anda tidak membutuhkannya") tetapi Anda membuat lompatan logis besar ke "semua bahasa yang dikelola harus memiliki otomatis, tetapi hanya -sort-dari pembuangan deterministik berbasis panggilan-tumpukan ". Itu tidak menahan air.
Aaronaught
5

Di C ++, Anda menggunakan lebih banyak tujuan umum, fitur-fitur tingkat bahasa yang lebih rendah (destruktor secara otomatis dipanggil pada objek berbasis stack) untuk mengimplementasikan yang lebih tinggi (RAII), dan pendekatan ini adalah sesuatu yang tampaknya tidak dimiliki oleh orang C # / Java. terlalu suka. Mereka lebih suka merancang alat tingkat tinggi spesifik untuk kebutuhan spesifik, dan menyediakannya kepada pemrogram yang siap pakai, dibangun ke dalam bahasa. Masalah dengan alat khusus seperti itu adalah bahwa mereka sering tidak mungkin untuk dikustomisasi (sebagian itulah yang membuatnya mudah dipelajari). Ketika membangun dari blok yang lebih kecil, solusi yang lebih baik mungkin muncul seiring waktu, sedangkan jika Anda hanya memiliki konstruksi tingkat tinggi, built-in, ini lebih kecil kemungkinannya.

Jadi ya, saya pikir (saya tidak benar-benar ada ...) itu adalah keputusan yang sadar, dengan tujuan membuat bahasa lebih mudah untuk diambil, tetapi menurut saya, itu adalah keputusan yang buruk. Kemudian lagi, saya umumnya lebih suka C ++ memberikan-programmer-kesempatan-untuk-roll-filosofi mereka sendiri, jadi saya agak bias.

imre
sumber
7
Filosofi "berikan-kepada-programmer-peluang-untuk-menggulung-sendiri" berfungsi dengan baik SAMPAI Anda perlu menggabungkan perpustakaan yang ditulis oleh programmer yang masing-masing menggulirkan kelas string dan pointer cerdas mereka sendiri.
dan04
@ dan04 sehingga bahasa yang dikelola yang memberikan Anda kelas string yang telah ditentukan, kemudian memungkinkan Anda untuk menambal monyet, yang merupakan resep untuk bencana jika Anda adalah tipe pria yang tidak bisa mengatasi string yang digulung sendiri kelas.
gbjbaanb
-1

Anda sudah memanggil kasar setara dengan ini di C # dengan Disposemetode. Java juga punya finalize. CATATAN: Saya menyadari bahwa finalisasi Java adalah non-deterministik dan berbeda dariDispose , saya hanya menunjukkan bahwa mereka berdua memiliki metode pembersihan sumber daya bersama dengan GC.

Namun C ++ menjadi lebih menyakitkan karena suatu objek harus dihancurkan secara fisik. Dalam bahasa tingkat yang lebih tinggi seperti C # dan Java kita bergantung pada pengumpul sampah untuk membersihkannya ketika tidak ada referensi lagi untuk itu. Tidak ada jaminan bahwa objek DBConnection di C ++ tidak memiliki referensi jahat atau petunjuk untuk itu.

Ya, kode C ++ bisa lebih intuitif untuk dibaca tetapi bisa menjadi mimpi buruk untuk di-debug karena batasan dan batasan yang digunakan bahasa seperti Java menyingkirkan beberapa bug yang lebih menjengkelkan dan sulit serta melindungi pengembang lain dari kesalahan pemula yang baru.

Mungkin turun ke preferensi, beberapa seperti kekuatan tingkat rendah, kontrol dan kemurnian C ++ di mana orang lain seperti saya lebih suka bahasa kotak pasir yang lebih eksplisit.

maple_shaft
sumber
12
Pertama-tama "finalisasi" Java adalah non-deterministik ... itu tidak setara dengan "buang" C # atau destruktor C ++ ... juga, C ++ juga memiliki pengumpul sampah jika Anda menggunakan .NET
JoelFan
2
@DeadMG: Masalahnya adalah, Anda mungkin bukan orang idiot, tetapi orang lain yang baru saja meninggalkan perusahaan (dan yang menulis kode yang sekarang Anda pertahankan) mungkin saja.
Kevin
7
Orang itu akan menulis kode menyebalkan apa pun yang Anda lakukan. Anda tidak dapat mengambil programmer yang buruk dan membuatnya menulis kode yang bagus. Pointer yang menggantung adalah keprihatinan saya yang paling sedikit ketika berhadapan dengan orang idiot. Standar pengkodean yang baik menggunakan pointer pintar untuk memori yang harus dilacak, jadi manajemen cerdas harus memperjelas cara mengalokasikan dan mengakses memori dengan aman.
DeadMG
3
Apa yang dikatakan DeadMG. Ada banyak hal buruk tentang C ++. Tapi RAII bukan salah satu dari mereka dalam waktu yang lama. Pada kenyataannya, kurangnya Java dan .NET untuk memperhitungkan dengan benar pengelolaan sumber daya (karena memori adalah satu-satunya sumber daya, bukan?) Adalah salah satu masalah terbesar mereka.
Konrad Rudolph
8
Finalizer menurut saya adalah desain bencana yang bijaksana. Saat Anda memaksakan penggunaan objek yang benar dari perancang ke pengguna objek (bukan dalam hal manajemen memori tetapi manajemen sumber daya). Dalam C ++ itu adalah tanggung jawab desainer kelas untuk mendapatkan manajemen sumber daya yang benar (dilakukan hanya sekali). Di Jawa itu adalah tanggung jawab pengguna kelas untuk mendapatkan manajemen sumber daya yang benar dan dengan demikian harus dilakukan setiap kali kelas yang kami gunakan. stackoverflow.com/questions/161177/…
Martin York