Mengapa paradigma penghancur objek dalam bahasa sampah yang dikumpulkan tidak ada?

27

Mencari wawasan tentang keputusan seputar desain bahasa sampah yang dikumpulkan. Mungkin seorang pakar bahasa bisa mencerahkan saya? Saya berasal dari latar belakang C ++, jadi area ini membingungkan saya.

Tampaknya hampir semua sampah modern mengumpulkan bahasa dengan dukungan objek OOPy seperti Ruby, Javascript / ES6 / ES7, Actionscript, Lua, dll. Sepenuhnya menghilangkan paradigma destruktor / finalisasi. Python tampaknya menjadi satu-satunya dengan class __del__()metodenya. Kenapa ini? Apakah ada keterbatasan fungsional / teoritis dalam bahasa dengan pengumpulan sampah otomatis yang mencegah penerapan metode destruktor / finalisasi objek yang efektif?

Saya merasa sangat kurang bahwa bahasa-bahasa ini menganggap memori sebagai satu - satunya sumber daya yang layak dikelola. Bagaimana dengan soket, pegangan file, status aplikasi? Tanpa kemampuan untuk mengimplementasikan logika kustom untuk membersihkan sumber daya non-memori dan menyatakan pada penyelesaian objek, saya diharuskan untuk membuang sampah aplikasi saya dengan myObject.destroy()panggilan gaya kustom , menempatkan logika pembersihan di luar "kelas" saya, memecahkan enkapsulasi percobaan, dan membuang degradasi saya aplikasi untuk kebocoran sumber daya karena kesalahan manusia daripada secara otomatis ditangani oleh gc.

Apa keputusan desain bahasa yang menyebabkan bahasa-bahasa ini tidak memiliki cara untuk mengeksekusi logika kustom pada pembuangan objek? Saya harus membayangkan ada alasan bagus. Saya ingin lebih memahami keputusan teknis dan teoritis yang mengakibatkan bahasa-bahasa ini tidak memiliki dukungan untuk penghancuran / finalisasi objek.

Memperbarui:

Mungkin cara yang lebih baik untuk mengungkapkan pertanyaan saya:

Mengapa bahasa memiliki konsep bawaan objek contoh dengan kelas atau struktur seperti kelas bersama dengan instantiasi khusus (konstruktor), namun sepenuhnya menghilangkan fungsionalitas penghancuran / penyelesaian? Bahasa yang menawarkan pengumpulan sampah otomatis tampaknya menjadi kandidat utama untuk mendukung penghancuran / penyelesaian objek karena mereka tahu dengan kepastian 100% ketika suatu objek tidak lagi digunakan. Namun sebagian besar bahasa itu tidak mendukungnya.

Saya tidak berpikir itu adalah kasus di mana destruktor mungkin tidak pernah dipanggil, karena itu akan menjadi kebocoran memori inti, yang dirancang untuk menghindari gcs. Saya bisa melihat argumen yang mungkin adalah bahwa destructor / finalizer mungkin tidak dipanggil sampai beberapa waktu di masa depan, tetapi itu tidak menghentikan Java atau Python dari mendukung fungsi.

Apa alasan desain inti bahasa untuk tidak mendukung segala bentuk finalisasi objek?

dbcb
sumber
9
Mungkin karena finalize/ destroyadalah dusta? Tidak ada jaminan itu akan pernah dieksekusi. Dan, bahkan jika, Anda tidak tahu kapan (diberi pengumpulan sampah otomatis), dan jika konteks yang diperlukan masih ada (mungkin sudah dikumpulkan). Jadi lebih aman untuk memastikan keadaan yang konsisten dengan cara lain, dan orang mungkin ingin memaksa programmer untuk melakukannya.
Raphael
1
Saya pikir pertanyaan ini adalah offtopic batas. Apakah ini merupakan pertanyaan desain bahasa pemrograman yang ingin kami hibur, atau apakah itu pertanyaan untuk situs yang lebih berorientasi pada pemrograman? Pemungutan suara komunitas, silakan.
Raphael
14
Ini pertanyaan yang bagus dalam desain PL, mari kita miliki.
Andrej Bauer
3
Ini bukan perbedaan statis / dinamis. Banyak bahasa statis tidak memiliki finalizer. Sebenarnya, bukankah bahasa dengan finalizer dalam minoritas?
Andrej Bauer
1
pikir ada beberapa pertanyaan di sini ... akan lebih baik jika Anda mendefinisikan istilah sedikit lebih. java memiliki akhirnya blok yang tidak terikat dengan penghancuran objek tetapi metode keluar. ada juga cara lain untuk berurusan dengan sumber daya. misal di java, kumpulan koneksi dapat menangani koneksi yang tidak terpakai [x] kapan pun & mengambilnya kembali. tidak elegan tapi berhasil. bagian dari jawaban untuk pertanyaan Anda adalah bahwa pengumpulan sampah kira-kira merupakan proses yang tidak ditentukan, bukan proses instan dan tidak didorong oleh objek yang tidak digunakan lagi tetapi oleh kendala memori / langit-langit yang dipicu.
vzn

Jawaban:

10

