null pointer vs Pola Objek Null

27

Atribusi: Ini tumbuh dari pertanyaan P.SE terkait

Latar belakang saya adalah dalam C / C ++, tetapi saya telah bekerja cukup banyak di Jawa dan saat ini saya sedang mengkode C #. Karena latar belakang C saya, memeriksa pointer yang lewat dan yang dikembalikan adalah barang bekas, tetapi saya mengakuinya bias pandangan saya.

Baru-baru ini saya melihat menyebutkan Pola Objek Null di mana idenya adalah bahwa objek selalu dikembalikan. Kasus normal mengembalikan objek yang diharapkan, diisi dan kasus kesalahan mengembalikan objek kosong bukan pointer nol. Premisnya adalah bahwa fungsi panggilan akan selalu memiliki semacam objek untuk diakses dan karenanya menghindari pelanggaran memori akses nol.

  • Jadi apa pro / kontra dari cek nol versus menggunakan Pola Objek Null?

Saya dapat melihat kode panggilan yang lebih bersih dengan NOP, tetapi saya juga dapat melihat di mana ia akan menciptakan kegagalan tersembunyi yang tidak dapat dinaikkan. Saya lebih suka aplikasi saya gagal keras (alias pengecualian) sementara saya mengembangkannya daripada memiliki kesalahan diam melarikan diri ke alam liar.

  • Tidak bisakah Pola Obyek Null memiliki masalah yang sama seperti tidak melakukan pemeriksaan nol?

Banyak benda yang pernah saya gunakan untuk memegang benda atau wadahnya sendiri. Sepertinya saya harus memiliki kasus khusus untuk menjamin semua wadah benda utama memiliki benda kosong sendiri. Sepertinya ini bisa menjadi jelek dengan beberapa lapisan sarang.

Komunitas
sumber
Bukan "kasus kesalahan" tetapi "dalam semua kasus objek yang valid".
@ ThorbjørnRavnAndersen - poin bagus, dan saya mengedit pertanyaan untuk mencerminkan hal itu. Tolong beri tahu saya jika saya masih salah mengartikan premis NOP.
6
Jawaban singkatnya adalah "pola objek nol" adalah nama yang salah. Ini lebih akurat disebut "objek anti-pola nol". Anda biasanya lebih baik dengan "objek kesalahan" - objek yang valid yang mengimplementasikan seluruh antarmuka kelas, tetapi setiap penggunaannya menyebabkan kematian yang keras dan tiba-tiba.
Jerry Coffin
1
@ JerryCoffin kasus itu harus ditandai dengan pengecualian. Idenya adalah bahwa Anda tidak pernah bisa mendapatkan nol dan karenanya tidak perlu memeriksa nol.
1
Saya tidak suka "pola" ini. Tetapi mungkin itu berakar pada basis data relasional yang dibatasi, di mana lebih mudah untuk merujuk ke "baris nol" khusus dalam kunci asing selama pementasan data yang dapat diedit, sebelum pembaruan akhir ketika semua kunci asing dengan sempurna bukan nol.

Jawaban:

24

Anda tidak akan menggunakan Pola Objek Null di tempat-tempat null (atau Objek Null) dikembalikan karena ada kegagalan bencana. Di tempat-tempat itu saya akan terus mengembalikan nol. Dalam beberapa kasus, jika tidak ada pemulihan, Anda mungkin juga macet karena setidaknya dump crash akan menunjukkan dengan tepat di mana masalah terjadi. Dalam kasus seperti itu ketika Anda menambahkan penanganan kesalahan Anda sendiri, Anda masih akan mematikan prosesnya (sekali lagi, saya katakan untuk kasus-kasus di mana tidak ada pemulihan) tetapi penanganan kesalahan Anda akan menutupi informasi yang sangat penting yang akan disediakan oleh tempat pembuangan kerusakan.

Pola Objek Null lebih untuk tempat-tempat di mana ada perilaku default yang bisa diambil dalam kasus di mana objek tidak ditemukan. Sebagai contoh pertimbangkan hal berikut:

User* pUser = GetUser( "Bob" );

if( pUser )
{
    pUser->SetAddress( "123 Fake St." );
}

Jika Anda menggunakan NOP, Anda akan menulis:

