Apakah bidang Boolean baru lebih baik daripada referensi nol ketika suatu nilai bisa tidak ada secara bermakna?

39

Sebagai contoh, misalkan saya memiliki kelas Member,, yang memiliki lastChangePasswordTime:

class Member{
  .
  .
  .
  constructor(){
    this.lastChangePasswordTime=null,
  }
}

yang terakhirChangePasswordTime dapat berarti tidak ada, karena beberapa anggota mungkin tidak pernah mengubah kata sandi mereka.

Tetapi menurut Jika nol itu jahat, apa yang harus digunakan ketika suatu nilai bisa tidak ada? dan https://softwareengineering.stackexchange.com/a/12836/248528 , saya tidak boleh menggunakan null untuk mewakili nilai yang tidak ada. Jadi saya mencoba menambahkan bendera Boolean:

class Member{
  .
  .
  .
  constructor(){
    this.isPasswordChanged=false,
    this.lastChangePasswordTime=null,
  }
}

Tapi saya pikir itu cukup usang karena:

  1. Ketika isPasswordChanged salah, lastChangePasswordTime harus menjadi nol, dan memeriksa lastChangePasswordTime == null hampir identik dengan memeriksa isPasswordChanged salah, jadi saya lebih suka memeriksa lastChangePasswordTime == null secara langsung

  2. Saat mengubah logika di sini, saya mungkin lupa memperbarui kedua bidang.

Catatan: ketika pengguna mengubah kata sandi, saya akan mencatat waktu seperti ini:

this.lastChangePasswordTime=Date.now();

Apakah bidang Boolean tambahan lebih baik daripada referensi nol di sini?

ocomfd
sumber
51
Pertanyaan ini cukup spesifik bahasa, karena apa yang merupakan solusi yang baik sangat tergantung pada apa yang ditawarkan bahasa pemrograman Anda. Misalnya, dalam C ++ 17 atau Scala, Anda akan menggunakan std::optionalatau Option. Dalam bahasa lain, Anda mungkin harus membangun mekanisme yang tepat sendiri, atau Anda mungkin benar-benar menggunakan nullatau sesuatu yang serupa karena lebih idiomatik.
Christian Hackl
16
Apakah ada alasan mengapa Anda tidak ingin lastChangePasswordTime disetel ke waktu pembuatan kata sandi (bagaimana pun juga, itu adalah mod)?
Kristian H
@ChristianHackl hmm, setuju bahwa ada solusi "sempurna" yang berbeda, tetapi saya tidak melihat bahasa (utama) meskipun di mana menggunakan boolean terpisah akan menjadi ide yang lebih baik secara umum daripada melakukan pemeriksaan nol / nol. Tidak sepenuhnya yakin tentang C / C ++ karena saya belum aktif di sana cukup lama.
Frank Hopkins
@FrankHopkins: Contohnya adalah bahasa di mana variabel dapat dibiarkan tidak diinisialisasi, misalnya C atau C ++. lastChangePasswordTimemungkin pointer tidak diinisialisasi di sana, dan membandingkannya dengan apa pun akan menjadi perilaku yang tidak terdefinisi. Bukan alasan yang sangat kuat untuk tidak menginisialisasi pointer ke NULL/ nullptrsebagai gantinya, terutama tidak dalam C ++ modern (di mana Anda tidak akan menggunakan pointer sama sekali), tetapi siapa yang tahu? Contoh lain adalah bahasa tanpa pointer, atau dengan dukungan buruk untuk pointer, mungkin. (FORTRAN 77 muncul di benak ...)
Christian Hackl
Saya akan menggunakan enum dengan 3 kasing untuk ini :)
J. Doe

Jawaban:

72

Saya tidak mengerti mengapa, jika Anda tidak memiliki nilai yang berarti, nullsebaiknya tidak digunakan jika Anda sengaja dan berhati-hati.