Pola yang Anda bicarakan, di mana objek tahu cara membersihkan sumber daya mereka, jatuh ke dalam tiga kategori yang relevan. Mari kita tidak mengacaukan destruktor dengan finalizer - hanya satu yang terkait dengan pengumpulan sampah:

  • The Pola finalizer : metode pembersihan dinyatakan secara otomatis, didefinisikan oleh programmer, yang disebut secara otomatis.

    Finalizers dipanggil secara otomatis sebelum deallokasi oleh seorang pemulung. Istilah ini berlaku jika algoritma pengumpulan sampah yang digunakan dapat menentukan siklus hidup objek.

  • The pola destructor : metode pembersihan dinyatakan secara otomatis, didefinisikan oleh programmer, yang disebut secara otomatis hanya kadang-kadang.

    Destructors dapat dipanggil secara otomatis untuk objek yang dialokasikan stack (karena umur objek adalah deterministik), tetapi harus secara eksplisit dipanggil pada semua jalur eksekusi yang mungkin untuk objek yang dialokasikan heap (karena masa objek tidak bersifat deterministik).

  • The Pola Pemelihara : metode pembersihan menyatakan, didefinisikan, dan disebut oleh programmer.

    Pemrogram membuat metode pembuangan dan menyebutnya sendiri - ini adalah tempat myObject.destroy()metode kustom Anda jatuh. Jika pembuangan mutlak diperlukan, maka pelempar harus dipanggil pada semua jalur eksekusi yang memungkinkan.

Finalizer adalah droid yang Anda cari.

Pola finalizer (pola yang ditanyakan oleh pertanyaan Anda) adalah mekanisme untuk mengaitkan objek dengan sumber daya sistem (soket, deskriptor file, dll.) Untuk saling direklamasi oleh pengumpul sampah. Tetapi, para finalizer pada dasarnya bergantung pada algoritma pengumpulan sampah yang digunakan.

Pertimbangkan asumsi Anda ini:

Bahasa yang menawarkan pengumpulan sampah otomatis ... ketahui dengan kepastian 100% saat sebuah objek tidak lagi digunakan.

Salah secara teknis (terima kasih, @babou). Pengumpulan sampah pada dasarnya adalah tentang ingatan, bukan benda. Jika atau ketika suatu algoritma pengumpulan menyadari memori suatu objek tidak lagi digunakan tergantung pada algoritma dan (mungkin) bagaimana objek Anda merujuk satu sama lain. Mari kita bicara tentang dua jenis pengumpul sampah runtime. Ada banyak cara untuk mengubah dan menambah ini ke teknik dasar:

  1. Menelusuri GC. Ini jejak memori, bukan benda. Kecuali jika ditambah untuk melakukannya, mereka tidak mempertahankan kembali referensi ke objek dari memori. Kecuali jika ditambah, GC ini tidak akan tahu kapan suatu objek dapat diselesaikan, bahkan jika mereka tahu kapan memorinya tidak dapat dijangkau. Oleh karena itu, panggilan finalizer tidak dijamin.

  2. Referensi Menghitung GC . Ini menggunakan objek untuk melacak memori. Mereka memodelkan jangkauan objek dengan grafik referensi yang diarahkan. Jika ada siklus dalam grafik referensi objek Anda, maka semua objek dalam siklus tidak akan pernah memanggil finalizer mereka (sampai penghentian program, jelas). Sekali lagi, panggilan finalizer tidak dijamin.

TLDR

Pengumpulan sampah sulit dan beragam. Panggilan finalizer tidak dapat dijamin sebelum penghentian program.

kdbanman
sumber
Anda benar bahwa ini bukan statis v. Dinamis. Ini masalah dengan bahasa sampah yang dikumpulkan. Pengumpulan sampah adalah masalah yang kompleks dan mungkin merupakan alasan utama karena ada banyak kasus tepi yang perlu dipertimbangkan (misalnya apa yang terjadi jika logika yang finalize()menyebabkan objek dibersihkan untuk direferensikan lagi?). Namun, tidak dapat menjamin finalizer dipanggil sebelum penghentian program tidak menghentikan Java untuk mendukungnya. Tidak mengatakan jawaban Anda salah, mungkin saja tidak lengkap. Masih posting yang sangat bagus. Terima kasih.
dbcb
Terima kasih untuk umpan baliknya. Berikut adalah upaya menyelesaikan jawaban saya: Dengan secara eksplisit menghilangkan finalizer, bahasa memaksa penggunanya untuk mengelola sumber daya mereka sendiri. Untuk banyak jenis masalah, itu mungkin merugikan. Secara pribadi, saya lebih suka pilihan Java, karena saya memiliki kekuatan finalizers dan tidak ada yang menghentikan saya dari menulis dan menggunakan disposer saya sendiri. Java berkata, "Hei, programmer. Kamu bukan idiot, jadi ini finalizer. Berhati-hatilah."
kdbanman
1
Diperbarui pertanyaan awal saya untuk mencerminkan bahwa ini berkaitan dengan sampah yang dikumpulkan bahasa. Menerima jawaban Anda. Terima kasih telah meluangkan waktu untuk menjawab.
dbcb
Saya senang bisa membantu. Apakah klarifikasi komentar saya membuat jawaban saya lebih jelas?
kdbanman
2
Ini baik. Bagi saya, jawaban sebenarnya di sini adalah bahwa bahasa memilih untuk tidak mengimplementasikannya karena nilai yang dirasakan tidak melebihi masalah penerapan fungsi. Bukan tidak mungkin (seperti yang diperlihatkan Java dan Python), tetapi ada pertukaran yang banyak bahasa pilih untuk tidak dibuat.
dbcb
5

