Ist ein neues Boolesches Feld besser als eine Nullreferenz, wenn ein Wert sinnvoll fehlen kann?

39

Angenommen, ich habe eine Klasse Membermit einer lastChangePasswordTime:

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

Wessen lastChangePasswordTime kann in Abwesenheit sinnvoll sein, da einige Mitglieder ihre Passwörter möglicherweise nie ändern.

Aber laut Wenn Nullen böse sind, was sollte verwendet werden, wenn ein Wert sinnvoll fehlen kann? und https://softwareengineering.stackexchange.com/a/12836/248528 , ich sollte nicht null verwenden, um einen sinnvollen fehlenden Wert darzustellen. Also versuche ich eine Boolesche Flagge hinzuzufügen:

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

Aber ich denke, es ist ziemlich veraltet, weil:

  1. Wenn isPasswordChanged false ist, muss lastChangePasswordTime null sein, und die Überprüfung von lastChangePasswordTime == null ist fast identisch mit der Überprüfung von isPasswordChanged false. Ich bevorzuge daher, lastChangePasswordTime == null direkt zu überprüfen

  2. Wenn ich hier die Logik ändere, kann es sein, dass ich vergesse, beide Felder zu aktualisieren .

Hinweis: Wenn ein Benutzer Passwörter ändert, würde ich die Zeit folgendermaßen aufzeichnen:

this.lastChangePasswordTime=Date.now();

Ist das zusätzliche Boolesche Feld hier besser als eine Nullreferenz?

ocomfd
quelle
51
Diese Frage ist sehr sprachspezifisch, denn was eine gute Lösung ausmacht, hängt weitgehend davon ab, was Ihre Programmiersprache bietet. In C ++ 17 oder Scala würden Sie beispielsweise std::optionaloder verwenden Option. In anderen Sprachen müssen Sie möglicherweise selbst einen geeigneten Mechanismus erstellen, oder Sie greifen auf einen nullähnlichen oder einen ähnlichen Mechanismus zurück, weil er idiomatischer ist.
Christian Hackl
16
Gibt es einen Grund, warum Sie nicht möchten, dass lastChangePasswordTime auf die Kennworterstellungszeit gesetzt wird (die Erstellung ist schließlich ein Mod)?
Kristian H
@ChristianHackl hmm, stimme zu, dass es verschiedene "perfekte" Lösungen gibt, aber ich sehe keine (Haupt-) Sprache, obwohl die Verwendung eines separaten Booleschen Werts im Allgemeinen eine bessere Idee wäre, als Null / Null-Prüfungen durchzuführen. Ich bin mir bei C / C ++ nicht ganz sicher, da ich dort schon eine ganze Weile nicht mehr aktiv war.
Frank Hopkins
@FrankHopkins: Ein Beispiel wären Sprachen, in denen Variablen nicht initialisiert werden können, z. B. C oder C ++. lastChangePasswordTimekann dort ein nicht initialisierter Zeiger sein und ein Vergleich mit irgendetwas wäre undefiniertes Verhalten. Kein wirklich zwingender Grund, den Zeiger nicht auf NULL/ zu initialisieren nullptr, insbesondere nicht in modernem C ++ (wo Sie überhaupt keinen Zeiger verwenden würden), aber wer weiß? Ein anderes Beispiel wären Sprachen ohne Zeiger oder mit schlechter Unterstützung für Zeiger. (FORTRAN 77 kommt in den Sinn ...)
Christian Hackl
Ich würde eine Aufzählung mit 3 Fällen dafür verwenden :)
J. Doe

Antworten:

72

Ich verstehe nicht, warum, wenn Sie einen bedeutungslosen Wert haben, nullsollte nicht verwendet werden, wenn Sie absichtlich und vorsichtig sind.

Wenn Sie den nullfähigen Wert umschließen möchten, um zu verhindern, dass versehentlich darauf verwiesen wird, würde ich vorschlagen, den isPasswordChangedWert als Funktion oder Eigenschaft zu erstellen, die das Ergebnis einer Nullprüfung zurückgibt. Beispiel:

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

}

Meiner Meinung nach mache ich das so:

  • Bessere Lesbarkeit des Codes als bei einer Nullprüfung, wodurch der Kontext möglicherweise verloren geht.
  • Sie müssen sich nicht mehr darum kümmern, den von isPasswordChangedIhnen genannten Wert beizubehalten .

Die Art und Weise, wie Sie die Daten speichern (vermutlich in einer Datenbank), ist dafür verantwortlich, dass die Nullen erhalten bleiben.