GetUser( "Bob" )->SetAddress( "123 Fake St." );

Perhatikan bahwa perilaku kode ini adalah "jika Bob ada, saya ingin memperbarui alamatnya". Tentunya jika aplikasi Anda mengharuskan Bob hadir, Anda tidak ingin berhasil secara diam-diam. Tetapi ada kasus-kasus di mana jenis perilaku ini akan sesuai. Dan dalam kasus tersebut, bukankah NOP menghasilkan kode yang jauh lebih bersih dan ringkas?

Di tempat-tempat di mana Anda benar-benar tidak bisa hidup tanpa Bob, saya akan meminta GetUser () melempar pengecualian aplikasi (yaitu tidak mengakses pelanggaran atau semacamnya) yang akan ditangani di tingkat yang lebih tinggi dan akan melaporkan kegagalan operasi umum. Dalam hal ini, tidak perlu untuk NOP tetapi juga tidak perlu secara eksplisit memeriksa NULL. IMO, yang memeriksa NULL, hanya membuat kode lebih besar dan menghilangkan keterbacaan. Periksa NULL masih merupakan pilihan desain yang tepat untuk beberapa antarmuka, tetapi tidak sebanyak yang diperkirakan orang.

DXM
sumber
4
Setuju dengan use case untuk pola objek nol. Tetapi mengapa kembali nol ketika menghadapi kegagalan bencana? Mengapa tidak melempar pengecualian?
Apoorv Khurasia
2
@ MonsterTruck: balikkan itu. Ketika saya menulis kode, saya memastikan untuk memvalidasi sebagian besar input yang diterima dari komponen eksternal. Namun, di antara kelas internal jika saya menulis kode sedemikian rupa sehingga fungsi satu kelas tidak akan pernah mengembalikan NULL, maka di sisi panggil saya tidak akan menambahkan centang nol hanya untuk "lebih aman". Satu-satunya cara nilai itu bisa NULL adalah jika beberapa kesalahan logika katastropik terjadi dalam aplikasi saya. Dalam hal itu, saya ingin program saya crash persis di tempat itu karena saya mendapatkan file crash dump yang bagus dan membalikkan kembali stack dan status objek untuk mengidentifikasi kesalahan logika.
DXM
2
@MonsterTruck: dengan "membalikkan" maksud saya, jangan secara eksplisit mengembalikan NULL ketika Anda mengidentifikasi kesalahan katastropik, tetapi mengharapkan NULL hanya terjadi karena beberapa kesalahan katastropik yang tidak terduga.
DXM
Saya sekarang mengerti apa yang Anda maksud dengan kesalahan katastropik - sesuatu yang menyebabkan alokasi objek itu sendiri gagal. Kanan? Sunting: Tidak dapat melakukan @ DXM karena nama panggilan Anda hanya sepanjang 3 karakter.
Apoorv Khurasia
@MonsterTruck: aneh tentang "@". Saya tahu orang lain pernah melakukannya sebelumnya. Saya ingin tahu apakah mereka men-tweak situs web baru-baru ini. Itu tidak harus alokasi. Jika saya menulis kelas dan maksud dari API adalah untuk selalu mengembalikan objek yang valid, maka saya tidak akan meminta penelepon melakukan pemeriksaan nol. Tetapi kesalahan katastropik bisa berupa kesalahan programmer (kesalahan ketik yang menyebabkan NULL dikembalikan), atau mungkin beberapa kondisi balapan yang menghapus objek sebelum waktunya, atau mungkin beberapa tumpukan korupsi. Intinya, setiap jalur kode yang tidak terduga yang menyebabkan NULL dikembalikan. Ketika itu terjadi, Anda ingin ...
DXM
7

Jadi apa pro / kontra dari cek nol versus menggunakan Pola Objek Null?

Pro

  • Pemeriksaan nol lebih baik karena memecahkan lebih banyak kasus. Tidak semua objek memiliki default waras atau perilaku tidak-op.
  • Cek nol lebih solid. Bahkan objek dengan standar waras digunakan di tempat-tempat di mana standar waras tidak valid. Kode harus gagal dekat dengan akar masalah jika gagal. Kode harus jelas gagal jika akan gagal.