Pendeknya

Finalisasi bukan masalah sederhana untuk ditangani oleh pemulung. Mudah digunakan dengan referensi penghitungan GC, tetapi keluarga GC ini sering tidak lengkap, membutuhkan kebocoran memori untuk dikompensasi oleh pemicu eksplisit penghancuran dan finalisasi beberapa objek dan struktur. Melacak pengumpul sampah jauh lebih efektif, tetapi mereka membuat lebih sulit untuk mengidentifikasi objek yang akan diselesaikan dan dihancurkan, bukan hanya mengidentifikasi memori yang tidak terpakai, sehingga membutuhkan manajemen yang lebih kompleks, dengan biaya dalam waktu dan ruang, dan dalam kompleksitas pelaksanaan.

pengantar

Saya berasumsi bahwa apa yang Anda tanyakan adalah mengapa bahasa sampah yang dikumpulkan tidak secara otomatis menangani kehancuran / finalisasi dalam proses pengumpulan sampah, seperti yang ditunjukkan oleh komentar:

Saya merasa sangat kurang bahwa bahasa-bahasa ini menganggap memori sebagai satu-satunya sumber daya yang layak dikelola. Bagaimana dengan soket, pegangan file, status aplikasi?

Saya tidak setuju dengan jawaban yang diterima yang diberikan oleh kdbanman . Sementara fakta-fakta yang disebutkan sebagian besar benar, meskipun sangat bias terhadap penghitungan referensi, saya tidak percaya mereka menjelaskan dengan tepat situasi yang dikeluhkan dalam pertanyaan.

Saya tidak percaya bahwa terminologi yang dikembangkan dalam jawaban itu jauh dari masalah, dan lebih cenderung membingungkan. Memang, sebagaimana disajikan, terminologi sebagian besar ditentukan oleh cara prosedur diaktifkan daripada oleh apa yang mereka lakukan. Intinya adalah bahwa dalam semua kasus, ada kebutuhan untuk menyelesaikan suatu objek tidak lagi diperlukan dengan beberapa proses pembersihan dan untuk membebaskan sumber daya apa pun yang telah digunakan, memori menjadi salah satunya. Idealnya, semua itu harus dilakukan secara otomatis ketika objek tidak lagi digunakan, melalui pengumpul sampah. Dalam praktiknya, GC mungkin hilang atau memiliki kekurangan, dan ini dikompensasikan dengan dipicu secara eksplisit oleh program finalisasi dan reklamasi.

Trigerring eksplisit oleh program adalah masalah karena hal itu memungkinkan untuk sulit menganalisis kesalahan pemrograman, ketika suatu objek yang masih digunakan sedang dihentikan secara eksplisit.

Oleh karena itu jauh lebih baik untuk mengandalkan pengumpulan sampah otomatis untuk mendapatkan kembali sumber daya. Tetapi ada dua masalah:

  • beberapa teknik pengumpulan sampah akan memungkinkan kebocoran memori yang mencegah reklamasi penuh sumber daya. Ini terkenal untuk referensi penghitungan GC, tetapi mungkin muncul untuk teknik GC lainnya ketika menggunakan beberapa organisasi data tanpa perawatan (poin tidak dibahas di sini).

  • sementara teknik GC mungkin baik dalam mengidentifikasi sumber daya memori tidak lagi digunakan, menyelesaikan objek yang terkandung di dalamnya mungkin tidak sederhana, dan yang mempersulit masalah reklamasi sumber daya lain yang digunakan oleh objek-objek ini, yang sering kali merupakan tujuan finalisasi.

Akhirnya, poin penting yang sering dilupakan adalah bahwa siklus GC dapat dipicu oleh apa saja, bukan hanya kekurangan memori, jika kait yang tepat disediakan dan jika biaya siklus GC dianggap layak. Oleh karena itu, boleh saja memulai GC ketika sumber daya apa pun hilang, dengan harapan membebaskan sebagian.

Referensi penghitungan pemulung

Penghitungan referensi adalah teknik pengumpulan sampah yang lemah , yang tidak akan menangani siklus dengan baik. Memang akan lemah dalam menghancurkan struktur usang, dan merebut kembali sumber daya lainnya hanya karena lemah pada reklamasi memori. Tetapi finalizer dapat digunakan dengan paling mudah dengan referensi penghitungan pengumpulan sampah (GC), karena penghitungan ulang GC memang mengklaim kembali suatu struktur ketika penghitungan refnya turun ke 0, di mana saat itu alamatnya diketahui bersama-sama dengan tipenya, baik secara statis atau secara dinamis. Oleh karena itu dimungkinkan untuk mendapatkan kembali memori tepat setelah menerapkan finalizer yang tepat, dan memanggil proses secara rekursif pada semua objek yang ditunjuk (mungkin melalui prosedur finalisasi).