TZHX
quelle
29
+1 für die Eröffnung. Die vorschriftsmäßige Verwendung von Null wird im Allgemeinen nur in Fällen abgelehnt, in denen Null ein unerwartetes Ergebnis ist, dh in denen es für einen Verbraucher keinen Sinn ergibt. Wenn null sinnvoll ist, ist dies kein unerwartetes Ergebnis.
Flater
28
Ich würde es etwas stärker ausdrücken, da es niemals zwei Variablen gibt, die den gleichen Zustand beibehalten. Es wird scheitern. Es ist eine sehr gute Sache, einen Nullwert von außen zu verbergen, vorausgesetzt, der Nullwert ist innerhalb der Instanz sinnvoll. In diesem Fall können Sie später entscheiden, dass 1970-01-01 angibt, dass das Kennwort nie geändert wurde. Dann muss sich die Logik des restlichen Programms nicht kümmern.
Bent
3
Sie könnten vergessen anzurufen, isPasswordChangedgenauso wie Sie vergessen können, zu überprüfen, ob es null ist. Ich sehe hier nichts gewonnen.
Gherman
9
@Gherman Wenn der Konsument nicht das Konzept bekommt, dass null bedeutsam ist oder dass er prüfen muss, ob der Wert vorhanden ist, ist er verloren, aber wenn die Methode verwendet wird, ist es für jeden, der den Code liest, klar, warum ist eine Überprüfung und es besteht eine vernünftige Wahrscheinlichkeit, dass der Wert null / nicht vorhanden ist. Ansonsten ist unklar, ob es sich nur um einen Entwickler handelte, der eine Nullprüfung hinzufügte, nur "weil warum nicht" oder ob dies Teil des Konzepts ist. Sicher, man kann es herausfinden, aber so, wie man die Informationen direkt hat, muss man auch in die Implementierung schauen.
Frank Hopkins
2
@FrankHopkins: Guter Punkt, aber wenn Parallelität verwendet wird, kann sogar die Überprüfung einer einzelnen Variablen eine Sperre erfordern.
Christian Hackl
63

nullssind nicht böse Verwenden Sie sie ohne nachzudenken ist. Dies ist ein Fall, in dem nullgenau die richtige Antwort ist - es gibt kein Datum.

Beachten Sie, dass Ihre Lösung mehr Probleme verursacht. Was bedeutet es, wenn das Datum auf etwas gesetzt wird, aber das isPasswordChangedist falsch? Sie haben gerade einen Fall von widersprüchlichen Informationen erstellt, den Sie speziell erfassen und behandeln müssen, während ein nullWert eine klar definierte, eindeutige Bedeutung hat und nicht mit anderen Informationen in Konflikt stehen kann.

Also nein, Ihre Lösung ist nicht besser. Unter Berücksichtigung einen nullWert ist der richtige Ansatz hier. Menschen, die behaupten, das nullsei immer böse, egal in welchem ​​Kontext, verstehen nicht, warum es das nullgibt.

Tom
quelle
17
Historisch gesehen nullexistiert es, weil Tony Hoare 1965 den Milliarden-Dollar-Fehler begangen hat. Leute, die behaupten, das nullsei "böse", vereinfachen das Thema natürlich zu stark, aber es ist eine gute Sache, dass moderne Sprachen oder Programmierstile es nullersetzen es mit dedizierten Optionstypen.
Christian Hackl
9
@ChristianHackl: Nur aus historischem Interesse - die Hoare sagen jemals, was die bessere praktische Alternative gewesen wäre (ohne zu einem völlig neuen Paradigma zu wechseln)?
AnoE
5
@AnoE: Interessante Frage. Ich weiß nicht, was Hoare dazu zu sagen hat, aber nullfreies Programmieren ist heutzutage in modernen, gut gestalteten Programmiersprachen oft Realität.
Christian Hackl
10
@ Tom Wat? In Sprachen wie C # und Java ist das DateTimeFeld ein Zeiger. Das ganze Konzept von nullmacht nur Sinn für Zeiger!
Todd Sewell
16
@Tom: Nullzeiger sind nur für Sprachen und Implementierungen gefährlich, mit denen sie blind indiziert und dereferenziert werden können. In einer Sprache, die versucht, einen Nullzeiger zu indizieren oder zu dereferenzieren, ist dies häufig weniger gefährlich als ein Zeiger auf ein Dummy-Objekt. Es gibt viele Situationen, in denen es vorzuziehen ist, ein Programm abnormal zu beenden, anstatt bedeutungslose Zahlen zu erzeugen, und auf Systemen, die diese abfangen, sind Nullzeiger das richtige Rezept dafür.
Supercat
29

