Mengapa begitu banyak bahasa yang lulus nilai?

36

Bahkan bahasa di mana Anda memiliki manipulasi pointer eksplisit seperti C selalu dilewati oleh nilai (Anda dapat meneruskannya dengan referensi tetapi itu bukan perilaku default).

Apa manfaatnya ini, mengapa begitu banyak bahasa yang dilewatkan oleh nilai-nilai dan mengapa yang lain lulus dengan referensi ? (Saya mengerti Haskell disahkan oleh referensi meskipun saya tidak yakin).

Kode saya tidak memiliki bug
sumber
5
void acceptEntireProgrammingLanguageByValue(C++);
Thomas Eding
4
Itu bisa menjadi lebih buruk. Beberapa bahasa lama juga diizinkan untuk dipanggil dengan nama
hugomg
25
Sebenarnya, di C Anda tidak dapat melewati referensi. Anda dapat melewatkan sebuah pointer berdasarkan nilai , yang sangat mirip dengan pass-by-reference, tetapi bukan hal yang sama. Di C ++, Anda dapat melewati referensi.
Mason Wheeler
@Mason Wheeler dapatkah Anda menguraikan lebih lanjut atau menambahkan beberapa tautan, karena pernyataan Anda tidak jelas bagi saya karena saya bukan C / C ++ guru, hanya seorang programmer biasa, terima kasih
Betlista
1
@Betlista: Dengan referensi per referensi, Anda dapat menulis rutin swap yang terlihat seperti ini: temp := a; a := b; b := temp;Dan ketika kembali, nilai adan bakan ditukar. Tidak ada cara untuk melakukan itu di C; Anda harus menyampaikan petunjuk adan bdan swaprutinitas harus bertindak berdasarkan nilai yang mereka tunjuk.
Mason Wheeler

Jawaban:

58

Pass by value seringkali lebih aman daripada pass by reference, karena Anda tidak dapat secara tidak sengaja memodifikasi parameter ke metode / fungsi Anda. Ini membuat bahasa lebih mudah digunakan, karena Anda tidak perlu khawatir tentang variabel yang Anda berikan ke suatu fungsi. Anda tahu mereka tidak akan diubah, dan ini yang sering Anda harapkan .

Namun, jika Anda ingin memodifikasi parameter, Anda harus memiliki beberapa operasi eksplisit untuk memperjelas hal ini (menyampaikan pointer). Ini akan memaksa semua penelepon Anda untuk membuat panggilan sedikit berbeda ( &variable, dalam C) dan ini membuatnya eksplisit bahwa parameter variabel dapat diubah.

Jadi sekarang Anda dapat mengasumsikan bahwa suatu fungsi tidak akan mengubah parameter variabel Anda, kecuali jika itu ditandai secara eksplisit untuk melakukannya (dengan mengharuskan Anda untuk memasukkan pointer). Ini adalah solusi yang lebih aman dan bersih daripada alternatifnya: Asumsikan semuanya dapat mengubah parameter Anda, kecuali mereka secara khusus mengatakan mereka tidak bisa.

