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.
sumber
Jawaban:
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:
Jika Anda menggunakan NOP, Anda akan menulis:
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.
sumber
Pro
Cons
"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.
sumber
Saya bukan sumber informasi obyektif yang baik, tetapi secara subyektif:
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.
sumber
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
Nil
dalam 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 mengembalikanint
. Itu dapat kembaliNil
jika 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
Nil
menyertakan fungsito_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 outputNil
jika 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.
sumber
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
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).
sumber
nil
dapatkan#define
NULL dan berperilaku seperti objek nol. Itu adalah fitur runtime sementara di C # / Java null pointer memunculkan kesalahan.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)
grep
mencari kesalahan potensial.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
MyClass
dalam "keadaan default" atau tidak. Selain menempatkanbool nonempty
bidang atau serupa ke permukaan default-saat di runtime, yang terbaik yang dapat Anda lakukan adalah menghasilkan contoh baruMyClass
dengan 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 kestd::unique_ptr<MyClass>
jika mungkin.Jika Anda ingin fungsi pabrik Anda mengembalikan semacam "tempat penampung" yang berbeda dari yang dibangun secara default
MyClass
, gunakan astd::pair
dan pastikan untuk mendokumentasikan bahwa fungsi Anda melakukan ini.Jika fungsi pabrik memiliki nama yang unik, mudah
grep
untuk namanya dan mencari kecerobohan. Namun, sulitgrep
untuk kasus-kasus di mana programmer seharusnya menggunakan fungsi pabrik tetapi tidak .OCaml
Jika Anda menggunakan bahasa seperti OCaml, maka Anda bisa menggunakan
option
tipe (di pustaka standar OCaml) ataueither
tipe (tidak di pustaka standar, tetapi mudah untuk menggulir sendiri). Ataudefaultable
tipe (saya mengarang istilahdefaultable
).A
defaultable
seperti yang ditunjukkan di atas lebih baik daripada pasangan karena pengguna harus mencocokkan pola untuk mengekstrak'a
dan tidak bisa begitu saja mengabaikan elemen pertama dari pasangan.Setara dengan
defaultable
jenis seperti yang ditunjukkan di atas dapat digunakan dalam C ++ menggunakanstd::variant
dengan dua contoh dari jenis yang sama, tetapi bagian daristd::variant
API tidak dapat digunakan jika kedua jenisnya sama. Juga penggunaan anehstd::variant
karena 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
DefaultedMyClass
yang mewarisiMyClass
dan mengembalikannya dari fungsi pabrik Anda. Itu memberi Anda fleksibilitas dalam hal "mematikan" fungsionalitas dalam kasus default jika Anda perlu.sumber