Abhängig von Ihrer Programmiersprache gibt es möglicherweise gute Alternativen, z. B. einen optionalDatentyp. In C ++ (17) wäre dies std :: optional . Es kann entweder fehlen oder einen beliebigen Wert des zugrunde liegenden Datentyps haben.

dasmy
quelle
8
Es gibt auch Optionalin Java; Die Bedeutung einer Null Optionalliegt bei Ihnen ...
Matthieu M.
1
Kam hierher, um sich auch mit Optional zu verbinden. Ich würde dies als Typ in einer Sprache kodieren, die sie hatte.
Zipp
2
@FrankHopkins Es ist eine Schande, dass nichts in Java Sie am Schreiben hindert Optional<Date> lastPasswordChangeTime = null;, aber ich würde nicht einfach deswegen aufgeben. Stattdessen würde ich eine sehr feste Teamregel einführen, dass niemals optionale Werte zugewiesen werden dürfen null. Optional kauft man sich zu viele nette Features, um darauf einfach zu verzichten. Sie können ganz einfach Standardwerte zuweisen ( orElseoder mit Lazy Evaluation orElseGet:), Fehler werfen ( orElseThrow), Werte nullsicher transformieren ( map, flatmap) usw.
Alexander
5
@FrankHopkins Checking isPresentist meiner Erfahrung nach fast immer ein Codegeruch und ein ziemlich zuverlässiges Zeichen dafür, dass die Entwickler, die diese Optionen verwenden, "den Punkt verfehlen". In der Tat gibt es im Grunde keinen Vorteil gegenüber ref == null, aber es ist auch nicht etwas, das Sie oft verwenden sollten. Selbst wenn das Team instabil ist, kann ein statisches Analysetool das Gesetz festlegen, wie z. B. ein Linter oder FindBugs , die die meisten Fälle abfangen können.
Alexander
5
@FrankHopkins: Es kann sein, dass die Java-Community nur einen kleinen Paradigmenwechsel benötigt, der noch viele Jahre dauern kann. Wenn Sie bei Scala suchen, zum Beispiel unterstützt es , nullweil die Sprache Java zu 100% kompatibel ist, aber niemand nutzt es, und Option[T]ist ganz über den Platz, mit hervorragender Funktions-Programmierunterstützung wie map, flatMap, Pattern - Matching und so weiter, die geht weit, weit jenseits von == nullSchecks. Sie haben Recht, dass sich ein Optionstyp theoretisch wie ein glorifizierter Zeiger anhört, aber die Realität beweist, dass dieses Argument falsch ist.
Christian Hackl
11

Richtig mit null

Es gibt verschiedene Verwendungsmöglichkeiten null. Die gebräuchlichste und semantisch korrekte Methode besteht darin, sie zu verwenden, wenn Sie einen einzelnen Wert haben oder nicht . In diesem Fall ist ein Wert entweder gleich nulloder etwas Sinnvolles wie ein Datensatz aus einer Datenbank oder so.

In diesen Situationen verwenden Sie es dann meistens so (in Pseudocode):

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

doSomethingUseful(value);

Problem

Und es hat ein sehr großes Problem. Das Problem ist, dass doSomethingUsefulder Wert zum Zeitpunkt des Aufrufs möglicherweise noch nicht überprüft wurde null! Wenn dies nicht der Fall ist, stürzt das Programm wahrscheinlich ab. Und der Benutzer kann möglicherweise nicht einmal gute Fehlermeldungen sehen, die mit so etwas wie "schrecklicher Fehler: Wert gesucht, aber null!" (nach dem Update: obwohl es möglicherweise noch weniger informative Fehler gibt Segmentation fault. Core dumped., oder schlimmer noch, keine Fehler und falsche Manipulationen bei Null in einigen Fällen)

Das Vergessen, Schecks zu schreiben nullund mit nullSituationen umzugehen , ist ein äußerst häufiger Fehler . Aus diesem Grund nullsagte Tony Hoare, der im Jahr 2009 auf einer Softwarekonferenz namens QCon London erfunden hatte , dass er 1965 den Milliarden-Dollar-Fehler begangen habe: https://www.infoq.com/presentations/Null-References-The-Billion-Dollar- Fehler-Tony-Hoare

Das Problem vermeiden

Einige Technologien und Sprachen machen es nullunmöglich, die Überprüfung auf unterschiedliche Weise zu vergessen, wodurch die Anzahl der Fehler verringert wird.

Zum Beispiel hat Haskell die MaybeMonade anstelle von Nullen. Angenommen, dies DatabaseRecordist ein benutzerdefinierter Typ. In Haskell kann ein Wert vom Typ Maybe DatabaseRecordgleich Just <somevalue>oder gleich sein Nothing. Sie können es dann auf verschiedene Arten verwenden, aber unabhängig davon, wie Sie es verwenden, können Sie keine Operation anwenden, Nothingohne es zu wissen.