Jika tujuan Anda adalah mengelilingi nilai isPasswordChangednol untuk mencegah referensi secara tidak sengaja, saya sarankan membuat nilai sebagai fungsi atau properti yang mengembalikan hasil pemeriksaan nol, misalnya:

class Member {
    DateTime lastChangePasswordTime = null;
    bool isPasswordChanged() { return lastChangePasswordTime != null; }

}

Menurut pendapat saya, melakukannya dengan cara ini:

  • Memberikan pembacaan kode yang lebih baik daripada pemeriksaan nol, yang mungkin kehilangan konteks.
  • Menghapus kebutuhan Anda yang harus benar-benar khawatir tentang mempertahankan isPasswordChangednilai yang Anda sebutkan.

Cara Anda mempertahankan data (mungkin dalam database) akan bertanggung jawab untuk memastikan bahwa nol tetap dipertahankan.

TZHX
sumber
29
+1 untuk pembukaan. Penggunaan sembarangan null umumnya tidak disetujui hanya dalam kasus-kasus di mana nol adalah hasil yang tidak terduga, yaitu di mana itu tidak masuk akal (untuk konsumen). Jika nol bermakna, maka itu bukan hasil yang tidak terduga.
Flater
28
Saya akan membuatnya sedikit lebih kuat karena tidak pernah memiliki dua variabel yang mempertahankan kondisi yang sama. Itu akan gagal. Menyembunyikan nilai nol, dengan asumsi nilai nol masuk akal di dalam instance, dari luar adalah hal yang sangat baik. Dalam hal ini di mana Anda nanti dapat memutuskan bahwa 1970-01-01 menunjukkan bahwa kata sandi tidak pernah diubah. Maka logika dari sisa program tidak harus peduli.
Bent
3
Anda bisa lupa menelepon isPasswordChangedsama seperti Anda lupa memeriksa apakah itu nol. Saya melihat tidak ada yang didapat di sini.
Gherman
9
@ Gherman Jika konsumen tidak mendapatkan konsep bahwa null bermakna atau bahwa ia perlu memeriksa nilai yang ada, ia tersesat, tetapi jika metode ini digunakan, jelas bagi semua orang yang membaca kode mengapa ada adalah cek dan ada kemungkinan masuk akal bahwa nilainya nol / tidak ada. Kalau tidak, tidak jelas apakah itu hanya pengembang menambahkan cek nol hanya "karena mengapa tidak" atau apakah itu bagian dari konsep. Tentu, satu dapat mengetahuinya, tetapi satu cara Anda memiliki informasi secara langsung yang lain Anda perlu melihat ke dalam implementasi.
Frank Hopkins
2
@ Frankhopkins: Poin bagus, tetapi jika konkurensi digunakan, bahkan pemeriksaan variabel tunggal mungkin memerlukan penguncian.
Christian Hackl
63

nullstidak jahat. Menggunakannya tanpa berpikir adalah. Ini adalah kasus di mana nulljawaban yang tepat benar - tidak ada tanggal.

Perhatikan bahwa solusi Anda menciptakan lebih banyak masalah. Apa arti dari tanggal yang ditetapkan untuk sesuatu, tetapi isPasswordChangeditu salah? Anda baru saja membuat kasus informasi yang saling bertentangan yang perlu Anda tangkap dan perlakukan secara khusus, sementara nullnilai memiliki makna yang jelas, tidak ambigu, dan tidak dapat bertentangan dengan informasi lainnya.

Jadi tidak, solusi Anda tidak lebih baik. Mengizinkan nullnilai adalah pendekatan yang tepat di sini. Orang yang mengklaim itu nullselalu jahat tidak peduli konteksnya tidak mengerti mengapa nullada.