Singkatnya, finalisasi mudah diterapkan dengan Ref Counting GC, tetapi menderita dari "ketidaklengkapan" dari GC itu, memang karena struktur melingkar, sampai pada tingkat yang sama persis dengan yang diderita reklamasi memori. Dengan kata lain, dengan jumlah referensi, memori justru dikelola dengan buruk seperti sumber daya lain seperti soket, pegangan file, dll.

Memang, Ref Count GC ketidakmampuan untuk mengklaim kembali struktur perulangan (secara umum) dapat dilihat sebagai kebocoran memori . Anda tidak dapat mengharapkan semua GC untuk menghindari kebocoran memori. Itu tergantung pada algoritma GC, dan pada tipe struktur informasi yang tersedia secara dinamis (misalnya dalam GC konservatif ).

Melacak pengumpul sampah

Keluarga GC yang lebih kuat, tanpa kebocoran seperti itu, adalah keluarga penelusuran yang mengeksplorasi bagian-bagian langsung dari memori, dimulai dari petunjuk root yang teridentifikasi dengan baik. Semua bagian memori yang tidak dikunjungi dalam proses penelusuran ini (yang sebenarnya dapat didekomposisi dengan berbagai cara, tetapi saya harus menyederhanakan) adalah bagian memori yang tidak digunakan yang dapat dengan demikian direklamasi 1 . Kolektor ini akan mendapatkan kembali semua bagian memori yang tidak lagi dapat diakses oleh program, apa pun fungsinya. Itu memang mengklaim kembali struktur lingkaran, dan GC yang lebih maju didasarkan pada beberapa variasi dari paradigma ini, kadang-kadang sangat canggih. Ini dapat dikombinasikan dengan penghitungan referensi dalam beberapa kasus, dan mengimbangi kelemahannya.

Masalahnya adalah pernyataan Anda (di akhir pertanyaan):

Bahasa yang menawarkan pengumpulan sampah otomatis tampaknya menjadi kandidat utama untuk mendukung penghancuran / penyelesaian objek karena mereka tahu dengan kepastian 100% ketika suatu objek tidak lagi digunakan.

secara teknis tidak benar untuk melacak kolektor.

Yang diketahui dengan kepastian 100% adalah bagian memori mana yang tidak lagi digunakan . (Lebih tepatnya, harus dikatakan bahwa mereka tidak lagi dapat diakses , karena beberapa bagian, yang tidak lagi dapat digunakan sesuai dengan logika program, masih dianggap digunakan jika masih ada pointer yang tidak berguna untuk mereka dalam program. data.) Tetapi pemrosesan lebih lanjut dan struktur yang sesuai diperlukan untuk mengetahui benda apa yang tidak terpakai yang mungkin telah disimpan di bagian memori yang sekarang tidak digunakan . Ini tidak dapat ditentukan dari apa yang diketahui dari program, karena program tidak lagi terhubung ke bagian memori ini.

Dengan demikian setelah melewati pengumpulan sampah, Anda dibiarkan dengan fragmen memori yang berisi benda-benda yang tidak lagi digunakan, tetapi ada apriori tidak ada cara untuk mengetahui apa objek-objek ini sehingga dapat menerapkan finalisasi yang benar. Selanjutnya, jika kolektor pelacak adalah tipe mark-and-sweep, mungkin beberapa fragmen mungkin berisi objek yang telah diselesaikan dalam lintasan GC sebelumnya, tetapi tidak digunakan karena alasan fragmentasi. Namun ini bisa diatasi dengan menggunakan pengetikan eksplisit yang diperluas.

Sementara seorang kolektor sederhana hanya akan mengklaim kembali fragmen memori ini, tanpa basa-basi lagi, finalisasi memerlukan izin khusus untuk mengeksplorasi memori yang tidak terpakai, mengidentifikasi objek yang ada di dalamnya, dan menerapkan prosedur finalisasi. Tetapi eksplorasi semacam itu membutuhkan penentuan jenis objek yang disimpan di sana, dan penentuan jenis juga diperlukan untuk menerapkan finalisasi yang tepat, jika ada.

Sehingga menyiratkan biaya tambahan dalam waktu GC (pass tambahan) dan mungkin biaya memori tambahan untuk membuat informasi jenis yang tepat tersedia selama melewati dengan berbagai teknik. Biaya-biaya ini mungkin signifikan karena kita sering ingin menyelesaikan hanya beberapa objek, sementara waktu dan ruang overhead dapat menyangkut semua objek.

Poin lain adalah bahwa waktu dan ruang overhead mungkin menyangkut eksekusi kode program, dan bukan hanya eksekusi GC.

Saya tidak dapat memberikan jawaban yang lebih tepat, menunjuk pada masalah tertentu, karena saya tidak tahu secara spesifik banyak bahasa yang Anda daftarkan. Dalam kasus C, mengetik adalah masalah yang sangat sulit yang mengarah pada pengembangan kolektor konservatif. Dugaan saya adalah bahwa ini juga mempengaruhi C ++, tapi saya bukan ahli C ++. Ini sepertinya dikonfirmasi oleh Hans Boehm yang melakukan banyak penelitian tentang GC konservatif. GC konservatif tidak dapat mengklaim kembali secara sistematis semua memori yang tidak digunakan secara tepat karena mungkin kekurangan informasi jenis yang tepat pada data. Untuk alasan yang sama, tidak akan dapat secara sistematis menerapkan prosedur penyelesaian.