Cons

  • Default waras biasanya menghasilkan kode bersih.
  • Default waras biasanya menghasilkan lebih sedikit kesalahan bencana jika mereka berhasil masuk ke alam liar.

"Pro" terakhir ini adalah pembeda utama (dalam pengalaman saya) kapan masing-masing harus diterapkan. "Haruskah kegagalan itu berisik?" Dalam beberapa kasus, Anda ingin kegagalan menjadi sulit dan langsung; jika beberapa skenario yang seharusnya tidak pernah terjadi tidak terjadi. Jika sumber daya vital tidak ditemukan ... dll. Dalam beberapa kasus, Anda setuju dengan default yang waras karena sebenarnya bukan kesalahan : mendapatkan nilai dari kamus, tetapi kuncinya hilang misalnya.

Seperti halnya keputusan desain lainnya, ada kelebihan dan kekurangan tergantung pada kebutuhan Anda.

Telastyn
sumber
1
Di bagian Pro: Setuju dengan poin pertama. Poin kedua adalah kesalahan desainer karena bahkan null dapat digunakan di mana seharusnya tidak. Tidak mengatakan hal buruk tentang pola hanya mencerminkan pada pengembang yang menggunakan pola itu secara tidak benar.
Apoorv Khurasia
Apa yang lebih merupakan kegagalan katstropik, tidak dapat mengirim uang atau mengirim (dan dengan demikian tidak lagi memiliki) uang tanpa penerima menerimanya dan tidak mengetahuinya sebelum dicek secara eksplisit?
user470365
Cons: Sane default dapat digunakan untuk membangun objek baru. Jika objek non-null dikembalikan, beberapa atributnya dapat dimodifikasi saat membuat objek lain. Jika objek null dikembalikan, itu dapat digunakan untuk membuat objek baru. Dalam kedua kasus, kode yang sama berfungsi. Tidak perlu kode terpisah untuk modifikasi salinan dan baru. Ini adalah bagian dari filosofi bahwa setiap baris kode adalah tempat lain untuk bug; mengurangi garis mengurangi bug.
shawnhcorey
@shawnhcorey - apa?
Telastyn
Objek nol harus dapat digunakan seolah-olah itu adalah objek nyata. Sebagai contoh, pertimbangkan NaN IEEE (bukan angka). Jika persamaan salah, itu dikembalikan. Dan itu dapat digunakan untuk perhitungan lebih lanjut karena operasi apa pun di atasnya akan mengembalikan NaN. Ini menyederhanakan kode karena Anda tidak perlu memeriksa NaN setelah setiap operasi aritmatika.
shawnhcorey
6

Saya bukan sumber informasi obyektif yang baik, tetapi secara subyektif:

  • Saya tidak ingin sistem saya mati, selamanya; bagi saya itu pertanda sistem yang dirancang dengan buruk atau tidak lengkap; sistem yang lengkap harus menangani semua kasus yang mungkin dan tidak pernah masuk ke keadaan yang tidak terduga; untuk mencapai ini, saya harus eksplisit dalam memodelkan semua aliran dan situasi; menggunakan nulls tidak eksplisit dan mengelompokkan banyak kasus berbeda di bawah satu umrella; Saya lebih suka objek NonExistingUser ke nol, saya lebih suka NaN ke nol, ... pendekatan seperti itu memungkinkan saya untuk secara eksplisit tentang situasi tersebut dan penanganannya dalam rincian sebanyak yang saya inginkan, sementara nol membuat saya hanya dengan opsi nol; dalam lingkungan seperti Java objek apa pun bisa menjadi nol jadi mengapa Anda lebih suka menyembunyikan di kumpulan generik kasus juga kasus spesifik Anda?
  • implementasi nol memiliki perilaku yang secara drastis berbeda dari objek dan sebagian besar terpaku pada himpunan semua objek (mengapa? mengapa? mengapa?); bagi saya perbedaan drastis seperti itu sepertinya merupakan hasil dari desain nulls sebagai indikasi kesalahan dimana sistem kasus kemungkinan besar akan mati (saya terkejut begitu banyak orang yang sebenarnya lebih suka sistem mereka mati di posting di atas) kecuali ditangani secara eksplisit; bagi saya itu adalah pendekatan yang cacat di root itu - itu memungkinkan sistem untuk mati secara default kecuali jika Anda secara eksplisit telah merawatnya, itu tidak terlalu aman bukan? tetapi dalam kasus apa pun tidak mungkin untuk menulis kode yang memperlakukan nilai null dan objek dengan cara yang sama - hanya beberapa operator bawaan dasar (tetapkan, sama, param, dll.) akan berfungsi, semua kode lain hanya akan gagal dan Anda perlu dua jalur yang sama sekali berbeda;
  • menggunakan skala Objek Null yang lebih baik - Anda dapat memasukkan sebanyak mungkin informasi dan struktur ke dalamnya; NonExistingUser adalah contoh yang sangat bagus - ini dapat berisi email dari pengguna yang Anda coba terima dan sarankan untuk membuat pengguna baru berdasarkan informasi itu, sementara dengan solusi nol saya perlu memikirkan bagaimana menjaga email yang orang coba akses dekat dengan penanganan hasil nol;