Tom
sumber
17
Secara historis, nullada karena Tony Hoare membuat kesalahan miliaran dolar pada tahun 1965. Orang-orang yang mengklaim bahwa null"jahat" terlalu menyederhanakan topik, tentu saja, tetapi itu adalah hal yang baik bahwa bahasa modern atau gaya pemrograman bergerak menjauh dari null, menggantikan dengan jenis opsi khusus.
Christian Hackl
9
@ChristianHackl: hanya karena kepentingan sejarah - die Hoare pernah mengatakan apa alternatif praktis yang lebih baik (tanpa beralih ke paradigma yang sama sekali baru)?
AnoE
5
@AnoE: Pertanyaan yang menarik. Tidak tahu apa yang dikatakan Hoare tentang hal itu, tetapi pemrograman tanpa-nol sering menjadi kenyataan akhir-akhir ini dalam bahasa pemrograman modern yang dirancang dengan baik.
Christian Hackl
10
@ Tom, apa? Dalam bahasa seperti C # dan Java, DateTimebidang adalah penunjuk. Seluruh konsep nullhanya masuk akal untuk petunjuk!
Todd Sewell
16
@ Tom: Null pointer hanya berbahaya pada bahasa dan implementasi yang memungkinkan mereka untuk diindeks secara buta dan ditinjau kembali, dengan keriuhan apa pun yang mungkin terjadi. Dalam bahasa yang menjebak upaya untuk mengindeks atau melakukan referensi pointer nol, itu akan sering lebih berbahaya daripada pointer ke objek dummy. Ada banyak situasi di mana memiliki program yang diakhiri secara tidak normal mungkin lebih baik daripada memiliki program yang menghasilkan angka yang tidak berarti, dan pada sistem yang menjebak mereka, pointer nol adalah resep yang tepat untuk itu.
supercat
29

Tergantung pada bahasa pemrograman Anda, mungkin ada alternatif yang baik, seperti optionaltipe data. Dalam C ++ (17), ini akan menjadi std :: opsional . Entah itu tidak ada atau memiliki nilai sewenang-wenang dari tipe data yang mendasarinya.

dasmy
sumber
8
Ada Optionaldi java juga; arti dari nol Optionalterserah padamu ...
Matthieu M.
1
Datang ke sini untuk terhubung dengan Opsional juga. Saya akan menyandikan ini sebagai jenis dalam bahasa yang memilikinya.
Zipp
2
@FrankHopkins Sayang sekali tidak ada di Jawa yang mencegah Anda menulis Optional<Date> lastPasswordChangeTime = null;, tetapi saya tidak akan menyerah hanya karena itu. Sebaliknya, saya akan menanamkan aturan tim yang dipegang teguh, bahwa tidak ada nilai opsional yang diizinkan untuk ditetapkan null. Opsional membeli Anda terlalu banyak fitur bagus untuk menyerah begitu saja. Anda dapat dengan mudah menetapkan nilai default ( orElseatau dengan evaluasi malas:) orElseGet, melempar kesalahan ( orElseThrow), mengubah nilai-nilai dengan cara aman nol ( map, flatmap), dll.
Alexander - Reinstate Monica
5
@FrankHopkins Memeriksa isPresenthampir selalu merupakan bau kode, dalam pengalaman saya, dan pertanda yang cukup dapat diandalkan bahwa para pengembang yang menggunakan opsi-opsi ini "tidak ada gunanya". Memang, pada dasarnya tidak ada manfaatnya ref == null, tetapi itu juga bukan sesuatu yang harus sering Anda gunakan. Bahkan jika tim tidak stabil, alat analisis statis dapat menetapkan hukum, seperti linter atau FindBugs , yang dapat menangkap sebagian besar kasus.
Alexander - Pasang kembali Monica
5
@FrankHopkins: Mungkin saja komunitas Jawa hanya membutuhkan sedikit perubahan paradigma, yang mungkin belum memakan waktu bertahun-tahun. Jika Anda melihat Scala, misalnya, itu mendukung nullkarena bahasa tersebut 100% kompatibel dengan Java, tetapi tidak ada yang menggunakannya, dan Option[T]semuanya ada di sana, dengan dukungan pemrograman fungsional yang sangat baik seperti map,, flatMappencocokan pola, dan sebagainya, yang berjalan jauh, jauh melampaui == nullcek. Anda benar bahwa dalam teori, jenis opsi terdengar sedikit lebih dari sekadar penunjuk yang dimuliakan, tetapi kenyataan membuktikan bahwa argumen itu salah.
Christian Hackl
11