Jadi, dimungkinkan untuk melakukan apa yang Anda minta, seperti yang Anda tahu dari beberapa bahasa. Tetapi itu tidak datang secara gratis. Bergantung pada bahasa dan implementasinya, mungkin memerlukan biaya bahkan ketika Anda tidak menggunakan fitur. Berbagai teknik dan trade-off dapat dipertimbangkan untuk mengatasi masalah ini, tetapi itu berada di luar cakupan jawaban yang cukup masuk akal.

1 - ini adalah presentasi abstrak dari tracing collection (mencakup baik copy dan mark-and-sweep GC), berbagai hal berbeda sesuai dengan jenis tracing collector, dan menjelajahi bagian memori yang tidak digunakan berbeda, tergantung pada apakah copy atau mark dan sapuan digunakan.

babou
sumber
Anda memberikan banyak detail bagus tentang pengumpulan sampah. Namun, jawaban Anda sebenarnya tidak setuju dengan saya - abstrak dan TLDR saya pada dasarnya mengatakan hal yang sama. Dan untuk apa nilainya, jawaban saya menggunakan penghitungan referensi GC sebagai contoh, bukan "bias kuat".
kdbanman
Setelah membaca lebih teliti, saya melihat perbedaan pendapat. Saya akan mengedit sesuai. Juga, terminologi saya menjadi tidak ambigu. Pertanyaannya adalah menyatukan para finalis dan destruktor, dan bahkan menyebut para disposer dengan nafas yang sama. Berguna untuk menyebarkan kata-kata yang tepat.
kdbanman
@kdbanman Kesulitannya adalah saya berbicara kepada Anda berdua, karena jawaban Anda berdiri sebagai referensi. Anda tidak dapat menggunakan penghitungan ref sebagai contoh paradigmatik karena ini adalah GC yang lemah, jarang digunakan dalam bahasa (periksa bahasa yang dikutip oleh OP), yang menambahkan finalizer sebenarnya akan mudah (tetapi dengan penggunaan terbatas). Menelusuri kolektor hampir selalu digunakan. Tetapi finalizers sulit untuk mengaitkannya, karena objek yang sekarat tidak diketahui (bertentangan dengan pernyataan yang Anda anggap benar). Perbedaan antara pengetikan statis dan dinamis tidak relevan, oleh pengetikan dinamis penyimpanan data sangat penting.
babou
@kdbanman Mengenai terminologi, ini berguna secara umum, karena sesuai dengan situasi yang berbeda. Tapi di sini tidak membantu, karena pertanyaannya adalah tentang mentransfer finalisasi ke GC. GC dasar seharusnya hanya melakukan penghancuran. Yang dibutuhkan adalah terminologi yang membedakan getting memory recycled, yang saya panggil reclamation, dan melakukan beberapa pembersihan sebelum itu, seperti reklamasi sumber daya lain atau memperbarui beberapa tabel objek, yang saya sebut finalization. Bagi saya ini adalah masalah yang relevan, tetapi saya mungkin telah melewatkan satu poin dalam terminologi Anda, yang baru bagi saya.
babou
1
Terima kasih @kdbanman, babou. Diskusi yang bagus Saya pikir kedua posting Anda membahas poin yang sama. Seperti yang Anda berdua tunjukkan, masalah inti tampaknya adalah kategori pengumpul sampah yang digunakan dalam runtime bahasa. Saya menemukan artikel ini , yang membersihkan beberapa kesalahpahaman bagi saya. Tampaknya gcs lebih kuat hanya menangani memori mentah level rendah, yang membuat jenis objek tingkat tinggi buram ke gc. Tanpa pengetahuan internal memori, gc tidak dapat merusak objek. Yang sepertinya menjadi kesimpulan Anda.
dbcb
4

Pola destruktor objek sangat mendasar untuk penanganan kesalahan dalam pemrograman sistem, tetapi tidak ada hubungannya dengan pengumpulan sampah. Sebaliknya, itu ada hubungannya dengan pencocokan objek seumur hidup ke ruang lingkup, dan dapat diimplementasikan / digunakan dalam bahasa apa pun yang memiliki fungsi kelas satu.

Contoh (pseudocode). Misalkan Anda memiliki tipe "file mentah", seperti jenis deskriptor file Posix. Ada empat operasi dasar, open(), close(), read(), write(). Anda ingin menerapkan tipe file "aman" yang selalu dibersihkan sendiri. (Yaitu, yang memiliki konstruktor dan penghancur otomatis.)

Saya akan menganggap bahasa kami memiliki penanganan perkecualian throw, trydan finally(dalam bahasa tanpa penanganan perkecualian, Anda dapat mengatur disiplin di mana pengguna tipe Anda mengembalikan nilai khusus untuk menunjukkan kesalahan.)

Anda mengatur fungsi yang menerima fungsi yang berfungsi. Fungsi pekerja menerima satu argumen (pegangan ke file "aman").

with_file_opened_for_read (string:   filename,
                           function: worker_function(safe_file f)):
  raw_file rf = open(filename, O_RDONLY)
  if rf == error:
    throw File_Open_Error

  try:
    worker_function(rf)
  finally:
    close(rf)

