Jika null buruk, mengapa bahasa modern menerapkannya? [Tutup]

82

Saya yakin perancang bahasa seperti Java atau C # tahu masalah yang terkait dengan keberadaan referensi nol (lihat Apakah referensi nol benar-benar buruk? ). Juga menerapkan jenis opsi tidak benar-benar jauh lebih kompleks daripada referensi nol.

Mengapa mereka memutuskan untuk memasukkannya? Saya yakin kurangnya referensi nol akan mendorong (atau bahkan memaksa) kode kualitas yang lebih baik (terutama desain perpustakaan yang lebih baik) baik dari pembuat bahasa dan pengguna.

Apakah itu hanya karena konservatisme - "bahasa lain memilikinya, kita harus memilikinya juga ..."?

mrpyo
sumber
99
null itu bagus. Saya menyukainya dan menggunakannya setiap hari.
Pieter B
17
@PieterB Tapi apakah Anda menggunakannya untuk sebagian besar referensi, atau Anda ingin sebagian besar referensi tidak menjadi nol? Argumennya bukanlah bahwa seharusnya tidak ada data yang dapat dibatalkan, hanya saja data itu harus eksplisit dan ikut serta.
11
@PieterB Tetapi ketika mayoritas tidak boleh nullable, tidakkah masuk akal untuk membuat null-kemampuan pengecualian daripada default? Perhatikan bahwa sementara desain jenis opsi yang umum adalah untuk memaksa pemeriksaan eksplisit untuk ketidakhadiran dan membongkar, orang juga dapat memiliki semantik Java / C # / ... yang terkenal untuk opt-in referensi nullable (gunakan seolah-olah tidak nullable, meledak jika nol). Setidaknya itu akan mencegah beberapa bug, dan membuat analisis statis yang mengeluhkan tidak adanya pemeriksaan nol yang jauh lebih praktis.
20
WTF dengan kalian? Dari semua hal yang dapat dan memang salah dengan perangkat lunak, mencoba untuk mengganti nol tidak ada masalah sama sekali. Ini SELALU menghasilkan AV / segfault dan diperbaiki. Apakah ada begitu banyak kekurangan bug sehingga Anda harus khawatir tentang ini? Jika demikian, saya punya banyak cadangan, dan tidak satu pun dari mereka yang mengajukan masalah dengan referensi / pointer nol.
Martin James
13
@ MartinJames "Ini SELALU menghasilkan AV / segfault dan diperbaiki" - tidak, tidak.
Detly

Jawaban:

97

Penafian: Karena saya tidak mengenal perancang bahasa secara pribadi, jawaban apa pun yang saya berikan kepada Anda akan spekulatif.

Dari Tony Hoare sendiri:

Saya menyebutnya kesalahan miliaran dolar saya. Itu adalah penemuan referensi nol pada tahun 1965. Pada waktu itu, saya sedang merancang sistem tipe komprehensif pertama untuk referensi dalam bahasa berorientasi objek (ALGOL W). Tujuan saya adalah untuk memastikan bahwa semua penggunaan referensi harus benar-benar aman, dengan pengecekan dilakukan secara otomatis oleh kompiler. Tapi saya tidak bisa menahan godaan untuk memasukkan referensi nol, hanya karena itu sangat mudah diimplementasikan. Ini telah menyebabkan kesalahan yang tak terhitung banyaknya, kerentanan, dan crash sistem, yang mungkin telah menyebabkan satu miliar dolar rasa sakit dan kerusakan dalam empat puluh tahun terakhir.

Tekankan milikku.

Tentu saja itu bukan ide yang buruk baginya saat itu. Kemungkinan itu telah diabadikan sebagian karena alasan yang sama - jika sepertinya ide yang bagus untuk penemu quicksort pemenang Penghargaan Turing, tidak mengherankan bahwa banyak orang masih tidak mengerti mengapa itu jahat. Mungkin juga sebagian karena bahasa baru yang digunakan mirip dengan bahasa yang lebih lama, baik karena alasan pemasaran dan kurva pembelajaran. Inti masalah:

"Kami mengejar programmer C ++. Kami berhasil menyeret banyak dari mereka sekitar setengah jalan ke Lisp." -Guy Steele, penulis bersama spec Java