Menggunakan null dengan benar

Ada berbagai cara penggunaan null. Cara yang paling umum dan benar secara semantik adalah menggunakannya ketika Anda mungkin atau mungkin tidak memiliki nilai tunggal . Dalam hal ini nilai sama dengan nullatau adalah sesuatu yang bermakna seperti catatan dari database atau sesuatu.

Dalam situasi ini, Anda lebih sering menggunakannya seperti ini (dalam pseudo-code):

if (value is null) {
  doSomethingAboutIt();
  return;
}

doSomethingUseful(value);

Masalah

Dan itu memiliki masalah yang sangat besar. Masalahnya adalah pada saat Anda meminta doSomethingUsefulnilainya mungkin belum diperiksa null! Jika tidak maka program kemungkinan akan macet. Dan pengguna bahkan mungkin tidak melihat pesan kesalahan yang baik, dibiarkan dengan sesuatu seperti "kesalahan mengerikan: nilai yang diinginkan tetapi mendapat nol!" (setelah pembaruan: meskipun mungkin ada kesalahan yang kurang informatif seperti Segmentation fault. Core dumped., atau lebih buruk lagi, tidak ada kesalahan dan manipulasi salah pada null dalam beberapa kasus)

Lupa menulis cek nulldan menangani nullsituasi adalah bug yang sangat umum . Inilah sebabnya mengapa Tony Hoare yang menemukan nullmengatakan pada konferensi perangkat lunak bernama QCon London pada 2009 bahwa ia membuat kesalahan miliaran dolar pada tahun 1965: https://www.infoq.com/presentations/Null-References-The-Billion-Dollar- Kesalahan-Tony-Hoare

Menghindari masalah

Beberapa teknologi dan bahasa membuat pengecekan nulltidak mungkin dilupakan dengan cara yang berbeda, mengurangi jumlah bug.

Misalnya Haskell memiliki Maybemonad bukan nol. Misalkan DatabaseRecordadalah tipe yang ditentukan pengguna. Dalam Haskell nilai tipe Maybe DatabaseRecordbisa sama Just <somevalue>atau mungkin sama dengan Nothing. Anda kemudian dapat menggunakannya dengan cara yang berbeda tetapi tidak peduli bagaimana Anda menggunakannya, Anda tidak dapat menerapkan beberapa operasi Nothingtanpa menyadarinya.

Misalnya fungsi ini disebut zeroAsDefaultpengembalian xuntuk Just xdan 0untuk Nothing:

zeroAsDefault :: Maybe Int -> Int
zeroAsDefault mx = case mx of
    Nothing -> 0
    Just x -> x

Christian Hackl mengatakan C ++ 17 dan Scala memiliki cara mereka sendiri. Jadi, Anda mungkin ingin mencoba mencari tahu apakah bahasa Anda memiliki sesuatu seperti itu dan menggunakannya.

Nulls masih digunakan secara luas

Jika Anda tidak memiliki yang lebih baik maka menggunakan nullitu tidak masalah. Tetap waspada untuk itu. Ketik deklarasi dalam fungsi akan sedikit membantu Anda.

Mungkin itu kedengarannya tidak terlalu progresif tetapi Anda harus memeriksa apakah rekan kerja Anda ingin menggunakan nullatau sesuatu yang lain. Mereka mungkin konservatif dan mungkin tidak ingin menggunakan struktur data baru karena beberapa alasan. Misalnya mendukung versi bahasa yang lebih lama. Hal-hal seperti itu harus dinyatakan dalam standar pengkodean proyek dan didiskusikan dengan baik dengan tim.

Atas proposal Anda