Anda juga menyediakan implementasi dari read()dan write()untuk safe_file(yang hanya memanggil raw_file read()dan write()). Sekarang pengguna menggunakan safe_filetipe seperti ini:

...
with_file_opened_for_read ("myfile.txt",
                           anonymous_function(safe_file f):
                             mytext = read(f)
                             ... (including perhaps throwing an error)
                          )

Sebuah destruktor C ++ benar-benar hanya gula sintaksis untuk satu try-finallyblok. Hampir semua yang saya lakukan di sini adalah mengonversi apa yang safe_fileakan dikompilasi menjadi kelas C ++ dengan konstruktor dan destruktor. Perhatikan bahwa C ++ tidak memiliki finallypengecualian, khususnya karena Stroustrup merasa bahwa menggunakan destruktor eksplisit lebih baik secara sintaksis (dan ia memperkenalkannya ke dalam bahasa sebelum bahasa tersebut memiliki fungsi anonim).

(Ini adalah penyederhanaan dari salah satu cara orang telah melakukan penanganan kesalahan dalam bahasa seperti Lisp selama bertahun-tahun. Saya pikir saya pertama kali bertemu dengannya di akhir 1980-an atau awal 1990-an, tapi saya tidak ingat di mana.)

Logika Pengembaraan
sumber
Ini menjelaskan internal pola destruktor berbasis stack di C ++, tetapi tidak menjelaskan mengapa bahasa sampah yang dikumpulkan tidak akan mengimplementasikan fungsionalitas tersebut. Anda mungkin benar bahwa ini tidak ada hubungannya dengan pengumpulan sampah, tetapi ini terkait dengan penghancuran / finalisasi objek umum, yang tampaknya sulit atau tidak efisien dalam bahasa sampah yang dikumpulkan. Jadi jika kehancuran umum tidak didukung, penghancuran berbasis tumpukan tampaknya dihilangkan juga.
dbcb
Seperti yang saya katakan di awal: setiap sampah yang dikumpulkan bahasa yang memiliki fungsi kelas satu (atau beberapa perkiraan fungsi kelas satu) memberi Anda kemampuan untuk menyediakan antarmuka seperti "bukti peluru" seperti safe_filedan with_file_opened_for_read(objek yang menutup sendiri ketika keluar dari ruang lingkup ). Itu yang penting, bahwa itu tidak memiliki sintaks yang sama dengan konstruktor tidak relevan. Lisp, Skema, Java, Scala, Go, Haskell, Rust, Javascript, Clojure semuanya mendukung fungsi-fungsi kelas satu yang cukup, sehingga mereka tidak memerlukan destruktor untuk menyediakan fitur berguna yang sama.
Pengembaraan Logika
Saya rasa saya mengerti apa yang Anda katakan. Karena bahasa menyediakan blok bangunan dasar (coba / tangkap / akhirnya, fungsi kelas satu, dll.) Untuk secara manual mengimplementasikan fungsionalitas seperti destruktor, mereka tidak memerlukan destruktor? Saya bisa melihat beberapa bahasa mengambil rute itu karena alasan kesederhanaan. Meskipun, sepertinya tidak mungkin itulah alasan utama untuk semua bahasa yang terdaftar, tapi mungkin itu yang sebenarnya. Mungkin saya hanya ada di minoritas luas yang menyukai destruktor C ++ dan tidak ada orang lain yang benar-benar peduli, yang bisa menjadi alasan mengapa sebagian besar bahasa tidak menerapkan destruktor. Mereka hanya tidak peduli.
dbcb
2

Ini bukan jawaban penuh untuk pertanyaan itu, tetapi saya ingin menambahkan beberapa pengamatan yang belum tercakup dalam jawaban atau komentar lainnya.

  1. Pertanyaan secara implisit mengasumsikan bahwa kita sedang berbicara tentang bahasa berorientasi objek gaya Simula, yang dengan sendirinya membatasi. Dalam kebanyakan bahasa, bahkan mereka yang memiliki objek, tidak semuanya adalah objek. Mesin untuk menerapkan destruktor akan membebankan biaya yang tidak semua pelaksana bahasa mau membayar.

  2. C ++ memiliki beberapa jaminan implisit tentang perintah penghancuran. Jika Anda memiliki struktur data seperti pohon, misalnya, anak-anak akan dihancurkan sebelum orang tua. Ini bukan kasus dalam bahasa GC'd, jadi sumber daya hierarkis dapat dirilis dalam urutan yang tidak terduga. Untuk sumber daya non-memori, ini bisa penting.

Nama samaran
sumber
2

Ketika dua kerangka kerja GC yang paling populer (Java dan .NET) sedang dirancang, saya pikir para penulis berharap bahwa finalisasi akan bekerja dengan cukup baik untuk menghindari kebutuhan akan bentuk-bentuk pengelolaan sumber daya lainnya. Banyak aspek desain bahasa dan kerangka kerja dapat sangat disederhanakan jika tidak perlu semua fitur yang diperlukan untuk mengakomodasi 100% manajemen sumber daya yang andal dan deterministik. Dalam C ++, penting untuk membedakan antara konsep:

  1. Pointer / referensi yang mengidentifikasi objek yang secara eksklusif dimiliki oleh pemegang referensi, dan yang tidak diidentifikasi oleh pointer / referensi yang tidak diketahui pemiliknya.

  2. Pointer / referensi yang mengidentifikasi objek yang dapat dibagi yang tidak secara eksklusif dimiliki oleh siapa pun.

  3. Pointer / referensi yang mengidentifikasi objek yang secara eksklusif dimiliki oleh pemegang referensi, tetapi yang dapat diakses melalui "pandangan" pemilik tidak memiliki cara untuk melacak.

  4. Pointer / referensi yang mengidentifikasi objek yang menyediakan tampilan objek yang dimiliki oleh orang lain.