Diese Funktion heißt zum Beispiel zeroAsDefaultreturn xfür Just xund 0für Nothing:

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

Christian Hackl sagt, C ++ 17 und Scala haben ihre eigenen Wege. Vielleicht möchten Sie also herausfinden, ob Ihre Sprache so etwas enthält, und es verwenden.

Nullen sind immer noch weit verbreitet

Wenn Sie nichts Besseres haben, ist die Verwendung nullin Ordnung. Pass einfach weiter auf. Typdeklarationen in Funktionen helfen Ihnen sowieso etwas.

Auch das mag nicht sehr progressiv klingen, aber Sie sollten prüfen, ob Ihre Kollegen nulletwas anderes verwenden möchten . Sie sind möglicherweise konservativ und möchten aus bestimmten Gründen möglicherweise keine neuen Datenstrukturen verwenden. Zum Beispiel die Unterstützung älterer Sprachversionen. Solche Dinge sollten in den Codierungsstandards des Projekts deklariert und ordnungsgemäß mit dem Team besprochen werden.

Auf Ihren Vorschlag

Sie schlagen vor, ein separates boolesches Feld zu verwenden. Aber Sie müssen es trotzdem überprüfen und können immer noch vergessen, es zu überprüfen. Hier gibt es also nichts zu gewinnen. Wenn Sie noch etwas anderes vergessen können, z. B. beide Werte jedes Mal aktualisieren, ist es noch schlimmer. Wenn das Problem des Vergessens, nach etwas zu suchen, nullnicht gelöst ist, hat es keinen Sinn. Vermeiden nullist schwierig und Sie sollten es nicht so machen, dass es noch schlimmer wird.

Wie man nicht null benutzt

Schließlich gibt es übliche Möglichkeiten, um nullfalsch zu verwenden . Eine Möglichkeit besteht darin, es anstelle von leeren Datenstrukturen wie Arrays und Strings zu verwenden. Ein leeres Array ist ein richtiges Array wie jedes andere! Es ist fast immer wichtig und nützlich, dass Datenstrukturen, die mehreren Werten entsprechen können, leer sein können, dh eine Länge von 0 haben.

Aus algebraischer Sicht ist eine leere Zeichenfolge für Zeichenfolgen ähnlich wie 0 für Zahlen, dh Identität:

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

Mit einer leeren Zeichenfolge können Zeichenfolgen im Allgemeinen zu einem Monoid werden: https://en.wikipedia.org/wiki/Monoid Wenn Sie sie nicht erhalten, ist sie für Sie nicht so wichtig.

Lassen Sie uns nun anhand dieses Beispiels sehen, warum es für die Programmierung wichtig ist:

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

Wenn wir hier ein leeres Array übergeben, funktioniert der Code einwandfrei. Es wird einfach nichts tun. Übergeben wir nullhier jedoch ein , kommt es wahrscheinlich zu einem Absturz mit der Fehlermeldung "Kann Null nicht durchlaufen, sorry". Wir könnten es einpacken, ifaber das ist weniger sauber, und Sie könnten vergessen, es zu überprüfen

Wie gehe ich mit null um?

Was doSomethingAboutIt()zu tun ist und insbesondere, ob eine Ausnahme ausgelöst werden soll, ist ein weiteres kompliziertes Problem. Kurz gesagt, es hängt davon ab, ob nullein akzeptabler Eingabewert für eine bestimmte Aufgabe vorhanden war und was als Antwort erwartet wird. Ausnahmen sind für Ereignisse, die nicht erwartet wurden. Ich werde nicht weiter auf dieses Thema eingehen. Diese Antwort ist schon sehr lang.

Gherman
quelle
5
Eigentlich horrible error: wanted value but got null!ist viel besser als die typischeren Segmentation fault. Core dumped....
Toby Speight
2
@TobySpeight Stimmt, aber ich würde etwas vorziehen, das in den Begriffen des Benutzers wie liegt Error: the product you want to buy is out of stock.
Gherman
Klar - ich habe nur beobachtet, dass es noch schlimmer sein könnte (und oft ist) . Ich stimme voll und ganz zu, was besser wäre! Und Ihre Antwort ist so ziemlich das, was ich zu dieser Frage gesagt hätte: +1.
Toby Speight
2
@Gherman: Übergeben, nullwo nullnicht erlaubt ist, ist ein Programmierfehler, dh ein Fehler im Code. Ein Produkt, das nicht auf Lager ist, ist Teil der normalen Geschäftslogik und kein Fehler. Sie können und sollten nicht versuchen, die Erkennung eines Fehlers in eine normale Nachricht auf der Benutzeroberfläche zu übersetzen. Sofort abstürzen und nicht mehr Code ausführen ist die bevorzugte Vorgehensweise, insbesondere wenn es sich um eine wichtige Anwendung handelt (z. B. eine, die echte Finanztransaktionen ausführt).
Christian Hackl
1
@EricDuminil Wir möchten die Möglichkeit, einen Fehler zu machen, beseitigen (oder verringern) und uns nullselbst nicht vollständig vom Hintergrund entfernen .
Gherman
4