Dalam lingkungan seperti Java atau C # itu tidak akan memberi Anda manfaat utama dari kesederhanaan - keamanan penyelesaian yang lengkap, karena Anda masih dapat menerima nol di tempat objek apa pun dalam sistem dan Java tidak memiliki sarana (kecuali penjelasan kustom untuk Kacang-kacangan , dll. untuk melindungi Anda dari hal itu kecuali ifs eksplisit. Tetapi dalam sistem yang bebas dari implementaitons nol, mendekati solusi masalah sedemikian rupa (dengan objek yang mewakili kasus luar biasa) memberikan semua manfaat yang disebutkan. Jadi cobalah membaca tentang sesuatu selain bahasa umum, dan Anda akan menemukan diri Anda mengubah cara pengkodean.

Secara umum objek Null / Kesalahan / jenis pengembalian dianggap strategi penanganan kesalahan yang sangat solid dan cukup matematis, sedangkan pengecualian strategi penanganan Java atau C berjalan seperti hanya satu langkah di atas strategi "mati ASAP" - jadi pada dasarnya meninggalkan semua beban ketidakstabilan dan ketidakpastian sistem pada pengembang atau pemelihara.

Ada juga monad yang dapat dianggap unggul untuk strategi ini (juga unggul dalam kompleksitas pemahaman pada awalnya), tetapi lebih pada kasus ketika Anda perlu memodelkan aspek tipe eksternal, sedangkan Null Object memodelkan aspek internal. Jadi NonExistingUser mungkin dimodelkan sebagai monad maybe_existing.

Dan terakhir saya tidak berpikir bahwa ada hubungan antara pola Objek Null dan diam-diam menangani situasi kesalahan. Itu harus sebaliknya - itu akan memaksa Anda untuk memodelkan masing-masing dan setiap kasus kesalahan secara eksplisit dan menanganinya sesuai. Sekarang tentu saja Anda mungkin memutuskan untuk menjadi ceroboh (untuk alasan apa pun) dan melewatkan penanganan kasus kesalahan secara eksplisit tetapi menanganinya secara umum - yah itu adalah pilihan eksplisit Anda.

Bohdan Tsymbala
sumber
9
Alasan saya menyukai aplikasi saya gagal ketika sesuatu yang tidak terduga terjadi - sesuatu yang tidak terduga terjadi. Bagaimana hal itu terjadi? Mengapa? Saya mendapatkan crash dump dan saya bisa menyelidiki dan memperbaiki masalah yang berpotensi berbahaya, daripada membiarkannya disembunyikan dalam kode. Dalam C #, tentu saja saya dapat menangkap semua pengecualian yang tidak terduga untuk mencoba dan memulihkan aplikasi, tetapi meskipun demikian saya ingin mencatat tempat di mana masalah terjadi dan mencari tahu apakah itu sesuatu yang memerlukan penanganan terpisah (bahkan jika itu "jangan" catat kesalahan ini, sebenarnya itu diharapkan dan dapat dipulihkan ").
Luaan
3

Saya pikir ini menarik untuk dicatat bahwa kelangsungan hidup Obyek Null tampaknya sedikit berbeda tergantung pada apakah bahasa Anda diketik secara statis atau tidak. Ruby adalah bahasa yang mengimplementasikan objek untuk Null (atau Nildalam terminologi bahasa).