Anda menyarankan menggunakan bidang boolean yang terpisah. Tetapi Anda harus memeriksanya dan mungkin masih lupa untuk memeriksanya. Jadi tidak ada yang menang di sini. Jika Anda bahkan dapat melupakan sesuatu yang lain, seperti memperbarui kedua nilai setiap kali, maka itu lebih buruk lagi. Jika masalah lupa untuk memeriksa nulltidak terpecahkan maka tidak ada gunanya. Menghindari nullitu sulit dan Anda seharusnya tidak melakukannya sedemikian rupa sehingga membuatnya lebih buruk.

Bagaimana tidak menggunakan null

Akhirnya ada cara umum untuk menggunakan secara nulltidak benar. Salah satu cara adalah menggunakannya di tempat struktur data kosong seperti array dan string. Array kosong adalah array yang tepat seperti yang lainnya! Itu hampir selalu penting dan berguna untuk struktur data, yang dapat memuat banyak nilai, untuk dapat kosong, yaitu memiliki 0 panjang.

Dari sudut pandang aljabar, string kosong untuk string sama seperti 0 untuk angka, yaitu identitas:

a+0=a
concat(str, '')=str

String kosong memungkinkan string secara umum menjadi monoid: https://en.wikipedia.org/wiki/Monoid Jika Anda tidak mendapatkannya, itu tidak penting bagi Anda.

Sekarang mari kita lihat mengapa penting untuk pemrograman dengan contoh ini:

for (element in array) {
  doSomething(element);
}

Jika kita melewatkan array kosong di sini kode akan berfungsi dengan baik. Itu tidak akan melakukan apa-apa. Namun jika kita melewati di nullsini maka kita kemungkinan akan mengalami crash dengan kesalahan seperti "tidak dapat melewati null, maaf". Kita bisa membungkusnya iftetapi itu kurang bersih dan lagi, Anda mungkin lupa memeriksanya

Cara menangani null

Apa yang doSomethingAboutIt()harus dilakukan dan terutama apakah harus mengeluarkan pengecualian adalah masalah rumit lainnya. Singkatnya itu tergantung pada apakah nullnilai input yang dapat diterima untuk tugas yang diberikan dan apa yang diharapkan sebagai respons. Pengecualian untuk acara yang tidak diharapkan. Saya tidak akan masuk lebih jauh ke topik itu. Jawaban ini sudah sangat lama.

Gherman
sumber
5
Sebenarnya, horrible error: wanted value but got null!jauh lebih baik daripada yang lebih khas Segmentation fault. Core dumped....
Toby Speight
2
@TobySpeight Benar, tapi saya lebih suka sesuatu yang ada di dalam istilah pengguna seperti Error: the product you want to buy is out of stock.
Gherman
Tentu - saya hanya mengamati bahwa itu bisa (dan seringkali) lebih buruk . Saya sepenuhnya setuju tentang apa yang akan lebih baik! Dan jawaban Anda cukup banyak apa yang akan saya katakan untuk pertanyaan ini: +1.
Toby Speight
2
@ Gembala: Melewati nulltempat nullyang tidak diizinkan adalah kesalahan pemrograman, yaitu bug dalam kode. Produk kehabisan stok adalah bagian dari logika bisnis biasa dan bukan bug. Anda tidak dapat, dan tidak boleh mencoba, menerjemahkan deteksi bug ke pesan normal di antarmuka pengguna. Menabrak segera dan tidak mengeksekusi lebih banyak kode adalah tindakan yang disukai, terutama jika itu adalah aplikasi penting (misalnya yang melakukan transaksi keuangan nyata).
Christian Hackl
1
@EricDuminil Kami ingin menghapus (atau mengurangi) kemungkinan melakukan kesalahan, tidak menghapusnya nullsepenuhnya, bahkan dari latar belakang.
Gherman
4