Zusätzlich zu all den sehr guten Antworten, die zuvor gegeben wurden, möchte ich hinzufügen, dass Sie jedes Mal, wenn Sie versucht sind, ein Feld auf Null zu setzen, sorgfältig überlegen, ob es sich möglicherweise um einen Listentyp handelt. Ein nullfähiger Typ ist äquivalent eine Liste von 0 oder 1 Elementen, und oft kann dies auf eine Liste von N Elementen verallgemeinert werden. In diesem Fall möchten Sie möglicherweise lastChangePasswordTimeeine Liste von passwordChangeTimes.

Joel
quelle
Das ist ein ausgezeichneter Punkt. Dasselbe gilt häufig für Optionstypen. Eine Option kann als Sonderfall einer Sequenz angesehen werden.
Christian Hackl
2

Stellen Sie sich folgende Frage: Für welches Verhalten ist das Feld lastChangePasswordTime erforderlich?

Wenn Sie dieses Feld für eine Methode IsPasswordExpired () benötigen, um zu bestimmen, ob ein Mitglied aufgefordert werden soll, sein Kennwort von Zeit zu Zeit zu ändern, würde ich das Feld auf den Zeitpunkt festlegen, zu dem das Mitglied ursprünglich erstellt wurde. Die Implementierung von IsPasswordExpired () ist für neue und vorhandene Mitglieder identisch.

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;
   }
}

Wenn Sie eine separate Anforderung haben , dass neu erstellte Mitglieder haben ihr Passwort zu aktualisieren, würde ich hinzufügen , ein boolean Feld namens passwordShouldBeChanged getrennt und setzen Sie ihn auf true bei der Erstellung. Ich würde dann die Funktionalität der IsPasswordExpired () -Methode ändern, um eine Prüfung für dieses Feld einzuschließen (und die Methode in ShouldChangePassword umbenennen).

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;
   }
}

Machen Sie Ihre Absichten im Code deutlich.

Rik D
quelle
2

Erstens ist Nullen, die böse sind, ein Dogma, und wie bei Dogmen üblich, funktioniert es am besten als Richtlinie und nicht als Pass / No-Pass-Test.

Zweitens können Sie Ihre Situation so neu definieren, dass es sinnvoll ist, dass der Wert niemals null sein kann. InititialPasswordChanged ist ein Boolescher Wert, der anfänglich auf false festgelegt wurde. PasswordSetTime ist das Datum und die Uhrzeit, zu der das aktuelle Kennwort festgelegt wurde.

Beachten Sie, dass dies zwar mit geringen Kosten verbunden ist, Sie jedoch IMMER berechnen können, wie lange es her ist, seit ein Passwort zum letzten Mal festgelegt wurde.

jmoreno
quelle
1

Beide sind "sicher / gesund / korrekt", wenn der Anrufer vor der Verwendung prüft. Das Problem ist, was passiert, wenn der Anrufer nicht überprüft. Was ist besser, eine Variante von Null-Fehler oder die Verwendung eines ungültigen Werts?

Es gibt keine einzige richtige Antwort. Es kommt darauf an, worüber Sie sich Sorgen machen.

Wenn Abstürze wirklich schlimm sind, die Antwort jedoch nicht kritisch ist oder einen akzeptierten Standardwert hat, ist es möglicherweise besser, einen Booleschen Wert als Flag zu verwenden. Wenn die falsche Antwort ein schlimmeres Problem ist als ein Absturz, ist die Verwendung von Null besser.

In den meisten „typischen“ Fällen ist ein schneller Ausfall und das Erzwingen der Überprüfung durch die Anrufer der schnellste Weg, um einen vernünftigen Code zu erhalten. Daher sollte meines Erachtens die Standardeinstellung Null sein. Ich würde jedoch nicht zu viel Vertrauen in die "X ist die Wurzel aller bösen" Evangelisten setzen. Normalerweise haben sie nicht alle Anwendungsfälle vorweggenommen.

Einer
quelle