Jika bahasa / kerangka kerja GC tidak perlu khawatir tentang manajemen sumber daya, semua hal di atas dapat diganti dengan satu jenis referensi.

Saya akan menemukan ide naif bahwa finalisasi akan menghilangkan kebutuhan untuk bentuk-bentuk lain dari manajemen sumber daya, tetapi apakah harapan seperti itu masuk akal atau tidak pada saat itu, sejarah telah menunjukkan bahwa ada banyak kasus yang membutuhkan manajemen sumber daya yang lebih tepat daripada penyelesaian menyediakan . Saya kebetulan berpikir bahwa penghargaan mengakui kepemilikan pada tingkat bahasa / kerangka kerja akan cukup untuk membenarkan biaya (kompleksitas harus ada di suatu tempat, dan memindahkannya ke bahasa / kerangka kerja akan menyederhanakan kode pengguna) tetapi mengakui bahwa ada signifikansi manfaat desain untuk memiliki satu "jenis" referensi - sesuatu yang hanya bekerja jika bahasa / kerangka kerja agnostik dengan masalah pembersihan sumber daya.

supercat
sumber
2

Mengapa paradigma penghancur objek dalam bahasa sampah yang dikumpulkan tidak ada?

Saya berasal dari latar belakang C ++, jadi area ini membingungkan saya.

Destructor dalam C ++ sebenarnya menggabungkan dua hal . Ini membebaskan RAM dan membebaskan id sumber daya.

Bahasa lain memisahkan masalah ini dengan membuat GC bertanggung jawab membebaskan RAM sementara fitur bahasa lain bertanggung jawab membebaskan id sumber daya.

Saya merasa sangat kurang bahwa bahasa-bahasa ini menganggap memori sebagai satu-satunya sumber daya yang layak dikelola.

Itu semua tentang GC. Mereka hanya melakukan satu hal dan itu untuk memastikan Anda tidak kehabisan memori. Jika RAM tidak terbatas, semua GC akan pensiun karena tidak ada lagi alasan nyata bagi mereka untuk ada.

Bagaimana dengan soket, pegangan file, status aplikasi?

Bahasa dapat menyediakan berbagai cara untuk membebaskan id sumber daya dengan:

  • manual .CloseOrDispose()tersebar di seluruh kode

  • manual .CloseOrDispose()tersebar di " finallyblok " manual

  • manual "blok id sumber daya" (yaitu using, with, try-dengan-sumber daya , dll) yang mengotomatiskan .CloseOrDispose()setelah blok tersebut dilakukan

  • dijamin "blok id sumber daya" yang terotomatisasi.CloseOrDispose() setelah blok selesai

Banyak bahasa menggunakan mekanisme manual (yang bertentangan dengan jaminan) yang menciptakan peluang untuk pengelolaan sumber daya yang salah. Ambil kode NodeJS sederhana ini:

require('fs').openSync('file1.txt', 'w');
// forget to .closeSync the opened file

..di mana programmer lupa untuk menutup file yang dibuka.

Selama program terus berjalan, file yang dibuka akan terjebak dalam limbo. Ini mudah diverifikasi dengan mencoba membuka file menggunakan HxD dan memverifikasi bahwa itu tidak dapat dilakukan:

masukkan deskripsi gambar di sini

Membebaskan id sumber daya di dalam destruktor C ++ juga tidak dijamin. Anda mungkin berpikir RAII beroperasi seperti "blok id sumber daya" yang dijamin, namun tidak seperti "blok id sumber daya", bahasa C ++ tidak menghentikan objek yang menyediakan blok RAII agar tidak bocor , sehingga blok RAII mungkin tidak pernah dilakukan .


Tampaknya hampir semua sampah modern mengumpulkan bahasa dengan dukungan objek OOPy seperti Ruby, Javascript / ES6 / ES7, Actionscript, Lua, dll. Sepenuhnya menghilangkan paradigma destruktor / finalisasi. Python tampaknya menjadi satu-satunya dengan __del__()metode kelasnya . Kenapa ini?

Karena mereka mengelola id sumber daya menggunakan cara lain, seperti yang disebutkan di atas.

Apa keputusan desain bahasa yang menyebabkan bahasa-bahasa ini tidak memiliki cara untuk mengeksekusi logika kustom pada pembuangan objek?

Karena mereka mengelola id sumber daya menggunakan cara lain, seperti yang disebutkan di atas.

Mengapa bahasa memiliki konsep bawaan objek contoh dengan kelas atau struktur seperti kelas bersama dengan instantiasi khusus (konstruktor), namun sepenuhnya menghilangkan fungsionalitas penghancuran / penyelesaian?

Karena mereka mengelola id sumber daya menggunakan cara lain, seperti yang disebutkan di atas.