Alih-alih mengembalikan pointer ke memori yang tidak diinisialisasi, bahasa akan mengembalikan objek Nil. Ini difasilitasi oleh fakta bahwa bahasa diketik secara dinamis. Fungsi tidak dijamin untuk selalu mengembalikan int. Itu dapat kembali Niljika itu yang perlu dilakukan. Itu menjadi lebih berbelit-belit dan sulit untuk melakukan ini dalam bahasa yang diketik secara statis, karena Anda secara alami berharap nol berlaku untuk objek apa pun yang dapat dipanggil dengan referensi. Anda harus mulai mengimplementasikan versi nol dari objek apa pun yang bisa menjadi nol (atau pergi rute Scala / Haskell dan memiliki semacam "Mungkin" bungkus).

MrLister mengemukakan fakta bahwa dalam C ++ Anda tidak dijamin memiliki inisialisasi referensi / penunjuk saat pertama kali Anda membuatnya. Dengan memiliki objek nol khusus alih-alih pointer nol mentah, Anda bisa mendapatkan beberapa fitur tambahan yang bagus. Ruby Nilmenyertakan fungsi to_s(toString) yang menghasilkan teks kosong. Ini bagus jika Anda tidak keberatan mendapatkan pengembalian nol pada string, dan ingin melewatkan melaporkannya tanpa menabrak. Open Classes juga memungkinkan Anda untuk secara manual mengganti output Niljika perlu.

Memang ini semua bisa diambil dengan sebutir garam. Saya percaya bahwa dalam bahasa yang dinamis, objek nol dapat menjadi kenyamanan ketika digunakan dengan benar. Sayangnya, untuk mendapatkan yang terbaik dari itu mungkin mengharuskan Anda untuk mengedit fungsionalitas dasarnya, yang dapat membuat program Anda sulit untuk dipahami dan dipelihara bagi orang-orang yang terbiasa bekerja dengan bentuk-bentuk yang lebih standar.

KChaloux
sumber
1

Untuk beberapa sudut pandang tambahan, lihat Objective-C, di mana alih-alih memiliki objek Null nilai nil jenis-karya seperti objek. Pesan apa pun yang dikirim ke objek nil tidak memiliki efek dan mengembalikan nilai 0 / 0.0 / NO (false) / NULL / nil, apa pun jenis nilai pengembalian metode ini. Ini digunakan sebagai pola yang sangat meresap dalam pemrograman MacOS X dan iOS, dan biasanya metode akan dirancang sehingga objek nil yang mengembalikan 0 adalah hasil yang masuk akal.

Misalnya, jika Anda ingin memeriksa apakah string memiliki satu atau lebih karakter, Anda bisa menulis

aString.length > 0

dan dapatkan hasil yang masuk akal jika aString == nil, karena string yang tidak ada tidak mengandung karakter apa pun. Orang-orang cenderung membutuhkan waktu untuk membiasakan diri, tetapi mungkin perlu waktu bagi saya untuk terbiasa memeriksa nilai nil di mana-mana dalam bahasa lain.

(Ada juga objek tunggal kelas NSNull yang berperilaku sangat berbeda dan tidak banyak digunakan dalam praktik).

gnasher729
sumber
Objective-C membuat Null Object / Null Pointer benar-benar buram. nildapatkan #defineNULL dan berperilaku seperti objek nol. Itu adalah fitur runtime sementara di C # / Java null pointer memunculkan kesalahan.
Maxthon Chan
0

Jangan gunakan jika Anda bisa menghindarinya. Gunakan fungsi pabrik untuk mengembalikan jenis opsi, tupel, atau jenis jumlah khusus. Dengan kata lain, mewakili nilai yang berpotensi gagal standar dengan tipe yang berbeda dari nilai yang dijamin tidak akan gagal bayar.