Selain semua jawaban yang sangat baik yang diberikan sebelumnya, saya akan menambahkan bahwa setiap kali Anda tergoda untuk membiarkan bidang menjadi nol, pikirkan baik-baik jika itu mungkin jenis daftar. Tipe nullable adalah daftar elemen 0 atau 1 yang ekivalen, dan seringkali ini dapat digeneralisasi ke daftar elemen N. Khususnya dalam hal ini, Anda mungkin ingin mempertimbangkan untuk lastChangePasswordTimemenjadi daftar passwordChangeTimes.

Joel
sumber
Itu poin yang bagus. Hal yang sama sering berlaku untuk tipe opsi; suatu opsi dapat dilihat sebagai kasus khusus dari suatu urutan.
Christian Hackl
2

Tanyakan pada diri Anda sendiri: perilaku mana yang membutuhkan bidang lastChangePasswordTime?

Jika Anda memerlukan bidang itu untuk metode IsPasswordExpired () untuk menentukan apakah Anggota harus sering diminta untuk mengubah kata sandi mereka, saya akan mengatur bidang tersebut ke waktu Anggota awalnya dibuat. Implementasi IsPasswordExpired () adalah sama untuk anggota baru dan yang sudah ada.

class Member{
   private DateTime lastChangePasswordTime;

   public Member(DateTime lastChangePasswordTime) {
      // set value, maybe check for null
   }

   public bool IsPasswordExpired() {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Jika Anda memiliki persyaratan terpisah bahwa anggota yang baru dibuat harus memperbarui kata sandi mereka, saya akan menambahkan bidang boolean terpisah yang disebut kata sandiShouldBeChanged dan setel ke true saat membuat. Saya kemudian akan mengubah fungsionalitas metode IsPasswordExpired () untuk menyertakan cek untuk bidang itu (dan mengganti nama metode menjadi ShouldChangePassword).

class Member{
   private DateTime lastChangePasswordTime;
   private bool passwordShouldBeChanged;

   public Member(DateTime lastChangePasswordTime, bool passwordShouldBeChanged) {
      // set values, maybe check for nulls
   }

   public bool ShouldChangePassword() {
      return PasswordExpired(lastChangePasswordTime) || passwordShouldBeChanged;
   }

   private static bool PasswordExpired(DateTime lastChangePasswordTime) {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Buat niat Anda eksplisit dalam kode.

Rik D
sumber
2

Pertama, nulls evil adalah dogma dan seperti biasa dengan dogma itu bekerja paling baik sebagai pedoman dan bukan sebagai lulus / tidak lulus ujian.

Kedua, Anda dapat mendefinisikan kembali situasi Anda dengan cara yang masuk akal bahwa nilainya tidak akan pernah menjadi nol. InititialPasswordChanged adalah Boolean yang awalnya disetel ke false, PasswordSetTime adalah tanggal dan waktu ketika kata sandi saat ini disetel.

Perhatikan bahwa meskipun ini memerlukan sedikit biaya, kini Anda SELALU dapat menghitung berapa lama sejak kata sandi terakhir kali ditetapkan.

jmoreno
sumber
1

Keduanya 'aman / waras / benar' jika penelepon memeriksa sebelum digunakan. Masalahnya adalah apa yang terjadi jika penelepon tidak memeriksa. Mana yang lebih baik, beberapa rasa kesalahan nol atau menggunakan nilai yang tidak valid?

Tidak ada jawaban yang benar. Itu tergantung pada apa yang Anda khawatirkan.

Jika crash benar - benar buruk tetapi jawabannya tidak kritis atau memiliki nilai default yang diterima, maka mungkin menggunakan boolean sebagai flag lebih baik. Jika menggunakan jawaban yang salah adalah masalah yang lebih buruk daripada menabrak, maka menggunakan null lebih baik.

Untuk sebagian besar kasus 'tipikal', kegagalan cepat dan memaksa penelepon untuk memeriksa adalah cara tercepat untuk waras kode, dan karenanya saya pikir nol harus menjadi pilihan default. Saya tidak akan terlalu percaya pada penginjil "X adalah akar dari semua kejahatan"; mereka biasanya tidak mengantisipasi semua kasus penggunaan.

Sendirian
sumber