(Sumber: http://www.paulgraham.com/icad.html )

Dan, tentu saja, C ++ memiliki nol karena C memiliki nol, dan tidak perlu masuk ke dampak historis C. Jenis C # menggantikan J ++, yang merupakan implementasi Microsoft dari Java, dan itu juga menggantikan C ++ sebagai bahasa pilihan untuk pengembangan Windows, jadi itu bisa didapat dari salah satunya.

Sunting Berikut ini kutipan lain dari Hoare yang patut dipertimbangkan:

Memprogram bahasa secara keseluruhan jauh lebih rumit daripada sebelumnya: orientasi objek, warisan, dan fitur-fitur lainnya masih belum benar-benar dipikirkan dari sudut pandang disiplin yang koheren dan berbasis ilmiah atau teori kebenaran. . Postulat orisinal saya, yang telah saya kejar sebagai ilmuwan sepanjang hidup saya, adalah bahwa seseorang menggunakan kriteria kebenaran sebagai alat untuk menyatu dengan desain bahasa pemrograman yang baik — yang tidak mengatur jebakan untuk penggunanya, dan yang di yang komponen-komponen program yang berbeda sesuai dengan komponen-komponen spesifik spesifikasinya, sehingga Anda dapat mempertimbangkannya secara komposisional. [...] Alat, termasuk kompiler, harus didasarkan pada beberapa teori tentang apa artinya menulis program yang benar. -Wawancara sejarah hewan oleh Philip L. Frana, 17 Juli 2002, Cambridge, Inggris; Institut Charles Babbage, Universitas Minnesota. [ Http://www.cbi.umn.edu/oh/display.phtml?id=343]

Sekali lagi, tekankan saya. Sun / Oracle dan Microsoft adalah perusahaan, dan garis bawah perusahaan mana pun adalah uang. Manfaat bagi mereka karena nullmungkin lebih besar daripada kontra, atau mereka mungkin memiliki tenggat waktu yang terlalu ketat untuk sepenuhnya mempertimbangkan masalah tersebut. Sebagai contoh kesalahan bahasa yang berbeda yang mungkin terjadi karena tenggat waktu:

Sayang sekali Cloneable rusak, tetapi itu terjadi. API Java asli dilakukan dengan sangat cepat di bawah tenggat waktu yang ketat untuk memenuhi jendela pasar penutupan. Tim Java asli melakukan pekerjaan yang luar biasa, tetapi tidak semua API sempurna. Cloneable adalah titik lemah, dan saya pikir orang harus menyadari keterbatasannya. -Yosh Bloch

(Sumber: http://www.artima.com/intv/bloch13.html )

Doval
sumber
32
Dear downvoter: bagaimana saya bisa meningkatkan jawaban saya?
Doval
6
Anda sebenarnya tidak menjawab pertanyaan itu; Anda hanya memberikan beberapa kutipan tentang pendapat setelah fakta dan beberapa melambaikan tangan tentang "biaya". (Jika nol adalah kesalahan miliar dolar, bukankah seharusnya dolar disimpan oleh MS dan Java dengan menerapkannya mengurangi utang itu?)
DougM
29
@DougM Apa yang Anda harapkan dari saya, temui setiap perancang bahasa dari 50 tahun terakhir dan tanyakan mengapa dia menerapkannya nulldalam bahasanya? Setiap jawaban untuk pertanyaan ini akan bersifat spekulatif kecuali berasal dari perancang bahasa. Saya tidak tahu ada yang sesering ini di situs ini selain Eric Lippert. Bagian terakhir adalah herring merah karena berbagai alasan. Jumlah kode pihak ke-3 yang ditulis di atas MS dan API Java jelas melebihi jumlah kode dalam API itu sendiri. Jadi, jika pelanggan Anda menginginkannya null, Anda memberi mereka null. Anda juga mengira mereka telah menerima nullbiaya mereka uang.
Doval
3
Jika satu-satunya jawaban yang dapat Anda berikan adalah spekulatif, sebutkan dengan jelas di paragraf pembuka Anda. (Anda bertanya bagaimana Anda dapat meningkatkan jawaban Anda, dan saya menjawab. Setiap tanda kurung hanyalah komentar yang dapat Anda abaikan; itu adalah tanda kurung untuk bahasa Inggris.)
DougM
7
Jawaban ini masuk akal; Saya sudah menambahkan beberapa pertimbangan di tambang. Saya perhatikan bahwa ICloneablejuga rusak di. NET; sayangnya ini adalah satu tempat di mana kekurangan Jawa tidak dipelajari dari waktu ke waktu.
Eric Lippert
121

Saya yakin perancang bahasa seperti Java atau C # tahu masalah terkait dengan keberadaan referensi nol

Tentu saja.

Juga menerapkan jenis opsi tidak benar-benar jauh lebih kompleks daripada referensi nol.

Saya mohon untuk berbeda! Pertimbangan desain yang masuk ke tipe nilai nullable di C # 2 adalah kompleks, kontroversial dan sulit. Mereka mengambil tim desain dari kedua bahasa dan runtime berbulan-bulan debat, implementasi prototipe, dan sebagainya, dan pada kenyataannya semantik tinju nullable diubah sangat sangat dekat dengan pengiriman C # 2.0, yang sangat kontroversial.

Mengapa mereka memutuskan untuk memasukkannya?

Semua desain adalah proses memilih di antara banyak tujuan yang tidak kompatibel secara halus dan sangat; Saya hanya bisa memberikan sketsa singkat dari beberapa faktor yang akan dipertimbangkan:

  • Fitur ortogonalitas bahasa umumnya dianggap sebagai hal yang baik. C # memiliki tipe nilai nullable, tipe nilai non-nullable, dan tipe referensi nullable. Tipe referensi yang tidak dapat dibatalkan tidak ada, yang membuat sistem tipe tidak orthogonal.

  • Keakraban dengan pengguna C, C ++ dan Java yang ada adalah penting.

  • Interoperabilitas yang mudah dengan COM adalah penting.

  • Interoperabilitas yang mudah dengan semua bahasa .NET lainnya adalah penting.

  • Interoperabilitas yang mudah dengan database adalah penting.

  • Konsistensi semantik penting; jika kita memiliki referensi TheKingOfFrance sama dengan nol apakah itu selalu berarti "tidak ada Raja Prancis saat ini", atau dapatkah itu juga berarti "Jelas ada Raja Prancis; Aku hanya tidak tahu siapa itu sekarang"? atau dapatkah itu berarti "gagasan memiliki seorang Raja di Prancis itu tidak masuk akal, jadi jangan bertanya!"? Null dapat berarti semua hal ini dan lebih banyak lagi dalam C #, dan semua konsep ini berguna.

  • Biaya kinerja penting.

  • Menjadi setuju dengan analisis statis adalah penting.

  • Konsistensi sistem tipe adalah penting; dapatkah kita selalu tahu bahwa referensi yang tidak dapat dibatalkan tidak pernah dalam keadaan apapun dianggap tidak valid? Bagaimana dengan konstruktor objek dengan bidang tipe referensi yang tidak dapat dibatalkan? Bagaimana dengan di finalizer dari objek seperti itu, di mana objek tersebut diselesaikan karena kode yang seharusnya mengisi referensi melemparkan pengecualian ? Jenis sistem yang berbohong kepada Anda tentang jaminannya berbahaya.

  • Dan bagaimana dengan konsistensi semantik? Null nilai merambat ketika digunakan, tapi nol referensi membuang pengecualian bila digunakan. Itu tidak konsisten; Apakah inkonsistensi itu dibenarkan oleh sejumlah manfaat?

  • Bisakah kita mengimplementasikan fitur tanpa merusak fitur lain? Apa fitur lain yang mungkin ada di masa depan yang tidak termasuk dalam fitur ini?

  • Anda pergi berperang dengan tentara yang Anda miliki, bukan yang Anda inginkan. Ingat, C # 1.0 tidak memiliki obat generik, jadi berbicara Maybe<T>sebagai alternatif adalah non-starter lengkap. Haruskah .NET tergelincir selama dua tahun sementara tim runtime menambahkan obat generik, semata-mata untuk menghilangkan referensi nol?

  • Bagaimana dengan konsistensi sistem tipe? Anda dapat mengatakan Nullable<T>untuk semua tipe nilai - tidak, tunggu, itu bohong. Anda tidak bisa mengatakannya Nullable<Nullable<T>>. Haruskah kamu bisa? Jika demikian, apa yang diinginkan semantiknya? Apakah bermanfaat membuat seluruh sistem jenis memiliki kasus khusus di dalamnya hanya untuk fitur ini?

Dan seterusnya. Keputusan-keputusan ini rumit.

Eric Lippert
sumber
12
+1 untuk semuanya tetapi terutama memunculkan obat generik. Sangat mudah untuk melupakan ada periode waktu di Jawa dan sejarah C # di mana obat generik tidak ada.
Doval
2
Mungkin pertanyaan bodoh (saya hanya sarjana IT) - tetapi tidak bisa jenis opsi diimplementasikan pada tingkat sintaksis (dengan CLR tidak tahu apa-apa tentang itu) sebagai referensi nullable reguler yang memerlukan pemeriksaan "memiliki nilai" sebelum menggunakan di kode? Saya percaya jenis opsi tidak memerlukan pemeriksaan saat runtime.
mrpyo
2
@ mrpyo: Tentu, itu pilihan implementasi yang mungkin. Tidak ada pilihan desain lain yang hilang, dan pilihan implementasi memiliki banyak pro dan kontra sendiri.
Eric Lippert
1
@ mrpyo Saya pikir memaksa cek "memiliki nilai" bukanlah ide yang baik. Secara teoritis itu adalah ide yang sangat bagus, tetapi dalam praktiknya, IMO akan membawa semua jenis cek kosong, hanya untuk memenuhi pengecualian seperti compiler di Jawa dan orang-orang membodohinya dengan catchesyang tidak melakukan apa-apa. Saya pikir lebih baik membiarkan sistem meledak daripada melanjutkan operasi dalam kondisi yang mungkin tidak valid.
NothingsImpossible
2
@ voo: Array tipe referensi yang tidak dapat dibatalkan sulit karena banyak alasan. Ada banyak solusi yang mungkin dan semuanya membebankan biaya pada operasi yang berbeda. Saran Supercat adalah untuk melacak apakah suatu elemen dapat dibaca secara legal sebelum ditugaskan, yang membebankan biaya. Milik Anda adalah untuk memastikan bahwa penginisialisasi berjalan pada setiap elemen sebelum array terlihat, yang membebankan serangkaian biaya yang berbeda. Jadi, inilah intinya: tidak peduli teknik mana yang dipilih, seseorang akan mengeluh bahwa itu tidak efisien untuk skenario hewan peliharaan mereka . Ini adalah poin serius terhadap fitur tersebut.
Eric Lippert
28

Null memiliki tujuan yang sangat valid untuk mewakili kurangnya nilai.

Saya akan mengatakan saya adalah orang yang paling vokal yang saya tahu tentang penyalahgunaan nol dan semua sakit kepala dan penderitaan yang dapat mereka sebabkan terutama ketika digunakan secara bebas.

Sikap pribadi saya adalah orang dapat menggunakan nulls hanya ketika mereka dapat membenarkan itu perlu dan sesuai.

Contoh justifikasi nol:

Tanggal Kematian biasanya adalah bidang yang tidak dapat dibatalkan. Ada tiga kemungkinan situasi dengan tanggal kematian. Entah orang itu telah meninggal dan tanggalnya diketahui, orang itu telah meninggal dan tanggalnya tidak diketahui, atau orang itu tidak mati dan karenanya tanggal kematian tidak ada.

Date of Death juga merupakan bidang DateTime dan tidak memiliki nilai "tidak dikenal" atau "kosong". Itu memang memiliki tanggal default yang muncul ketika Anda membuat datetime baru yang bervariasi berdasarkan bahasa yang digunakan, tetapi secara teknis ada kemungkinan orang itu benar-benar mati pada waktu itu dan akan ditandai sebagai "nilai kosong" Anda jika Anda ingin gunakan tanggal default.

Data perlu mewakili situasi dengan benar.

Orang meninggal Tanggal kematian diketahui (3/9/1984)

Sederhana, '3/9/1984'

Orang meninggal Tanggal kematian tidak diketahui

Jadi apa yang terbaik? Null , '0/0/0000', atau '01 / 01/1869 '(atau apa pun nilai default Anda?)

Orang tidak mati, tanggal kematian tidak berlaku

Jadi apa yang terbaik? Null , '0/0/0000', atau '01 / 01/1869 '(atau apa pun nilai default Anda?)

Jadi mari kita pikirkan setiap nilai lebih dari ...

  • Null , ia memiliki implikasi dan kekhawatiran yang perlu Anda waspadai, tanpa sengaja mencoba memanipulasi tanpa memastikan itu bukan nol dulu misalnya akan melempar pengecualian, tetapi juga terbaik mewakili situasi aktual ... Jika orang tersebut tidak mati tanggal kematian tidak ada ... tidak ada ... itu nol ...
  • 0/0/0000 , ini bisa oke dalam beberapa bahasa, dan bahkan bisa menjadi representasi yang sesuai dari tanggal. Sayangnya beberapa bahasa dan validasi akan menolak ini sebagai waktu yang tidak valid yang menjadikannya tidak masuk dalam banyak kasus.
  • 1/1/1869 (atau apa pun nilai datetime default Anda) , masalahnya adalah sulit untuk ditangani. Anda bisa menggunakannya sebagai nilai nilai yang kurang, kecuali apa yang terjadi jika saya ingin memfilter semua catatan saya, saya tidak memiliki tanggal kematian? Saya dapat dengan mudah menyaring orang yang benar-benar mati pada tanggal tersebut yang dapat menyebabkan masalah integritas data.

Faktanya kadang-kadang Anda Apakah perlu untuk mewakili apa-apa dan yakin kadang-kadang jenis variabel bekerja dengan baik untuk itu, tapi jenis sering variabel harus mampu mewakili apa-apa.

Jika saya tidak memiliki apel, saya memiliki 0 apel, tetapi bagaimana jika saya tidak tahu berapa banyak apel yang saya miliki?

Bagaimanapun, null dilecehkan dan berpotensi berbahaya, tetapi kadang-kadang diperlukan. Ini hanya default dalam banyak kasus karena sampai saya memberikan nilai, kurangnya nilai dan sesuatu yang perlu untuk mewakilinya. (Batal)

RualStorge
sumber
37
Null serves a very valid purpose of representing a lack of value.Suatu tipe Optionatau Maybemelayani tujuan yang sangat valid ini tanpa melewati sistem tipe.
Doval
34
Tidak ada yang berpendapat bahwa seharusnya tidak ada nilai yang kurang, mereka berpendapat bahwa nilai yang mungkin hilang harus secara eksplisit ditandai seperti itu, daripada setiap nilai yang berpotensi hilang.
2
Saya kira RualStorge berbicara dalam kaitannya dengan SQL, karena ada kamp-kamp yang menyatakan setiap kolom harus ditandai sebagai NOT NULL. Pertanyaan saya tidak terkait dengan RDBMS ...
mrpyo
5
+1 untuk membedakan antara "tidak ada nilai" dan "nilai tidak diketahui"
David
2
Tidakkah lebih masuk akal untuk memisahkan keadaan seseorang? Yaitu Persontipe memiliki statebidang tipe State, yang merupakan serikat terdiskriminasi Alivedan Dead(dateOfDeath : Date).
jon-hanson
10

Saya tidak akan pergi sejauh "bahasa lain memilikinya, kita harus memilikinya juga ..." seperti itu semacam mengikuti keluarga Jones. Fitur utama dari setiap bahasa baru adalah kemampuan untuk beroperasi dengan perpustakaan yang ada dalam bahasa lain (baca: C). Karena C memiliki null pointer, lapisan interoperabilitas tentu membutuhkan konsep nol (atau setara lainnya "tidak ada" yang meledak ketika Anda menggunakannya).

Perancang bahasa bisa memilih untuk menggunakan Jenis Opsi dan memaksa Anda untuk menangani jalur nol di mana - mana hal itu bisa menjadi nol. Dan itu hampir pasti akan menyebabkan lebih sedikit bug.

Tetapi (terutama untuk Java dan C # karena waktu pengenalan mereka dan audiens target mereka) menggunakan jenis opsi untuk lapisan interoperabilitas ini mungkin akan membahayakan jika tidak menghentikan adopsi mereka. Entah jenis opsi diteruskan, mengganggu programer C ++ dari pertengahan hingga akhir 90-an - atau lapisan interoperabilitas akan melempar pengecualian ketika menemukan nol, mengganggu keluar dari programmer C ++ dari pertengahan hingga akhir 90-an. ..

Telastyn
sumber
3
Paragraf pertama tidak masuk akal bagi saya. Java tidak memiliki C interop dalam bentuk yang Anda sarankan (ada JNI tetapi sudah melompati selusin lingkaran untuk segala sesuatu yang berkaitan dengan referensi; ditambah itu jarang digunakan dalam praktiknya), sama untuk bahasa "modern" lainnya.
@delnan - maaf, saya lebih akrab dengan C #, yang memang memiliki interop semacam ini. Saya agak berasumsi bahwa banyak perpustakaan Java dasar menggunakan JNI di bagian bawah juga.
Telastyn
6
Anda membuat argumen yang bagus untuk mengizinkan nol, tetapi Anda tetap bisa mengizinkan nol tanpa mendorongnya . Scala adalah contoh yang bagus untuk ini. Ini dapat beroperasi tanpa hambatan dengan Java apis yang menggunakan null, tetapi Anda dianjurkan untuk membungkusnya dalam Optionuntuk digunakan dalam Scala, yang semudah itu val x = Option(possiblyNullReference). Dalam prakteknya, itu tidak butuh waktu lama bagi orang untuk melihat manfaat dari Option.
Karl Bielefeldt
1
Jenis opsi berjalan seiring dengan pencocokan pola (diverifikasi secara statis), yang sayangnya tidak dimiliki oleh C #. F # melakukannya, dan itu luar biasa.
Steven Evers
1
@SteveEvers Dimungkinkan untuk memalsunya menggunakan kelas dasar abstrak dengan konstruktor pribadi, kelas dalam tertutup, dan Matchmetode yang menggunakan delegasi sebagai argumen. Kemudian Anda meneruskan ekspresi lambda ke Match(poin bonus untuk menggunakan argumen bernama) dan Matchmemanggil yang benar.
Doval
7

Pertama-tama, saya pikir kita semua bisa sepakat bahwa konsep pembatalan diperlukan. Ada beberapa situasi di mana kita perlu mewakili ketiadaan informasi.

Mengizinkan nullreferensi (dan petunjuk) hanya satu implementasi dari konsep ini, dan mungkin yang paling populer meskipun diketahui memiliki masalah: C, Java, Python, Ruby, PHP, JavaScript, ... semua menggunakan yang serupa null.

Mengapa Nah, apa alternatifnya?

Dalam bahasa fungsional seperti Haskell Anda memiliki Optionatau Maybeketik; namun itu dibangun di atas:

  • tipe parametrik
  • tipe data aljabar

Sekarang, apakah C, Java, Python, Ruby, atau PHP yang asli mendukung salah satu fitur itu? Tidak. Generik Java yang cacat baru - baru ini dalam sejarah bahasa dan entah bagaimana saya ragu yang lain bahkan mengimplementasikannya sama sekali.

Itu dia. nullitu mudah, tipe data aljabar parametrik lebih sulit. Orang-orang mencari alternatif paling sederhana.

Matthieu M.
sumber
+1 untuk "null itu mudah, tipe data aljabar parametrik lebih sulit." Tapi saya pikir itu bukan masalah mengetik parametrik dan ADT menjadi lebih sulit; hanya saja mereka tidak dianggap perlu. Jika Java dikirim tanpa sistem objek, di sisi lain, itu akan gagal; OOP adalah fitur "showstopping", dalam hal itu jika Anda tidak memilikinya, tidak ada yang tertarik.
Doval
@Doval: yah, OOP mungkin diperlukan untuk Java, tapi itu bukan untuk C :) Tapi memang benar bahwa Java bertujuan untuk menjadi sederhana. Sayangnya orang-orang tampaknya menganggap bahwa bahasa yang sederhana mengarah ke program-program sederhana, yang agak aneh (Brainfuck adalah bahasa yang sangat sederhana ...), tetapi kami tentu setuju bahwa bahasa yang rumit (C ++ ...) bukanlah obat mujarab meskipun mereka bisa sangat berguna.
Matthieu M.
1
@ MatthieuM .: Sistem nyata sangat kompleks. Bahasa yang dirancang dengan baik yang kerumitannya cocok dengan sistem dunia nyata yang dimodelkan dapat memungkinkan sistem yang kompleks dimodelkan dengan kode sederhana. Upaya menyederhanakan bahasa hanya dengan mendorong kompleksitas ke programmer yang menggunakannya.
supercat
@supercat: Saya sangat setuju. Atau seperti Einstein diparafrasekan: "Jadikan semuanya sesederhana mungkin, tetapi tidak sesederhana itu."
Matthieu M.
@ MatthieuM .: Einstein bijak dalam banyak hal. Bahasa yang mencoba untuk menganggap "semuanya adalah objek, referensi yang dapat disimpan dalam Object" gagal untuk mengenali bahwa aplikasi praktis membutuhkan objek yang bisa dibagikan yang tidak dibagi dan objek yang dapat diubah yang dapat dibagi (keduanya harus berperilaku seperti nilai), serta dapat dibagi dan tidak dapat dibagi entitas. Menggunakan satu Objecttipe untuk semuanya tidak menghilangkan kebutuhan untuk perbedaan seperti itu; itu hanya membuatnya lebih sulit untuk menggunakannya dengan benar.
supercat
5

Tidak ada / tidak ada itu sendiri tidak jahat.

Jika Anda menyaksikan pidachnya yang terkenal menyesatkan bernama "The Billion dollar Kesalahan", Tony Hoare berbicara tentang bagaimana membiarkan variabel apa pun untuk dapat menahan nol adalah kesalahan besar. Alternatif - menggunakan Opsi - sebenarnya tidak menghilangkan referensi nol. Alih-alih itu memungkinkan Anda menentukan variabel mana yang diizinkan untuk menahan nol, dan mana yang tidak.

Sebenarnya, dengan bahasa modern yang menerapkan penanganan pengecualian yang tepat, kesalahan null dereference tidak berbeda dari pengecualian lain - Anda menemukannya, Anda memperbaikinya. Beberapa alternatif untuk referensi nol (pola Objek Null misalnya) menyembunyikan kesalahan, menyebabkan semuanya gagal diam-diam sampai jauh kemudian. Menurut pendapat saya, lebih baik gagal cepat .

Jadi pertanyaannya adalah, mengapa bahasa gagal mengimplementasikan Opsi? Sebagai soal fakta, bahasa C ++ yang bisa dibilang paling populer sepanjang masa memiliki kemampuan untuk mendefinisikan variabel objek yang tidak dapat ditugaskan NULL. Ini adalah solusi untuk "masalah nol" Tony Hoare yang disebutkan dalam pidatonya. Mengapa bahasa yang diketik paling populer berikutnya, Java, tidak memilikinya? Orang mungkin bertanya mengapa ia memiliki begitu banyak kekurangan secara umum, terutama dalam sistem tipenya. Saya tidak berpikir Anda dapat benar-benar mengatakan bahwa bahasa secara sistematis membuat kesalahan ini. Ada yang melakukannya, ada yang tidak.

BT
sumber
1
Salah satu kekuatan terbesar Jawa dari perspektif implementasi, tetapi kelemahan dari perspektif bahasa, adalah bahwa hanya ada satu tipe non-primitif: Referensi Obyek Promiscuous. Ini sangat menyederhanakan runtime, memungkinkan beberapa implementasi JVM sangat ringan. Desain itu, bagaimanapun, berarti bahwa setiap jenis harus memiliki nilai default, dan untuk Referensi Obyek Promiscuous, satu-satunya standar yang mungkin adalah null.
supercat
Nah, satu jenis root non-primitif pada tingkat apa pun. Mengapa ini merupakan kelemahan dari perspektif bahasa? Saya tidak mengerti mengapa fakta ini mengharuskan setiap jenis memiliki nilai default (atau sebaliknya, mengapa beberapa jenis root akan memungkinkan jenis tidak memiliki nilai default), atau mengapa itu merupakan kelemahan.
BT
Jenis non-primitif apa lagi yang bisa dimiliki oleh suatu bidang atau elemen array? Kelemahannya adalah bahwa beberapa referensi digunakan untuk merangkum identitas, dan beberapa untuk merangkum nilai-nilai yang terkandung dalam objek yang diidentifikasi dengan demikian. Untuk variabel tipe referensi yang digunakan untuk merangkum identitas, nulladalah satu-satunya standar yang masuk akal. Referensi yang digunakan untuk merangkum nilai, bagaimanapun, dapat memiliki perilaku default yang masuk akal dalam kasus di mana suatu tipe akan memiliki atau dapat membangun instance default yang masuk akal. Banyak aspek tentang bagaimana referensi harus berperilaku tergantung pada apakah dan bagaimana mereka merangkum nilai, tetapi ...
supercat
... sistem tipe Java tidak memiliki cara untuk mengekspresikannya. Jika foomemegang satu-satunya referensi untuk sebuah int[]berisi {1,2,3}dan kode ingin foomenyimpan referensi ke sebuah int[]berisi {2,2,3}, cara tercepat untuk mencapai itu adalah dengan peningkatan foo[0]. Jika kode ingin membiarkan suatu metode tahu bahwa metode itu fooberlaku {1,2,3}, metode lain tidak akan mengubah array atau mempertahankan referensi di luar titik di mana Anda fooingin memodifikasinya, cara tercepat untuk mencapai itu adalah dengan mengirimkan referensi ke array. Jika Java memiliki tipe "referensi baca-saja yang sementara", maka ...
supercat
... array dapat dikirimkan dengan aman sebagai referensi sementara, dan metode yang ingin mempertahankan nilainya akan tahu bahwa ia perlu menyalinnya. Dengan tidak adanya tipe seperti itu, satu-satunya cara untuk mengekspos konten array dengan aman adalah dengan membuat salinannya atau merangkumnya dalam objek yang dibuat hanya untuk tujuan itu.
supercat
4

Karena bahasa pemrograman pada umumnya dirancang agar praktis bermanfaat daripada benar secara teknis. Faktanya adalah bahwa nullkeadaan adalah kejadian umum karena data yang buruk atau hilang atau keadaan yang belum diputuskan. Solusi yang secara teknis unggul semuanya lebih sulit daripada hanya membiarkan keadaan nol dan menyedot fakta bahwa programmer membuat kesalahan.

Misalnya, jika saya ingin menulis skrip sederhana yang berfungsi dengan file, saya bisa menulis pseudocode seperti:

file = openfile("joebloggs.txt")

for line in file
{
  print(line)
}

dan itu hanya akan gagal jika joebloggs.txt tidak ada. Masalahnya adalah, untuk skrip sederhana yang mungkin oke dan untuk banyak situasi dalam kode yang lebih kompleks saya tahu itu ada dan kegagalan tidak akan terjadi sehingga memaksa saya untuk memeriksa buang-buang waktu saya. Alternatif yang lebih aman mencapai keselamatan mereka dengan memaksa saya untuk berurusan dengan keadaan potensi kegagalan dengan benar, tetapi seringkali saya tidak ingin melakukan itu, saya hanya ingin melanjutkan.

Jack Aidley
sumber
13
Dan di sini Anda memberi contoh tentang apa yang sebenarnya salah dengan null. Fungsi "openfile" yang diterapkan dengan benar harus mengeluarkan pengecualian (untuk file yang hilang) yang akan menghentikan eksekusi di sana dengan penjelasan yang tepat tentang apa yang terjadi. Alih-alih jika mengembalikan null, ia menyebar lebih jauh (ke for line in file) dan melempar pengecualian referensi nol yang tidak berarti, yang OK untuk program yang sederhana namun menyebabkan masalah debugging nyata dalam sistem yang jauh lebih kompleks. Jika nulls tidak ada, perancang "openfile" tidak akan dapat membuat kesalahan ini.
mrpyo
2
+1 untuk "Karena bahasa pemrograman pada umumnya dirancang agar praktis bermanfaat daripada benar secara teknis"
Martin Ba
2
Setiap jenis opsi yang saya tahu memungkinkan Anda melakukan failing-on-null dengan satu panggilan metode ekstra pendek (contoh Karat:) let file = something(...).unwrap(). Bergantung pada POV Anda, ini adalah cara mudah untuk tidak menangani kesalahan atau pernyataan singkat bahwa null tidak dapat terjadi. Waktu yang terbuang adalah minimal, dan Anda menghemat waktu di tempat lain karena Anda tidak perlu mencari tahu apakah sesuatu dapat menjadi nol. Keuntungan lain (yang dengan sendirinya bernilai panggilan ekstra) adalah bahwa Anda secara eksplisit mengabaikan kasus kesalahan; ketika gagal ada sedikit keraguan apa yang salah dan ke mana perbaikan harus pergi.
4
@ mrpyo Tidak semua bahasa mendukung pengecualian dan / atau penanganan pengecualian (a la try / catch). Dan pengecualian dapat disalahgunakan juga - "pengecualian sebagai kontrol aliran" adalah anti-pola umum. Skenario ini - file tidak ada - adalah AFAIK contoh yang paling sering dikutip dari anti-pola itu. Tampaknya Anda mengganti satu praktik buruk dengan yang lain.
David
8
@mrpyo if file exists { open file }menderita kondisi balapan. Satu-satunya cara yang dapat diandalkan untuk mengetahui apakah membuka file akan berhasil adalah dengan mencoba membukanya.
4

Ada penggunaan yang jelas dan praktis dari pointer NULL(atau nil, atau Nil, atau null, Nothingatau apa pun namanya dalam bahasa pilihan Anda).

Untuk bahasa-bahasa yang tidak memiliki sistem pengecualian (misalnya C) pointer nol dapat digunakan sebagai tanda kesalahan ketika pointer harus dikembalikan. Sebagai contoh:

char *buf = malloc(20);
if (!buf)
{
    perror("memory allocation failed");
    exit(1);
}

Di sini, NULLkembali dari malloc(3)digunakan sebagai penanda kegagalan.

Ketika digunakan dalam argumen metode / fungsi, ini dapat menunjukkan penggunaan default untuk argumen atau mengabaikan argumen output. Contoh di bawah ini.

Bahkan untuk bahasa-bahasa dengan mekanisme pengecualian, sebuah null pointer dapat digunakan sebagai indikasi kesalahan lunak (yaitu, kesalahan yang dapat dipulihkan) terutama ketika penanganan pengecualian mahal (misalnya Objective-C):

NSError *err = nil;
NSString *content = [NSString stringWithContentsOfURL:sourceFile
                                         usedEncoding:NULL // This output is ignored
                                                error:&err];
if (!content) // If the object is null, we have a soft error to recover from
{
    fprintf(stderr, "error: %s\n", [[err localizedDescription] UTF8String]);
    if (!error) // Check if the parent method ignored the error argument
        *error = err;
    return nil; // Go back to parent layer, with another soft error.
}

Di sini, kesalahan lunak tidak menyebabkan program macet jika tidak tertangkap. Ini menghilangkan try-catch gila seperti yang dimiliki Java dan memiliki kontrol yang lebih baik dalam aliran program karena kesalahan lunak tidak mengganggu (dan beberapa pengecualian keras yang tersisa biasanya tidak dapat dipulihkan dan tidak tertangkap)

Maxthon Chan
sumber
5
Masalahnya adalah bahwa tidak ada cara untuk membedakan variabel yang seharusnya tidak mengandung nulldari yang seharusnya. Sebagai contoh, jika saya ingin tipe baru yang berisi 5 nilai di Java, saya bisa menggunakan enum, tapi yang saya dapatkan adalah tipe yang bisa menampung 6 nilai (nilai 5 yang saya inginkan + null). Ini cacat dalam sistem tipe.
Doval
@Doval Jika itu situasinya, berikan saja NULL makna (atau jika Anda memiliki default, perlakukan sebagai sinonim dari nilai default) atau gunakan NULL (yang seharusnya tidak muncul di tempat pertama) sebagai penanda kesalahan lunak (Yaitu kesalahan tapi setidaknya tidak menabrak dulu)
Maxthon Chan
1
@ MaxtonChan Nullhanya dapat diberi arti ketika nilai-nilai dari suatu jenis tidak membawa data (misalnya nilai enum). Segera setelah nilai Anda lebih rumit (mis. Sebuah struct), nulltidak dapat diberi arti yang masuk akal untuk jenis itu. Tidak ada cara untuk menggunakan nullsebagai struct atau daftar. Dan, sekali lagi, masalah dengan menggunakan nullsebagai sinyal kesalahan adalah bahwa kita tidak bisa mengatakan apa yang mungkin mengembalikan nol atau menerima nol. Variabel apa pun dalam program Anda bisa menjadi nullkecuali Anda sangat teliti untuk memeriksa setiap satu nullsebelum setiap penggunaan, yang tidak ada yang melakukannya.
Doval
1
@Doval: Tidak akan ada kesulitan inheren tertentu dalam memiliki jenis referensi tetap yang dianggap nullsebagai nilai default yang dapat digunakan (misalnya memiliki nilai default stringberperilaku sebagai string kosong, seperti yang dilakukan di bawah Common Object Model sebelumnya). Semua yang diperlukan akan digunakan untuk bahasa calldaripada callvirtsaat memanggil anggota non-virtual.
supercat
@supercat Itu poin yang bagus, tapi sekarang bukankah Anda perlu menambahkan dukungan untuk membedakan antara tipe yang tidak bisa diubah dan yang tidak bisa diubah? Saya tidak yakin seberapa sepele itu menambahkan ke bahasa.
Doval
4

Ada dua masalah terkait, tetapi sedikit berbeda:

  1. Haruskah nullada? Atau haruskah Anda selalu menggunakan Maybe<T>mana null berguna?
  2. Haruskah semua referensi dibatalkan? Jika tidak, yang mana yang seharusnya menjadi default?

    Harus secara eksplisit menyatakan jenis referensi yang dapat dibatalkan sebagai string?atau serupa akan menghindari sebagian besar (tetapi tidak semua) nullpenyebab masalah , tanpa menjadi terlalu berbeda dari apa yang digunakan oleh programmer.

Saya setidaknya setuju dengan Anda bahwa tidak semua referensi harus dibatalkan. Tetapi menghindari null bukan tanpa kerumitannya:

.NET menginisialisasi semua bidang default<T>sebelum dapat diakses oleh kode terkelola terlebih dahulu. Ini berarti bahwa untuk tipe referensi yang Anda butuhkan nullatau sesuatu yang setara dan bahwa tipe nilai dapat diinisialisasi ke semacam nol tanpa menjalankan kode. Meskipun keduanya memiliki kelemahan parah, kesederhanaan defaultinisialisasi mungkin lebih penting daripada kerugian tersebut.

  • Untuk bidang Misalnya Anda bisa menyiasatinya dengan mengharuskan inisialisasi bidang sebelum mengekspos thispointer ke kode dikelola. Spec # menggunakan rute ini, menggunakan sintaks yang berbeda dari rangkaian konstruktor dibandingkan dengan C #.

  • Untuk bidang statis memastikan ini lebih sulit, kecuali jika Anda mengajukan batasan kuat pada jenis kode apa yang dapat berjalan di penginisialisasi bidang karena Anda tidak bisa menyembunyikan thispointer.

  • Bagaimana cara menginisialisasi array tipe referensi? Pertimbangkan List<T>yang didukung oleh array dengan kapasitas lebih besar dari panjangnya. Elemen yang tersisa harus memiliki beberapa nilai.

Masalah lain adalah bahwa itu tidak memungkinkan metode seperti bool TryGetValue<T>(key, out T value)yang kembali default(T)seolah- valueolah mereka tidak menemukan apa pun. Meskipun dalam kasus ini mudah untuk berpendapat bahwa parameter out adalah desain yang buruk di tempat pertama dan metode ini harus mengembalikan serikat diskriminatif atau mungkin sebagai gantinya.

Semua masalah ini dapat diselesaikan, tetapi tidak semudah "melarang nol dan semuanya baik-baik saja".

CodesInChaos
sumber
Ini List<T>adalah IMHO contoh terbaik, karena akan mengharuskan setiap Tmemiliki nilai default, bahwa setiap item di toko dukungan menjadi Maybe<T>dengan bidang "isValid" tambahan, bahkan ketika Ta Maybe<U>, atau bahwa kode untuk List<T>berperilaku berbeda tergantung apakah Titu sendiri adalah tipe nullable. Saya akan mempertimbangkan inisialisasi T[]elemen ke nilai default untuk menjadi yang paling jahat dari pilihan itu, tetapi tentu saja berarti bahwa elemen harus memiliki nilai default.
supercat
Rust mengikuti poin 1 - tidak ada null sama sekali. Ceylon mengikuti poin 2 - bukan-nol secara default. Referensi yang bisa nol secara eksplisit dinyatakan dengan tipe gabungan yang mencakup referensi atau nol, tetapi nol tidak pernah bisa menjadi nilai referensi biasa. Akibatnya, bahasanya benar-benar aman dan tidak ada NullPointerException karena tidak semantik mungkin.
Jim Balter
2

Sebagian besar bahasa pemrograman yang berguna memungkinkan item data untuk ditulis dan dibaca dalam urutan yang sewenang-wenang, sehingga seringkali tidak mungkin untuk secara statis menentukan urutan di mana membaca dan menulis akan terjadi sebelum suatu program dijalankan. Ada banyak kasus di mana kode sebenarnya akan menyimpan data yang berguna ke setiap slot sebelum membacanya, tetapi di mana membuktikan itu akan sulit. Dengan demikian, akan sering diperlukan untuk menjalankan program di mana setidaknya secara teori dimungkinkan untuk mencoba membaca sesuatu yang belum ditulis dengan nilai yang bermanfaat. Apakah kode itu legal atau tidak, tidak ada cara umum untuk menghentikan kode dari upaya tersebut. Satu-satunya pertanyaan adalah apa yang harus terjadi ketika itu terjadi.

Bahasa dan sistem yang berbeda mengambil pendekatan yang berbeda.

  • Salah satu pendekatan akan mengatakan bahwa setiap upaya untuk membaca sesuatu yang belum ditulis akan memicu kesalahan langsung.

  • Pendekatan kedua adalah meminta kode untuk menyediakan beberapa nilai di setiap lokasi sebelum dimungkinkan untuk membacanya, bahkan jika tidak ada cara agar nilai yang disimpan itu berguna secara semantik.

  • Pendekatan ketiga adalah mengabaikan masalah dan membiarkan apa pun yang terjadi "secara alami" terjadi begitu saja.

  • Pendekatan keempat adalah mengatakan bahwa setiap jenis harus memiliki nilai default, dan setiap slot yang belum ditulis dengan yang lain akan default ke nilai itu.

Pendekatan # 4 jauh lebih aman daripada pendekatan # 3, dan secara umum lebih murah daripada pendekatan # 1 dan # 2. Itu kemudian meninggalkan pertanyaan tentang apa nilai default seharusnya untuk tipe referensi. Untuk tipe referensi yang tidak berubah, dalam banyak kasus akan masuk akal untuk mendefinisikan instance default, dan mengatakan bahwa default untuk variabel apa pun dari tipe itu harus menjadi referensi ke instance tersebut. Namun, untuk jenis referensi yang dapat berubah, itu tidak akan sangat membantu. Jika suatu upaya dilakukan untuk menggunakan jenis referensi yang bisa berubah sebelum ditulis, biasanya tidak ada tindakan yang aman selain menjebak pada titik penggunaan yang dicoba.

Secara semantik, jika seseorang memiliki larik customerstipe Customer[20], dan satu upaya Customer[4].GiveMoney(23)tanpa menyimpan apa pun untuk Customer[4], eksekusi harus terperangkap. Orang dapat berargumen bahwa upaya untuk membaca Customer[4]harus menjebak segera, daripada menunggu sampai kode mencoba GiveMoney, tetapi ada cukup kasus di mana itu berguna untuk membaca slot, mengetahui bahwa itu tidak memiliki nilai, dan kemudian memanfaatkannya informasi, bahwa upaya membaca sendiri gagal sering menjadi gangguan besar.

Beberapa bahasa memungkinkan seseorang menentukan bahwa variabel tertentu tidak boleh mengandung nol, dan segala upaya untuk menyimpan nol harus memicu jebakan segera. Itu adalah fitur yang bermanfaat. Secara umum, bagaimanapun, bahasa apa pun yang memungkinkan pemrogram untuk membuat array referensi akan harus memungkinkan untuk kemungkinan elemen array nol, atau memaksa inisialisasi elemen array ke data yang tidak mungkin bermakna.

supercat
sumber
Tidak akan a Maybe/ Optionjenis memecahkan masalah dengan # 2, karena jika Anda tidak memiliki nilai untuk referensi Anda belum tapi akan memiliki satu di masa depan, Anda hanya dapat menyimpan Nothingdalam Maybe <Ref type>?
Doval
@Doval: Tidak, itu tidak akan menyelesaikan masalah - setidaknya, bukan tanpa memperkenalkan referensi nol lagi. Haruskah "tidak ada" yang bertindak seperti anggota jenis itu? Jika demikian, yang mana? Atau haruskah itu melempar pengecualian? Dalam hal ini, bagaimana Anda lebih baik daripada hanya menggunakan nulldengan benar / masuk akal?
cao
@Doval: Haruskah jenis dukungan dari List<T>a T[]atau a Maybe<T>? Bagaimana dengan jenis dukungan a List<Maybe<T>>?
supercat
@ supercat Saya tidak yakin bagaimana jenis dukungan Maybemasuk akal Listkarena Maybememegang nilai tunggal. Apakah maksud Anda Maybe<T>[]?
Doval
@ cHao Nothinghanya dapat ditugaskan untuk nilai tipe Maybe, jadi tidak seperti menugaskan null. Maybe<T>dan Tdua tipe berbeda.
Doval