Seorang rekan kerja muda yang sedang mempelajari OO telah bertanya kepada saya mengapa setiap objek dilewatkan dengan referensi, yang merupakan kebalikan dari tipe atau struct primitif. Ini adalah karakteristik umum dari bahasa seperti Java dan C #.
Saya tidak dapat menemukan jawaban yang bagus untuknya.
Apa motivasi untuk keputusan desain ini? Apakah pengembang bahasa ini lelah karena harus membuat pointer dan mengetik setiap saat?
programming-languages
object-oriented
Gustavo Cardoso
sumber
sumber
Jawaban:
Alasan dasar sampai pada ini:
Karena itu, referensi.
sumber
Jawaban sederhana:
Meminimalkan konsumsi memori
dan
waktu CPU dalam menciptakan dan melakukan salinan yang mendalam dari setiap objek yang dilewatkan di suatu tempat.
sumber
Di C ++, Anda memiliki dua opsi utama: kembali dengan nilai atau kembali dengan pointer. Mari kita lihat yang pertama:
Dengan asumsi kompiler Anda tidak cukup pintar untuk menggunakan optimasi nilai balik, yang terjadi di sini adalah ini:
Kami telah membuat salinan objek tanpa tujuan. Ini buang waktu pemrosesan.
Mari kita lihat return-by-pointer sebagai gantinya:
Kami telah menghilangkan salinan yang berlebihan, tetapi sekarang kami telah memperkenalkan masalah lain: kami telah membuat objek di heap yang tidak akan hancur secara otomatis. Kita harus menghadapinya sendiri:
Mengetahui siapa yang bertanggung jawab untuk menghapus objek yang dialokasikan dengan cara ini adalah sesuatu yang hanya dapat dikomunikasikan dengan komentar atau dengan konvensi. Dengan mudah menyebabkan kebocoran memori.
Banyak solusi telah disarankan untuk menyelesaikan dua masalah ini - optimasi nilai kembali (di mana kompiler cukup pintar untuk tidak membuat salinan berlebihan dalam nilai-demi-nilai), meneruskan referensi ke metode (sehingga fungsi menyuntikkan ke dalam objek yang sudah ada daripada membuat yang baru), smart pointer (sehingga pertanyaan tentang kepemilikan diperdebatkan).
Pembuat Java / C menyadari bahwa selalu mengembalikan objek dengan referensi adalah solusi yang lebih baik, terutama jika bahasa mendukungnya secara asli. Ini terkait dengan banyak fitur lain yang dimiliki bahasa, seperti pengumpulan sampah, dll.
sumber
Banyak jawaban lain punya info bagus. Saya ingin menambahkan satu poin penting tentang kloning yang hanya dibahas sebagian.
Menggunakan referensi itu cerdas. Menyalin sesuatu itu berbahaya.
Seperti yang orang lain katakan, di Jawa, tidak ada "klon" alami. Ini bukan hanya fitur yang hilang. Anda tidak akan pernah mau hanya menyalin * mau tak mau (baik dangkal atau dalam) setiap properti dalam suatu objek. Bagaimana jika properti itu adalah koneksi basis data? Anda tidak bisa hanya "mengkloning" koneksi basis data lagi daripada mengkloning manusia. Inisialisasi ada karena suatu alasan.
Salinan yang dalam adalah masalah mereka sendiri - seberapa dalam Anda benar - benar pergi? Anda pasti tidak dapat menyalin apa pun yang statis (termasuk
Class
objek apa pun ).Jadi untuk alasan yang sama mengapa tidak ada kloning alami, objek yang dilewatkan sebagai salinan akan membuat kegilaan . Bahkan jika Anda dapat "mengkloning" koneksi DB - bagaimana Anda sekarang memastikan bahwa itu ditutup?
* Lihat komentar - Dengan pernyataan "tidak pernah" ini, maksud saya klon-otomatis yang mengkloning setiap properti. Java tidak menyediakannya, dan mungkin bukan ide yang baik bagi Anda sebagai pengguna bahasa untuk membuat sendiri, karena alasan yang tercantum di sini. Mengkloning hanya bidang non-transien akan menjadi awal, tetapi bahkan kemudian Anda harus rajin mendefinisikan
transient
mana yang sesuai.sumber
clone
baik-baik saja. Maksudnya "mau tak mau" maksud saya menyalin semua properti tanpa memikirkannya - tanpa maksud yang disengaja. Perancang bahasa Jawa memaksa niat ini dengan mengharuskan implementasi yang dibuat penggunaclone
.Objek selalu direferensikan di Jawa. Mereka tidak pernah melewati diri mereka sendiri.
Satu keuntungannya adalah ini menyederhanakan bahasa. Objek C ++ dapat direpresentasikan sebagai nilai atau referensi, menciptakan kebutuhan untuk menggunakan dua operator yang berbeda untuk mengakses anggota:
.
dan->
. (Ada alasan mengapa ini tidak dapat dikonsolidasikan; misalnya, smart pointer adalah nilai yang merupakan referensi, dan harus membuatnya berbeda.) Java hanya perlu.
.Alasan lain adalah bahwa polimorfisme harus dilakukan dengan referensi, bukan nilai; objek yang diperlakukan oleh nilai ada di sana, dan memiliki tipe tetap. Dimungkinkan untuk mengacaukannya dalam C ++.
Juga, Java dapat mengganti tugas default / salin / apa pun. Dalam C ++, ini adalah salinan yang kurang lebih dalam, sedangkan di Jawa itu adalah penugasan pointer sederhana / salin / apa pun, dengan
.clone()
dan jika Anda perlu menyalin.sumber
const
, nilainya dapat diubah untuk menunjuk ke objek lain. Referensi adalah nama lain untuk suatu objek, tidak bisa NULL, dan tidak bisa diulang. Ini umumnya diimplementasikan dengan penggunaan pointer sederhana, tapi itu detail implementasi.Pernyataan awal Anda tentang objek C # yang diteruskan oleh referensi tidak benar. Dalam C #, objek adalah tipe referensi, tetapi secara default mereka dilewatkan oleh nilai seperti tipe nilai. Dalam kasus tipe referensi, "nilai" yang sedang disalin sebagai parameter metode pass-by-value adalah referensi itu sendiri, jadi perubahan properti di dalam metode akan tercermin di luar ruang lingkup metode.
Namun, jika Anda menetapkan ulang variabel parameter itu sendiri di dalam suatu metode, Anda akan melihat bahwa perubahan ini tidak tercermin di luar ruang lingkup metode. Sebaliknya, jika Anda benar-benar melewati parameter dengan referensi menggunakan
ref
kata kunci, perilaku ini berfungsi seperti yang diharapkan.sumber
Jawaban cepat
Para perancang bahasa Jawa dan sejenisnya ingin menerapkan konsep "semuanya adalah objek". Dan melewatkan data sebagai referensi sangat cepat dan tidak menghabiskan banyak memori.
Tambahan komentar membosankan yang diperpanjang
Selain itu, bahasa-bahasa tersebut menggunakan referensi objek (Java, Delphi, C #, VB.NET, Vala, Scala, PHP), kebenarannya adalah bahwa referensi objek adalah penunjuk ke objek yang disamarkan. Nilai nol, alokasi memori, salinan referensi tanpa menyalin seluruh data suatu objek, semuanya adalah pointer objek, bukan objek biasa !!!
Dalam Object Pascal (bukan Delphi), anc C ++ (bukan Java, bukan C #), sebuah objek dapat dideklarasikan sebagai variabel alokasi statis, dan juga dengan variabel alokasi dinamis, melalui penggunaan pointer ("referensi objek" tanpa " sintaksis gula "). Setiap kasus menggunakan sintaks tertentu, dan tidak ada cara untuk bingung seperti di Jawa "dan teman". Dalam bahasa-bahasa tersebut, suatu objek dapat dilewatkan sebagai nilai atau sebagai referensi.
Pemrogram tahu kapan sintaks pointer diperlukan, dan kapan tidak diperlukan, tetapi dalam bahasa Jawa dan bahasa yang sama, ini membingungkan.
Sebelum Java ada atau menjadi arus utama, banyak programmer mempelajari OO dalam C ++ tanpa pointer, melewati nilai atau dengan referensi ketika diperlukan. Ketika beralih dari belajar ke aplikasi bisnis., Maka, mereka biasanya menggunakan pointer objek. Perpustakaan QT adalah contoh yang bagus untuk itu.
Ketika saya belajar Java, saya mencoba mengikuti semuanya adalah konsep objek, tetapi menjadi bingung dalam pengkodean. Akhirnya, saya berkata "ok, ini adalah objek yang dialokasikan secara dinamis dengan pointer dengan sintaks dari objek yang dialokasikan secara statis", dan tidak mengalami kesulitan untuk kode lagi.
sumber
Java dan C # mengambil kendali atas memori tingkat rendah dari Anda. "Tumpukan" tempat benda-benda yang Anda ciptakan berada menjalani kehidupannya sendiri; misalnya, pengumpul sampah menuai benda kapan pun dia mau.
Karena ada lapisan terpisah dari tipuan antara program Anda dan "tumpukan" itu, dua cara untuk merujuk ke objek, dengan nilai dan dengan penunjuk (seperti dalam C ++), menjadi tidak dapat dibedakan : Anda selalu merujuk ke objek "dengan penunjuk" ke suatu tempat di tumpukan. Itu sebabnya pendekatan desain seperti itu menjadikan pass-by-reference sebagai semantik standar penugasan. Java, C #, Ruby, dan sebagainya.
Di atas hanya menyangkut bahasa imperatif. Dalam bahasa yang disebutkan di atas kontrol atas memori akan diteruskan ke runtime, tapi desain bahasa juga mengatakan "hey, tetapi sebenarnya, ada adalah memori, dan ada yang benda-benda, dan mereka melakukan menempati memori". Bahasa fungsional abstrak lebih jauh, dengan mengecualikan konsep "memori" dari definisi mereka. Itu sebabnya pass-by-reference tidak selalu berlaku untuk semua bahasa di mana Anda tidak mengontrol memori tingkat rendah.
sumber
Saya dapat memikirkan beberapa alasan:
Menyalin tipe primitif sepele, biasanya diterjemahkan menjadi satu instruksi mesin.
Menyalin objek tidak sepele, objek dapat berisi anggota yang merupakan objek itu sendiri. Menyalin objek mahal dalam waktu dan memori CPU. Bahkan ada beberapa cara menyalin objek tergantung pada konteksnya.
Melewati objek dengan referensi itu murah dan itu juga menjadi berguna ketika Anda ingin berbagi / memperbarui informasi objek antara banyak klien objek.
Struktur data yang kompleks (terutama yang bersifat rekursif) memerlukan petunjuk. Melewati objek dengan referensi hanyalah cara yang lebih aman untuk melewati pointer.
sumber
Karena jika tidak, fungsi harus dapat secara otomatis membuat salinan (jelas dalam) dari segala jenis objek yang diteruskan ke sana. Dan biasanya itu tidak bisa ditebak untuk membuatnya. Jadi Anda harus mendefinisikan implementasi metode salin / klon untuk semua objek / kelas Anda.
sumber
Karena Java dirancang sebagai C ++ yang lebih baik, dan C # dirancang sebagai Java yang lebih baik, dan para pengembang bahasa ini bosan dengan model objek C ++ yang rusak secara fundamental, di mana objek adalah tipe nilai.
Dua dari tiga prinsip dasar pemrograman berorientasi objek adalah pewarisan dan polimorfisme, dan memperlakukan objek sebagai tipe nilai alih-alih tipe referensi mendatangkan malapetaka pada keduanya. Ketika Anda melewatkan objek ke fungsi sebagai parameter, kompiler perlu tahu berapa byte yang harus dilewati. Ketika objek Anda adalah tipe referensi, jawabannya sederhana: ukuran pointer, sama untuk semua objek. Tetapi ketika objek Anda adalah tipe nilai, itu harus melewati ukuran sebenarnya dari nilai. Karena kelas turunan dapat menambahkan bidang baru, ini berarti sizeof (diturunkan)! = Sizeof (basis), dan polimorfisme keluar jendela.
Berikut adalah program C ++ sepele yang menunjukkan masalah:
Output dari program ini bukanlah seperti program yang setara dalam bahasa OO yang waras, karena Anda tidak dapat melewatkan objek turunan berdasarkan nilai ke fungsi yang mengharapkan objek dasar, sehingga kompilator membuat copy constructor dan pass tersembunyi salinan bagian Orangtua dari objek Anak , bukannya melewati objek Anak seperti yang Anda perintahkan. Gotcha semantik tersembunyi seperti ini adalah alasan mengapa melewatkan objek dengan nilai harus dihindari dalam C ++ dan tidak mungkin sama sekali di hampir setiap bahasa OO lainnya.
sumber
Karena tidak akan ada polimorfisme sebaliknya.
Di Pemrograman OO, Anda dapat membuat
Derived
kelas yang lebih besar dari yangBase
satu, dan kemudian meneruskannya ke fungsi yang mengharapkanBase
satu. Cukup sepele ya?Kecuali bahwa ukuran argumen fungsi tetap, dan ditentukan pada waktu kompilasi. Anda dapat memperdebatkan semua yang Anda inginkan, kode yang dapat dieksekusi seperti ini, dan bahasa perlu dijalankan pada satu titik atau yang lain (bahasa yang ditafsirkan murni tidak dibatasi oleh ini ...)
Sekarang, ada satu bagian data yang terdefinisi dengan baik di komputer: alamat sel memori, biasanya dinyatakan sebagai satu atau dua "kata". Itu terlihat baik sebagai pointer atau referensi dalam bahasa pemrograman.
Jadi untuk melewatkan objek yang panjangnya sewenang-wenang, hal yang paling sederhana untuk dilakukan adalah melewatkan pointer / referensi ke objek ini.
Ini adalah batasan teknis Pemrograman OO.
Tapi karena untuk tipe besar, Anda biasanya lebih suka melewati referensi untuk menghindari penyalinan, itu umumnya tidak dianggap sebagai pukulan besar :)
Namun ada satu konsekuensi penting, di Java atau C #, ketika meneruskan objek ke suatu metode, Anda tidak tahu apakah objek Anda akan dimodifikasi oleh metode tersebut atau tidak. Itu membuat debugging / paralelisasi yang lebih sulit, dan ini adalah masalah Bahasa Fungsional dan Referensi Transparansi mencoba untuk mengatasi -> menyalin tidak terlalu buruk (ketika masuk akal).
sumber
Jawabannya ada dalam nama (well anyways). Referensi (seperti alamat) hanya merujuk ke sesuatu yang lain, suatu nilai adalah salinan dari sesuatu yang lain. Saya yakin seseorang mungkin menyebutkan sesuatu dengan efek berikut tetapi akan ada keadaan di mana satu dan tidak yang lain cocok (Keamanan memori vs Efisiensi Memori). Ini semua tentang mengelola memori, memori, memori ...... MEMORY! : D
sumber
Baiklah jadi saya tidak mengatakan ini adalah mengapa objek adalah tipe referensi atau lulus dengan referensi tapi saya bisa memberi Anda sebuah contoh mengapa ini adalah ide yang sangat bagus dalam jangka panjang.
Jika saya tidak salah, ketika Anda mewarisi kelas di C ++, semua metode dan properti dari kelas itu secara fisik disalin ke kelas anak. Itu seperti menulis isi kelas itu lagi di dalam kelas anak.
Jadi ini berarti bahwa ukuran total data di kelas anak Anda adalah kombinasi dari barang-barang di kelas induk dan kelas turunan.
EG: #termasuk
Yang akan menunjukkan kepada Anda:
Ini berarti bahwa jika Anda memiliki hierarki besar dari beberapa kelas, ukuran total objek, seperti yang dinyatakan di sini akan menjadi kombinasi dari semua kelas tersebut. Jelas, benda-benda ini akan sangat besar dalam banyak kasus.
Solusinya, saya percaya, adalah membuatnya di heap dan menggunakan pointer. Ini berarti bahwa ukuran objek kelas dengan beberapa orang tua akan dapat dikelola, dalam arti tertentu.
Inilah sebabnya mengapa menggunakan referensi akan menjadi metode yang lebih disukai untuk melakukan ini.
sumber