Oleksi
sumber
4
+1 Array yang membusuk ke pointer menyajikan pengecualian yang penting, tetapi keseluruhan penjelasannya bagus.
dasblinkenlight
6
@dasblinkenlight: array adalah tempat yang sakit di C :(
Matthieu M.
55

Call-by-value dan call-by-reference adalah teknik implementasi yang keliru untuk mode parameter-passing sejak lama.

Awalnya, ada FORTRAN. FORTRAN hanya memiliki call-by-reference, karena subrutin harus dapat memodifikasi parameter mereka, dan siklus komputasi terlalu mahal untuk memungkinkan beberapa mode parameter-passing, ditambah tidak cukup diketahui tentang pemrograman ketika FORTRAN pertama kali didefinisikan.

ALGOL datang dengan nama panggilan dan nilai panggilan. Nilai panggilan-adalah untuk hal-hal yang tidak seharusnya diubah (parameter input). Call-by-name adalah untuk parameter output. Panggilan-dengan-nama ternyata menjadi tempayan besar, dan ALGOL 68 menjatuhkannya.

PASCAL menyediakan call-by-value dan call-by-reference. Itu tidak menyediakan cara bagi programmer untuk memberitahu kompiler bahwa ia melewati objek besar (biasanya array) dengan referensi, untuk menghindari meniup tumpukan parameter, tetapi objek tidak boleh diubah.

PASCAL menambahkan pointer ke leksikon desain bahasa.

C memberikan call-by-value, dan simulasi call-by-reference dengan mendefinisikan operator kludge untuk mengembalikan pointer ke objek yang berubah-ubah dalam memori.

Kemudian bahasa-bahasa disalin C, terutama karena para perancang tidak pernah melihat yang lain. Ini mungkin mengapa panggilan-oleh-nilai sangat populer.

C ++ menambahkan kludge di atas kludge C untuk memberikan panggilan-oleh-referensi.

Sekarang, sebagai akibat langsung dari call-by-value vs call-by-reference vs call-by-pointer-kludge, C dan C ++ (programmer) mengalami sakit kepala yang mengerikan dengan pointer pointer dan pointer ke const (read-only) benda.

Ada yang berhasil menghindari seluruh mimpi buruk ini.

Ada tidak memiliki call-by-value secara eksplisit vs call-by-reference. Alih-alih, Ada memiliki parameter (yang dapat dibaca tetapi tidak ditulis), parameter keluar (yang HARUS ditulis sebelum dapat dibaca), dan parameter keluar, yang dapat dibaca dan ditulis dalam urutan apa pun. Compiler memutuskan apakah parameter tertentu dilewatkan oleh nilai atau dengan referensi: itu transparan untuk programmer.

John R. Strohm
sumber
1
+1. Ini adalah satu-satunya jawaban yang saya lihat di sini sejauh ini yang sebenarnya menjawab pertanyaan dan masuk akal dan tidak menggunakannya sebagai alasan transparan untuk kata-kata kasar pro-FP.
Mason Wheeler
11
+1 untuk "Bahasa yang belakangan disalin C, sebagian besar karena para desainer belum pernah melihat yang lain." Setiap kali saya melihat bahasa baru dengan konstanta oktal 0-awalan saya mati sedikit di dalam.
Libib
4
Ini bisa menjadi kalimat pertama dari Alkitab pemrograman: In the beginning, there was FORTRAN.
1
Berkenaan dengan ADA, jika variabel global diteruskan ke parameter masuk / keluar, apakah bahasa akan mencegah panggilan bersarang memeriksa atau memodifikasinya? Jika tidak, saya akan berpikir akan ada perbedaan yang jelas antara parameter in / out dan ref. Secara pribadi, saya ingin melihat bahasa secara eksplisit mendukung semua kombinasi "masuk / keluar / masuk + keluar" dan "berdasarkan nilai / dengan ref / tidak peduli", sehingga pemrogram dapat menawarkan kejelasan maksimum tentang maksud mereka tetapi pengoptimal akan memiliki fleksibilitas maksimum dalam implementasi [selama itu sesuai dengan maksud programmer].
supercat
2
@Neikos Ada juga fakta bahwa 0awalan tidak konsisten dengan setiap awalan sepuluh non-basis lainnya. Itu mungkin agak dimaafkan dalam C (dan sebagian besar bahasa yang kompatibel dengan sumber, misalnya C ++, Objective C), jika oktal adalah satu - satunya basis alternatif ketika diperkenalkan, tetapi ketika bahasa yang lebih modern menggunakan keduanya 0xdan 0dari awal, itu hanya membuat mereka terlihat buruk dipikirkan.
8bittree
13

Lulus dengan referensi memungkinkan efek samping yang sangat tidak disengaja yang sangat sulit untuk mustahil dilacak ketika mereka mulai menyebabkan perilaku yang tidak diinginkan.

Pass by value, terutama final, staticatau constparameter input membuat seluruh kelas bug ini menghilang.

Bahasa yang tidak dapat berubah bahkan lebih deterministik dan lebih mudah untuk berpikir tentang dan memahami apa yang sedang terjadi dan apa yang diharapkan keluar dari suatu fungsi.


sumber
7
Ini adalah salah satu hal yang Anda ketahui setelah mencoba men-debug kode VB 'kuno' di mana semuanya dengan referensi sebagai default.
jfrankcarr
Mungkin hanya karena latar belakang saya berbeda, tetapi saya tidak tahu apa jenis "efek samping yang sangat tidak disengaja yang sangat sulit untuk dilacak" yang Anda bicarakan. Saya sudah terbiasa dengan Pascal, di mana nilai-nilai adalah default tetapi dengan-referensi dapat digunakan dengan secara eksplisit menandai parameter, (pada dasarnya model yang sama dengan C #,) dan saya tidak pernah memiliki masalah yang menyebabkan masalah bagi saya. Saya bisa melihat bagaimana itu akan menjadi masalah di "VB kuno" di mana by-ref adalah default, tetapi ketika by-ref adalah opt-in, itu membuat Anda memikirkannya saat Anda sedang menulisnya.
Mason Wheeler
4
@MasonWheeler bagaimana dengan para pemula yang datang di belakang Anda dan salin apa yang Anda lakukan tanpa memahaminya dan mulai memanipulasi keadaan itu di semua tempat secara tidak langsung, dunia nyata terjadi setiap saat. Kurang tipuan adalah hal yang baik. Abadi adalah yang terbaik.
1
@MasonWheeler Contoh alias sederhana, seperti Swapperetasan yang tidak menggunakan variabel sementara, meskipun tidak cenderung menjadi masalah sendiri, menunjukkan betapa sulitnya contoh yang lebih rumit untuk melakukan debug.
Mark Hurd
1
@MasonWheeler: Contoh klasik dalam FORTRAN, yang mengarah pada pepatah "Variabel tidak akan; konstanta tidak", akan melewati konstanta floating-point seperti 3.0 ke fungsi yang memodifikasi parameter pass-in. Untuk konstanta floating-point yang dilewatkan ke suatu fungsi sistem akan membuat variabel "tersembunyi" yang diinisialisasi dengan nilai yang tepat dan dapat diteruskan ke fungsi. Jika fungsi ditambahkan 1,0 ke parameternya, subset sewenang-wenang dari 3.0 dalam program tiba-tiba bisa menjadi 4.0.
supercat
8

Mengapa begitu banyak bahasa yang lulus nilai?

Inti dari memecah program besar menjadi subrutin kecil adalah Anda dapat menggunakan subrutin secara mandiri. Referensi per langkah merusak properti ini. (Seperti halnya keadaan yang dapat diubah yang dibagikan.)

Bahkan bahasa di mana Anda memiliki manipulasi pointer eksplisit seperti C selalu dilewati oleh nilai (Anda dapat meneruskannya dengan referensi tetapi itu bukan perilaku default).

Sebenarnya, C selalu pass-by-value, tidak pernah pass-by-reference. Anda dapat mengambil alamat sesuatu dan meneruskan alamat itu, tetapi alamat tersebut tetap akan diteruskan berdasarkan nilai.

Apa manfaatnya ini, mengapa begitu banyak bahasa yang dilewatkan oleh nilai-nilai dan mengapa yang lain lulus dengan referensi ?

Ada dua alasan utama untuk menggunakan pass-by-reference:

  1. mensimulasikan beberapa nilai balik
  2. efisiensi

Secara pribadi, saya pikir # 1 adalah palsu: hampir selalu menjadi alasan untuk API buruk dan / atau desain bahasa:

  1. Jika Anda membutuhkan beberapa nilai pengembalian, jangan mensimulasikannya, cukup gunakan bahasa yang mendukungnya.
  2. Anda bisa juga mensimulasikan beberapa nilai pengembalian dengan mengemasnya menjadi beberapa struktur data ringan seperti tuple. Ini bekerja dengan baik jika bahasa mendukung pencocokan pola atau pengikatan ikatan. Misalnya Ruby:

    def foo
      # This is actually just a single return value, an array: [1, 2, 3]
      return 1, 2, 3
    end
    
    # Ruby supports destructuring bind for arrays: a, b, c = [1, 2, 3]
    one, two, three = foo
    
  3. Seringkali, Anda bahkan tidak memerlukan banyak nilai pengembalian. Misalnya, satu pola populer adalah bahwa subrutin mengembalikan kode kesalahan dan hasil aktual ditulis kembali dengan referensi. Sebagai gantinya, Anda hanya perlu melempar pengecualian jika kesalahannya tidak terduga atau mengembalikan Either<Exception, T>jika kesalahan tersebut diharapkan. Pola lain adalah mengembalikan boolean yang memberi tahu apakah operasi berhasil dan mengembalikan hasil aktual dengan referensi. Sekali lagi, jika kegagalannya tidak terduga, Anda harus melempar pengecualian, jika kegagalan diharapkan, misalnya ketika mencari nilai dalam kamus, Anda harus mengembalikannya Maybe<T>.

Pass-by-reference mungkin lebih efisien daripada pass-by-value, karena Anda tidak perlu menyalin nilainya.

(Saya mengerti Haskell disahkan oleh referensi meskipun saya tidak yakin).

Tidak, Haskell bukan pass-by-reference. Juga bukan nilai per nilai. Pass-by-reference dan pass-by-value adalah strategi evaluasi yang ketat, tetapi Haskell tidak-ketat.

Bahkan, Haskell Keterangan tidak menentukan apa strategi evaluasi tertentu. Sebagian besar implementasi Hakell menggunakan campuran panggilan-dengan-nama dan panggilan-dengan-kebutuhan (varian panggilan-dengan-nama dengan memoisasi), tetapi standar tidak mengamanatkan ini.

Perhatikan bahwa bahkan untuk bahasa yang ketat, tidak masuk akal untuk membedakan antara pass-by-reference atau pass-by-value untuk bahasa fungsional karena perbedaan antara mereka hanya dapat diamati jika Anda bermutasi referensi. Oleh karena itu, implementor bebas memilih di antara keduanya, tanpa melanggar semantik bahasa.

Jörg W Mittag
sumber
9
"Jika Anda membutuhkan beberapa nilai pengembalian, jangan mensimulasikannya, cukup gunakan bahasa yang mendukungnya." Ini sedikit aneh untuk dikatakan. Itu pada dasarnya mengatakan "jika bahasa Anda dapat melakukan semua yang Anda butuhkan, tetapi satu fitur yang mungkin Anda butuhkan dalam kurang dari 1% dari kode Anda - tetapi Anda masih akan membutuhkannya untuk itu 1% - tidak dapat dilakukan dalam cara yang sangat bersih, maka bahasa Anda tidak cukup baik untuk proyek Anda dan Anda harus menulis ulang semuanya dalam bahasa lain. " Maaf, tapi itu benar-benar konyol.
Mason Wheeler
+1 Saya sangat setuju dengan poin ini. Maksud dari memecah program besar menjadi subrutin kecil adalah Anda dapat beralasan tentang subrutin secara independen. Referensi per langkah merusak properti ini. (Seperti halnya berbagi keadaan yang bisa berubah.)
Rémi
3

Ada perilaku yang berbeda tergantung pada model panggilan bahasa, jenis argumen dan model memori bahasa.

Dengan tipe asli sederhana, lewat nilai memungkinkan Anda untuk melewati nilai oleh register. Itu bisa sangat cepat karena nilainya tidak perlu dimuat dari memori juga tidak menyimpan kembali. Optimalisasi serupa juga dimungkinkan hanya dengan menggunakan kembali memori yang digunakan oleh argumen begitu callee selesai dengan itu, tanpa risiko mengacaukan salinan penelepon objek. Jika argumennya adalah objek sementara, Anda mungkin akan menyimpan salinan lengkap melakukannya (C ++ 11 membuat jenis optimasi ini lebih jelas dengan referensi-kanan baru dan semantik langkahnya).

Dalam banyak bahasa OO (C ++ lebih merupakan pengecualian dalam konteks ini), Anda tidak bisa melewati objek dengan nilai. Anda dipaksa untuk melewatinya dengan referensi. Ini membuat kode polimorfik secara default dan lebih sejalan dengan gagasan instance yang tepat untuk OO. Juga, jika Anda ingin memberikan nilai, Anda harus membuat salinan sendiri, mengakui biaya kinerja yang menghasilkan tindakan tersebut. Dalam hal ini, bahasa yang dipilih untuk Anda adalah pendekatan yang lebih mungkin memberi Anda kinerja terbaik.

Dalam hal bahasa fungsional, saya kira memberikan nilai atau referensi hanyalah masalah optimasi. Karena fungsi dalam bahasa seperti itu murni dan bebas dari efek samping, sebenarnya tidak ada alasan untuk menyalin nilai kecuali untuk kecepatan. Saya bahkan cukup yakin bahwa bahasa seperti itu sering berbagi salinan objek yang sama dengan nilai yang sama, kemungkinan hanya tersedia jika Anda menggunakan semantic referensi lewat (konst). Python juga menggunakan trik ini untuk string integer dan umum (seperti metode dan nama kelas), menjelaskan mengapa integer dan string adalah objek konstan dalam Python. Ini juga membantu untuk mengoptimalkan kode lagi, dengan memungkinkan misalnya perbandingan pointer, bukan perbandingan konten, dan melakukan evaluasi malas beberapa data internal.

Fabien Ninoles
sumber
2

Anda dapat memberikan ekspresi berdasarkan nilai, itu wajar. Melewati ekspresi dengan referensi (sementara) adalah ... aneh.

herba
sumber
1
Juga, menyampaikan ekspresi dengan referensi sementara dapat menyebabkan bug buruk (dalam bahasa stateful), ketika Anda dengan senang hati mengubahnya (karena itu hanya sementara), tetapi kemudian menjadi bumerang ketika Anda benar-benar melewati variabel, dan Anda harus memikirkan solusi jelek seperti melewati foo +0 bukan foo.
herby
2

Jika Anda melewati referensi, maka Anda secara efektif selalu bekerja dengan nilai-nilai global, dengan semua masalah global (yaitu cakupan dan efek samping yang tidak diinginkan).

Referensi, seperti halnya global, terkadang bermanfaat, tetapi seharusnya tidak menjadi pilihan pertama Anda.

jmoreno
sumber
3
Saya berasumsi Anda maksud lewat referensi dalam kalimat pertama?
jk.