Pertama, mari kita daftar desiderata lalu pikirkan tentang bagaimana kita dapat mengekspresikan ini dalam beberapa bahasa (C ++, OCaml, Python)

  1. menangkap penggunaan objek default yang tidak diinginkan pada waktu kompilasi.
  2. memperjelas apakah nilai yang diberikan berpotensi gagal atau tidak saat membaca kode.
  3. pilih nilai default kami yang masuk akal, jika berlaku, satu kali per jenis . Tentu saja jangan memilih default yang berpotensi berbeda di setiap situs panggilan .
  4. membuatnya mudah untuk alat analisis statis atau manusia dengan grepmencari kesalahan potensial.
  5. Untuk beberapa aplikasi , program harus dilanjutkan secara normal jika secara tak terduga diberi nilai default. Untuk aplikasi lain , program harus dihentikan segera jika diberi nilai default, idealnya dengan cara yang informatif.

Saya pikir ketegangan antara Pola Obyek Null dan pointer nol berasal dari (5). Jika kita dapat mendeteksi kesalahan cukup awal, (5) menjadi moot.

Mari pertimbangkan bahasa ini dengan bahasa:

C ++

Menurut pendapat saya, kelas C ++ umumnya harus dibangun-standar karena memudahkan interaksi dengan perpustakaan dan membuatnya lebih mudah untuk menggunakan kelas dalam wadah. Ini juga menyederhanakan pewarisan karena Anda tidak perlu memikirkan tentang konstruktor superclass mana yang harus dihubungi.

Namun, ini berarti bahwa Anda tidak dapat mengetahui dengan pasti apakah nilai tipe MyClassdalam "keadaan default" atau tidak. Selain menempatkan bool nonemptybidang atau serupa ke permukaan default-saat di runtime, yang terbaik yang dapat Anda lakukan adalah menghasilkan contoh baru MyClassdengan cara yang memaksa pengguna untuk memeriksanya .

Saya akan merekomendasikan menggunakan fungsi pabrik yang mengembalikan std::optional<MyClass>, std::pair<bool, MyClass>atau referensi nilai-r ke std::unique_ptr<MyClass>jika mungkin.

Jika Anda ingin fungsi pabrik Anda mengembalikan semacam "tempat penampung" yang berbeda dari yang dibangun secara default MyClass, gunakan a std::pairdan pastikan untuk mendokumentasikan bahwa fungsi Anda melakukan ini.

Jika fungsi pabrik memiliki nama yang unik, mudah grepuntuk namanya dan mencari kecerobohan. Namun, sulit grepuntuk kasus-kasus di mana programmer seharusnya menggunakan fungsi pabrik tetapi tidak .

OCaml

Jika Anda menggunakan bahasa seperti OCaml, maka Anda bisa menggunakan optiontipe (di pustaka standar OCaml) atau eithertipe (tidak di pustaka standar, tetapi mudah untuk menggulir sendiri). Atau defaultabletipe (saya mengarang istilah defaultable).

type 'a option =
    | None
    | Some of 'a

type ('a, 'b) either =
    | Left of 'a
    | Right of 'b

type 'a defaultable =
    | Default of 'a
    | Real of 'a

A defaultableseperti yang ditunjukkan di atas lebih baik daripada pasangan karena pengguna harus mencocokkan pola untuk mengekstrak 'adan tidak bisa begitu saja mengabaikan elemen pertama dari pasangan.

Setara dengan defaultablejenis seperti yang ditunjukkan di atas dapat digunakan dalam C ++ menggunakan std::variantdengan dua contoh dari jenis yang sama, tetapi bagian dari std::variantAPI tidak dapat digunakan jika kedua jenisnya sama. Juga penggunaan aneh std::variantkarena konstruktor jenisnya tidak disebutkan namanya.

Python

Lagi pula, Anda tidak mendapatkan waktu kompilasi untuk memeriksa Python. Tapi, karena diketik secara dinamis, biasanya tidak ada keadaan di mana Anda memerlukan instance placeholder dari beberapa jenis untuk menenangkan kompiler.

Saya akan merekomendasikan hanya melempar pengecualian ketika Anda akan dipaksa untuk membuat contoh default.

Jika itu tidak dapat diterima, saya akan merekomendasikan membuat DefaultedMyClassyang mewarisi MyClassdan mengembalikannya dari fungsi pabrik Anda. Itu memberi Anda fleksibilitas dalam hal "mematikan" fungsionalitas dalam kasus default jika Anda perlu.

Gregory Nisbet
sumber