Saya bisa melihat argumen yang mungkin adalah bahwa destructor / finalizer mungkin tidak dipanggil sampai beberapa waktu di masa depan, tetapi itu tidak menghentikan Java atau Python dari mendukung fungsi.

Java tidak memiliki destruktor.

Dokumen Java menyebutkan :

tujuan finalisasi yang biasa, bagaimanapun, adalah untuk melakukan tindakan pembersihan sebelum objek dibuang secara permanen. Misalnya, metode finalisasi untuk objek yang mewakili koneksi input / output mungkin melakukan transaksi I / O eksplisit untuk memutuskan koneksi sebelum objek tersebut dibuang secara permanen.

..tapi menempatkan kode manajemen resource-id di dalam Object.finalizersebagian besar dianggap sebagai anti-pola ( lih .). Kode-kode itu seharusnya ditulis di situs panggilan.

Untuk orang-orang yang menggunakan anti-pola, pembenaran mereka adalah bahwa mereka mungkin lupa untuk melepaskan id sumber daya di situs panggilan. Dengan demikian, mereka melakukannya lagi di finalizer, untuk berjaga-jaga.

Apa alasan desain inti bahasa untuk tidak mendukung segala bentuk finalisasi objek?

Tidak ada banyak kasus penggunaan untuk finalizer karena mereka menjalankan sepotong kode antara waktu ketika tidak ada lagi referensi kuat ke objek, dan waktu ketika memori itu direklamasi oleh GC.

Case use yang mungkin adalah ketika Anda ingin menyimpan catatan waktu antara objek yang dikumpulkan oleh GC dan waktu ketika tidak ada lagi referensi kuat ke objek, seperti:

finalize() {
    Log(TimeNow() + ". Obj " + toString() + " is going to be memory-collected soon!"); // "soon"
}
Pacerier
sumber
-1

menemukan referensi tentang hal ini di Dr Dobbs wrt c ++ yang memiliki gagasan yang lebih umum yang berpendapat destructor bermasalah dalam bahasa di mana mereka diterapkan. ide yang kasar di sini tampaknya adalah bahwa tujuan utama dari destructor adalah untuk menangani deallokasi memori, dan itu sulit dicapai dengan benar. memori dialokasikan sedikit demi sedikit tetapi objek yang berbeda terhubung dan kemudian tanggung jawab / alokasi deallokasi tidak begitu jelas.

jadi solusi untuk ini dari seorang pengumpul sampah berevolusi tahun yang lalu, tetapi pengumpulan sampah tidak didasarkan pada objek yang menghilang dari ruang lingkup pada metode keluar (itu adalah ide konseptual yang sulit untuk diterapkan), tetapi pada pengumpul yang berjalan secara berkala, agak tidak deterministik, ketika aplikasi mengalami "tekanan memori" (yaitu kehabisan memori).

dengan kata lain konsep manusia semata-mata tentang "objek yang baru tidak digunakan" sebenarnya dalam beberapa hal adalah abstraksi menyesatkan dalam arti bahwa tidak ada objek yang "instan" menjadi tidak terpakai. objek yang tidak terpakai hanya dapat "ditemukan" dengan menjalankan algoritma pengumpulan sampah yang melintasi grafik referensi objek dan algoritma berkinerja terbaik berjalan sebentar-sebentar.

mungkin saja algoritma pengumpulan sampah yang lebih baik menunggu untuk ditemukan yang dapat secara instan mengidentifikasi objek yang tidak digunakan, yang kemudian dapat mengarah pada kode panggilan destruktor yang konsisten, tetapi orang belum ditemukan setelah bertahun-tahun penelitian di daerah tersebut.

solusi untuk area manajemen sumber daya seperti file atau koneksi tampaknya memiliki objek "manajer" yang berupaya menangani penggunaannya.

ay
sumber
2
Temuan yang menarik. Terima kasih. Argumen penulis didasarkan pada destruktor yang dipanggil pada waktu yang salah karena melewati instance kelas dengan nilai di mana kelas tidak memiliki copy constructor yang tepat (yang merupakan masalah nyata). Namun, skenario ini tidak benar-benar ada di sebagian besar (jika tidak semua) bahasa dinamis modern karena semuanya disahkan oleh referensi yang menghindari situasi penulis. Meskipun ini adalah perspektif yang menarik, saya tidak berpikir itu menjelaskan mengapa sebagian besar bahasa sampah dikumpulkan telah memilih untuk menghilangkan fungsionalitas destruktor / finalisasi.
dbcb
2
Jawaban ini salah mengartikan artikel Dr. Dobb: artikel tersebut tidak menyatakan bahwa destruktor secara umum bermasalah. Artikel ini sebenarnya memperdebatkan ini: Manajemen memori primitif seperti pernyataan goto, karena keduanya sederhana tetapi terlalu kuat. Dengan cara yang sama bahwa pernyataan goto paling baik dikemas dalam "struktur kontrol terbatas yang tepat" (Lihat: Dijktsra), manajemen memori primitif paling baik dienkapsulasi dalam "struktur data terbatas yang tepat." Destructors adalah langkah ke arah ini, tetapi tidak cukup jauh. Putuskan sendiri apakah itu benar atau tidak.
kdbanman