Beste Erklärung für Sprachen ohne Null

225

Von Zeit zu Zeit, wenn sich Programmierer über Nullfehler / Ausnahmen beschweren, fragt jemand, was wir ohne Null machen.

Ich habe eine grundlegende Vorstellung von der Coolness von Optionstypen, aber ich habe nicht die Kenntnisse oder Sprachkenntnisse, um sie am besten auszudrücken. Was ist eine großartige Erklärung für das Folgende, die auf eine Weise geschrieben wurde, die für den durchschnittlichen Programmierer zugänglich ist und auf die wir diese Person hinweisen könnten?

  • Die Unerwünschtheit, Referenzen / Zeiger zu haben, ist standardmäßig nullbar
  • Funktionsweise von Optionstypen, einschließlich Strategien zur Erleichterung der Überprüfung von Nullfällen wie z
    • Mustervergleich und
    • monadisches Verständnis
  • Alternative Lösung wie Nachricht essen Null
  • (andere Aspekte, die ich vermisst habe)
Roman A. Taycher
quelle
11
Wenn Sie dieser Frage Tags für die Funktionsprogrammierung oder F # hinzufügen, erhalten Sie mit Sicherheit einige fantastische Antworten.
Stephen Swensen
Ich habe ein funktionales Programmier-Tag hinzugefügt, da der Optionstyp aus der ml-Welt stammt. Ich möchte es lieber nicht mit F # markieren (zu spezifisch). Übrigens muss jemand mit Taxonomie-Fähigkeiten Tags vom Typ Vielleicht oder Option hinzufügen.
Roman A. Taycher
4
Ich vermute, dass solche spezifischen Tags kaum benötigt werden. Die Tags dienen hauptsächlich dazu, Personen das Auffinden relevanter Fragen zu ermöglichen (z. B. "Fragen, über die ich viel weiß und die ich beantworten kann", und "funktionale Programmierung" ist dort sehr hilfreich. Aber so etwas wie "null" oder " Optionstyp "sind viel weniger nützlich. Nur wenige Leute überwachen wahrscheinlich ein" Optionstyp "-Tag auf der Suche nach Fragen, die sie beantworten können .;)
Jalf
Vergessen wir nicht, dass einer der Hauptgründe für Null darin besteht, dass sich Computer stark an die Mengenlehre gebunden entwickelt haben. Null ist eine der wichtigsten Mengen in der gesamten Mengenlehre. Ohne sie würden ganze Algorithmen zusammenbrechen. Führen Sie beispielsweise eine Zusammenführungssortierung durch. Dabei wird eine Liste mehrmals in zwei Hälften geteilt. Was ist, wenn die Liste 7 Elemente lang ist? Zuerst teilen Sie es in 4 und 3. Dann 2, 2, 2 und 1. Dann 1, 1, 1, 1, 1, 1, 1 und ... null! Null hat einen Zweck, nur einen, den Sie praktisch nicht sehen. Es existiert mehr für den theoretischen Bereich.
Stevendesu
6
@steven_desu - Ich bin anderer Meinung. In 'nullbaren' Sprachen können Sie einen Verweis auf eine leere Liste [] sowie einen Verweis auf eine Nullliste haben. Diese Frage bezieht sich auf die Verwechslung zwischen den beiden.
Stusmith

Antworten:

433

Ich denke, die kurze Zusammenfassung, warum Null unerwünscht ist, ist, dass bedeutungslose Zustände nicht darstellbar sein sollten .

Angenommen, ich modelliere eine Tür. Es kann sich in einem von drei Zuständen befinden: Öffnen, Schließen, aber Entsperren und Schließen und Sperren. Jetzt konnte ich es nach dem Vorbild von modellieren

class Door
    private bool isShut
    private bool isLocked

und es ist klar, wie ich meine drei Zustände diesen beiden booleschen Variablen zuordnen kann. Damit bleibt jedoch ein vierter, unerwünschter Zustand verfügbar : isShut==false && isLocked==true. Da die Typen, die ich als meine Darstellung ausgewählt habe, diesen Zustand zulassen, muss ich mich mental anstrengen, um sicherzustellen, dass die Klasse niemals in diesen Zustand gelangt (möglicherweise durch explizite Codierung einer Invariante). Im Gegensatz dazu, wenn ich eine Sprache mit algebraischen Datentypen oder überprüften Aufzählungen verwendet habe, mit denen ich definieren kann

type DoorState =
    | Open | ShutAndUnlocked | ShutAndLocked

dann könnte ich definieren

class Door
    private DoorState state

und es gibt keine Sorgen mehr. Das Typsystem stellt sicher, dass nur drei mögliche Zustände für eine Instanz class Doorvorhanden sind. Dies ist der Typ, in dem Typsysteme gut sind - indem eine ganze Klasse von Fehlern beim Kompilieren explizit ausgeschlossen wird.

Das Problem dabei nullist, dass jeder Referenztyp diesen zusätzlichen Status in seinem Bereich erhält, der normalerweise unerwünscht ist. Eine stringVariable kann eine beliebige Folge von Zeichen sein, oder es kann sich um diesen verrückten Zusatzwert handeln null, der nicht in meine Problemdomäne passt. Ein TriangleObjekt hat drei Points, die selbst Xund YWerte haben, aber leider kann das Points oder das Triangleselbst dieser verrückte Nullwert sein, der für die Grafikdomäne, in der ich arbeite, bedeutungslos ist.

Wenn Sie beabsichtigen, einen möglicherweise nicht vorhandenen Wert zu modellieren, sollten Sie sich explizit dafür entscheiden. Wenn ich beabsichtige, Menschen zu modellieren, dass jeder Personein FirstNameund ein hat LastName, aber nur einige Menschen ein MiddleNames haben, dann möchte ich so etwas sagen

class Person
    private string FirstName
    private Option<string> MiddleName
    private string LastName

wobei stringhier angenommen wird, dass es sich um einen nicht nullbaren Typ handelt. Dann sind keine kniffligen Invarianten zu ermitteln und keine unerwarteten NullReferenceExceptions, wenn versucht wird, die Länge des Namens einer Person zu berechnen. Das Typsystem stellt sicher, dass jeder Code, der sich mit den MiddleNameKonten befasst, die Möglichkeit hat, dass er vorhanden ist None, während jeder Code, der sich mit den Konten befasst, FirstNamesicher davon ausgehen kann, dass dort ein Wert vorhanden ist.

Mit dem obigen Typ könnten wir beispielsweise diese dumme Funktion erstellen:

let TotalNumCharsInPersonsName(p:Person) =
    let middleLen = match p.MiddleName with
                    | None -> 0
                    | Some(s) -> s.Length
    p.FirstName.Length + middleLen + p.LastName.Length

ohne Sorgen. Im Gegensatz dazu wird in einer Sprache mit nullbaren Referenzen für Typen wie Zeichenfolge angenommen

class Person
    private string FirstName
    private string MiddleName
    private string LastName

Am Ende verfassen Sie Dinge wie

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length + p.MiddleName.Length + p.LastName.Length

Dies geht in die Luft, wenn das Objekt der eingehenden Person nicht die Invariante hat, dass alles nicht null ist, oder

let TotalNumCharsInPersonsName(p:Person) =
    (if p.FirstName=null then 0 else p.FirstName.Length)
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + (if p.LastName=null then 0 else p.LastName.Length)

oder vielleicht

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + p.LastName.Length

Angenommen, dies pstellt sicher , dass zuerst / zuletzt vorhanden ist, aber die Mitte kann null sein, oder Sie führen Überprüfungen durch, die verschiedene Arten von Ausnahmen auslösen, oder wer weiß was. All diese verrückten Implementierungsoptionen und Dinge, über die man nachdenken sollte, tauchen auf, weil es diesen dummen darstellbaren Wert gibt, den man nicht will oder braucht.

Null fügt normalerweise unnötige Komplexität hinzu. Komplexität ist der Feind aller Software, und Sie sollten sich bemühen, die Komplexität zu reduzieren, wann immer dies sinnvoll ist.

(Beachten Sie auch, dass selbst diese einfachen Beispiele komplexer sind. Auch wenn a FirstNamenicht sein kann null, stringkann a ""(die leere Zeichenfolge) darstellen, was wahrscheinlich auch kein Personenname ist, den wir modellieren möchten. Bei nullbaren Zeichenfolgen kann es dennoch vorkommen, dass wir "bedeutungslose Werte darstellen". Auch hier können Sie dies entweder über Invarianten und bedingten Code zur Laufzeit oder mithilfe des Typsystems (z. B. um einen NonEmptyStringTyp zu haben ) bekämpfen Letzteres ist vielleicht schlecht beraten ("gute" Typen werden oft über eine Reihe gemeinsamer Operationen "geschlossen" und z. B. NonEmptyStringnicht geschlossen.SubString(0,0)), aber es zeigt mehr Punkte im Designraum. Letztendlich gibt es in einem bestimmten Typsystem eine gewisse Komplexität, die sehr gut beseitigt werden kann, und eine andere Komplexität, die nur an sich schwieriger zu beseitigen ist. Der Schlüssel für dieses Thema ist, dass in fast jedem Typsystem die Änderung von "standardmäßig nullfähige Referenzen" zu "standardmäßig nicht nullfähige Referenzen" fast immer eine einfache Änderung ist, die das Typensystem im Kampf gegen Komplexität und erheblich verbessert bestimmte Arten von Fehlern und bedeutungslosen Zuständen ausschließen. Es ist also ziemlich verrückt, dass so viele Sprachen diesen Fehler immer wieder wiederholen.)

Brian
quelle
31
Re: Namen - In der Tat. Und vielleicht ist es Ihnen wichtig, eine Tür zu modellieren, die offen hängt, aber mit dem Riegel des Schlosses herausragt und verhindert, dass sich die Tür schließt. Es gibt viel Komplexität auf der Welt. Der Schlüssel ist nicht hinzuzufügen , mehr Komplexität , wenn die Zuordnung zwischen „Weltstaat“ und „Programmzuständen“ in Ihrer Software zu implementieren.
Brian
59
Was, du hast noch nie Türen geöffnet?
Joshua
58
Ich verstehe nicht, warum sich Leute über die Semantik einer bestimmten Domäne aufregen. Brian hat die Fehler auf prägnante und einfache Weise mit null dargestellt. Ja, er hat die Problemdomäne in seinem Beispiel vereinfacht, indem er sagte, dass jeder Vor- und Nachnamen hat. Die Frage wurde mit einem 'T' beantwortet, Brian - wenn du jemals in Boston bist, schulde ich dir ein Bier für all die Beiträge, die du hier machst!
Akaphenom
67
@akaphenom: danke, aber beachte, dass nicht alle Leute Bier trinken (ich bin ein Nichttrinker). Aber ich weiß es zu schätzen, dass Sie nur ein vereinfachtes Modell der Welt verwenden, um Dankbarkeit zu kommunizieren, sodass ich nicht mehr über die fehlerhaften Annahmen Ihres Weltmodells streiten werde. : P (So viel Komplexität in der realen Welt! :))
Brian
4
Seltsamerweise gibt es 3-Staaten-Türen auf dieser Welt! Sie werden in einigen Hotels als Toilettentüren verwendet. Ein Druckknopf fungiert von innen als Schlüssel, der die Tür von außen verriegelt. Es wird automatisch entriegelt, sobald sich der Riegel bewegt.
Comonad
65

Das Schöne an Optionstypen ist nicht, dass sie optional sind. Es ist so, dass alle anderen Typen es nicht sind .

Manchmal müssen wir in der Lage sein, eine Art "Null" -Zustand darzustellen. Manchmal müssen wir eine Option "kein Wert" sowie die anderen möglichen Werte darstellen, die eine Variable annehmen kann. Eine Sprache, die dies nicht zulässt, wird ein bisschen verkrüppelt sein.

Aber oft brauchen wir es nicht und das Zulassen eines solchen "Null" -Zustands führt nur zu Mehrdeutigkeit und Verwirrung: Jedes Mal, wenn ich auf eine Referenztypvariable in .NET zugreife, muss ich berücksichtigen, dass sie möglicherweise Null ist .

Oft wird es nie tatsächlich null sein, weil die Programmierer Strukturen den Code , so dass es nie passieren können. Der Compiler kann dies jedoch nicht überprüfen, und jedes Mal, wenn Sie es sehen, müssen Sie sich fragen: "Kann dies null sein? Muss ich hier nach null suchen?"

Idealerweise sollte dies in den vielen Fällen, in denen null keinen Sinn ergibt, nicht zulässig sein .

Dies ist in .NET schwierig zu erreichen, wo fast alles null sein kann. Sie müssen sich darauf verlassen, dass der Autor des Codes, den Sie aufrufen, 100% diszipliniert und konsistent ist und klar dokumentiert hat, was null sein kann und was nicht, oder Sie müssen paranoid sein und alles überprüfen .

Wenn Typen jedoch standardmäßig nicht nullwertfähig sind , müssen Sie nicht überprüfen, ob sie nullwert sind. Sie wissen, dass sie niemals null sein können, da der Compiler / Typprüfer dies für Sie erzwingt.

Und dann brauchen wir nur noch eine Hintertür für die seltenen Fälle , in denen wir tun müssen einen Null - Zustand zu behandeln. Dann kann ein "Optionstyp" verwendet werden. Dann erlauben wir null in den Fällen, in denen wir eine bewusste Entscheidung getroffen haben, dass wir in der Lage sein müssen, den Fall "kein Wert" darzustellen, und in jedem anderen Fall wissen wir, dass der Wert niemals null sein wird.

Wie andere bereits erwähnt haben, kann null in C # oder Java eines von zwei Dingen bedeuten:

  1. Die Variable ist nicht initialisiert. Dies sollte im Idealfall niemals passieren. Eine Variable sollte nur existieren, wenn sie initialisiert ist.
  2. Die Variable enthält einige "optionale" Daten: Sie muss in der Lage sein, den Fall darzustellen, in dem keine Daten vorhanden sind . Dies ist manchmal notwendig. Vielleicht versuchen Sie, ein Objekt in einer Liste zu finden, und Sie wissen nicht im Voraus, ob es dort ist oder nicht. Dann müssen wir darstellen können, dass "kein Objekt gefunden wurde".

Die zweite Bedeutung muss erhalten bleiben, die erste sollte jedoch vollständig beseitigt werden. Und selbst die zweite Bedeutung sollte nicht die Standardeinstellung sein. Es ist etwas, für das wir uns entscheiden können, wenn und wann wir es brauchen . Wenn wir jedoch nicht möchten, dass etwas optional ist, möchten wir, dass die Typprüfung garantiert, dass sie niemals null ist.

jalf
quelle
Und in der zweiten Bedeutung möchten wir, dass der Compiler uns warnt (stoppt?), Wenn wir versuchen, auf solche Variablen zuzugreifen, ohne vorher auf Null zu prüfen. Hier ist ein großartiger Artikel über die bevorstehende null / nicht null C # -Funktion (endlich!) Blogs.msdn.microsoft.com/dotnet/2017/11/15/…
Ohad Schneider
44

Alle bisherigen Antworten konzentrieren sich darauf, warum dies nulleine schlechte Sache ist und wie praktisch es ist, wenn eine Sprache garantieren kann, dass bestimmte Werte niemals null sind.

Anschließend schlagen sie vor, dass es eine ziemlich gute Idee wäre, wenn Sie die Nicht-Null-Fähigkeit für alle Werte erzwingen. Dies ist möglich, wenn Sie ein Konzept hinzufügen Optionoder MaybeTypen darstellen, die möglicherweise nicht immer einen definierten Wert haben. Dies ist der Ansatz von Haskell.

Es ist alles gutes Zeug! Es schließt jedoch nicht aus, explizit nullbare / nicht null-Typen zu verwenden, um den gleichen Effekt zu erzielen. Warum ist Option dann immer noch eine gute Sache? Schließlich unterstützt Scala nullfähige Werte ( muss , damit es mit Java-Bibliotheken funktionieren kann), unterstützt Optionsaber auch.

Frage : Was sind die Vorteile jenseits der Lage, nulls von einer Sprache vollständig zu entfernen?

A. Zusammensetzung

Wenn Sie eine naive Übersetzung aus nullbewusstem Code erstellen

def fullNameLength(p:Person) = {
  val middleLen =
    if (null == p.middleName)
      p.middleName.length
    else
      0
  p.firstName.length + middleLen + p.lastName.length
}

zu optionsbewusstem Code

def fullNameLength(p:Person) = {
  val middleLen = p.middleName match {
    case Some(x) => x.length
    case _ => 0
  }
  p.firstName.length + middleLen + p.lastName.length
}

Es gibt keinen großen Unterschied! Aber es ist auch eine schreckliche Art, Optionen zu verwenden ... Dieser Ansatz ist viel sauberer:

def fullNameLength(p:Person) = {
  val middleLen = p.middleName map {_.length} getOrElse 0
  p.firstName.length + middleLen + p.lastName.length
}

Oder auch:

def fullNameLength(p:Person) =       
  p.firstName.length +
  p.middleName.map{length}.getOrElse(0) +
  p.lastName.length

Wenn Sie anfangen, sich mit der Liste der Optionen zu befassen, wird es noch besser. Stellen Sie sich vor, die Liste peopleselbst ist optional:

people flatMap(_ find (_.firstName == "joe")) map (fullNameLength)

Wie funktioniert das?

//convert an Option[List[Person]] to an Option[S]
//where the function f takes a List[Person] and returns an S
people map f

//find a person named "Joe" in a List[Person].
//returns Some[Person], or None if "Joe" isn't in the list
validPeopleList find (_.firstName == "joe")

//returns None if people is None
//Some(None) if people is valid but doesn't contain Joe
//Some[Some[Person]] if Joe is found
people map (_ find (_.firstName == "joe")) 

//flatten it to return None if people is None or Joe isn't found
//Some[Person] if Joe is found
people flatMap (_ find (_.firstName == "joe")) 

//return Some(length) if the list isn't None and Joe is found
//otherwise return None
people flatMap (_ find (_.firstName == "joe")) map (fullNameLength)

Der entsprechende Code mit Nullprüfungen (oder sogar elvis ?: Operatoren) wäre schmerzhaft lang. Der eigentliche Trick dabei ist die flatMap-Operation, die das verschachtelte Verständnis von Optionen und Sammlungen auf eine Weise ermöglicht, die nullbare Werte niemals erreichen können.

Kevin Wright
quelle
8
+1, das ist ein guter Punkt, den man hervorheben sollte. Ein Nachtrag: Drüben in Haskell-Land flatMapwürde man (>>=)den "Bind" -Operator für Monaden nennen. Das stimmt, Haskeller mögen es flatMapso sehr, Dinge anzupingen, dass wir sie in das Logo unserer Sprache einfügen .
CA McCann
1
+1 Hoffentlich würde ein Ausdruck von Option<T>niemals null sein. Leider ist Scala äh, immer noch mit Java verbunden :-) (Wenn Scala andererseits nicht gut mit Java spielen würde, wer würde es verwenden? Oo)
Einfach genug: 'List (null) .headOption'. Beachten Sie, dass dies eine ganz andere Sache bedeutet als ein Rückgabewert von 'Keine'
Kevin Wright
4
Ich habe dir Kopfgeld gegeben, da mir das, was du über Komposition gesagt hast, wirklich gefällt, was andere Leute nicht zu erwähnen schienen.
Roman A. Taycher
Hervorragende Antwort mit tollen Beispielen!
thSoft
38

Da scheinen die Leute es zu vermissen: nullist mehrdeutig.

Alices Geburtsdatum ist null. Was heißt das?

Bobs Todesdatum ist null. Was bedeutet das?

Eine "vernünftige" Interpretation könnte sein, dass Alices Geburtsdatum existiert, aber unbekannt ist, während Bobs Todesdatum nicht existiert (Bob lebt noch). Aber warum haben wir unterschiedliche Antworten bekommen?


Ein weiteres Problem: nullist ein Randfall.

  • Ist null = null?
  • Ist nan = nan?
  • Ist inf = inf?
  • Ist +0 = -0?
  • Ist +0/0 = -0/0?

Die Antworten lauten normalerweise "Ja", "Nein", "Ja", "Ja", "Nein" bzw. "Ja". Verrückte "Mathematiker" nennen NaN "Nichtigkeit" und sagen, dass es mit sich selbst vergleichbar ist. SQL behandelt Nullen als ungleich (also verhalten sie sich wie NaNs). Man fragt sich, was passiert, wenn Sie versuchen, ± ∞, ± 0 und NaNs in derselben Datenbankspalte zu speichern (es gibt 2 53 NaNs, von denen die Hälfte "negativ" ist).

Um die Sache noch schlimmer zu machen, unterscheiden sich Datenbanken darin, wie sie NULL behandeln, und die meisten von ihnen sind nicht konsistent ( eine Übersicht finden Sie unter NULL-Behandlung in SQLite ). Es ist ziemlich schrecklich.


Und nun zur obligatorischen Geschichte:

Ich habe kürzlich eine (sqlite3) Datenbanktabelle mit fünf Spalten entworfen a NOT NULL, b, id_a, id_b NOT NULL, timestamp. Da es sich um ein generisches Schema handelt, mit dem ein generisches Problem für ziemlich beliebige Apps gelöst werden soll, gibt es zwei Eindeutigkeitsbeschränkungen:

UNIQUE(a, b, id_a)
UNIQUE(a, b, id_b)

id_aexistiert nur aus Kompatibilitätsgründen mit einem vorhandenen App-Design (teilweise weil ich keine bessere Lösung gefunden habe) und wird in der neuen App nicht verwendet. Da NULL der Art und Weise in SQL arbeitet, kann ich einfügen (1, 2, NULL, 3, t)und (1, 2, NULL, 4, t)und nicht verletzen die erste Eindeutigkeitsbedingung (da (1, 2, NULL) != (1, 2, NULL)).

Dies funktioniert speziell aufgrund der Funktionsweise von NULL in einer Eindeutigkeitsbeschränkung für die meisten Datenbanken (vermutlich, um "reale" Situationen einfacher zu modellieren, z. B. können keine zwei Personen dieselbe Sozialversicherungsnummer haben, aber nicht alle Personen haben eine).


FWIW, ohne zuerst undefiniertes Verhalten aufzurufen, können C ++ - Referenzen nicht auf null "zeigen", und es ist nicht möglich, eine Klasse mit nicht initialisierten Referenzmitgliedsvariablen zu erstellen (wenn eine Ausnahme ausgelöst wird, schlägt die Erstellung fehl).

Nebenbemerkung: Gelegentlich möchten Sie möglicherweise sich gegenseitig ausschließende Zeiger (dh nur einer von ihnen kann nicht NULL sein), z. B. in einem hypothetischen iOS type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed. Stattdessen bin ich gezwungen, Dinge wie zu tun assert((bool)actionSheet + (bool)alertView == 1).

tc.
quelle
Tatsächliche Mathematiker verwenden das Konzept von "NaN" jedoch nicht, seien Sie versichert.
Noldorin
@Noldorin: Sie tun es, aber sie verwenden den Begriff "unbestimmte Form".
IJ Kennedy
@IJKennedy: Das ist ein anderes College, das ich ganz gut kenne, danke. Einige NaNs mögen eine unbestimmte Form darstellen, aber da FPA kein symbolisches Denken betreibt, ist es ziemlich irreführend, sie mit einer unbestimmten Form gleichzusetzen!
Noldorin
Was ist los mit assert(actionSheet ^ alertView)? Oder kann deine Sprache XOR nicht bools?
Katze
16

Die Unerwünschtheit, Referenzen / Zeiger zu haben, ist standardmäßig nullbar.

Ich denke nicht, dass dies das Hauptproblem bei Nullen ist, das Hauptproblem bei Nullen ist, dass sie zwei Dinge bedeuten können:

  1. Die Referenz / der Zeiger ist nicht initialisiert: Das Problem ist hier dasselbe wie die Veränderlichkeit im Allgemeinen. Zum einen macht es die Analyse Ihres Codes schwieriger.
  2. Die Variable null bedeutet tatsächlich etwas: Dies ist der Fall, den Optionstypen tatsächlich formalisieren.

Sprachen, die Optionstypen unterstützen, verbieten oder raten normalerweise auch von der Verwendung nicht initialisierter Variablen.

Funktionsweise von Optionstypen, einschließlich Strategien zur Erleichterung der Überprüfung von Nullfällen, z. B. Musterabgleich.

Um effektiv zu sein, müssen Optionstypen direkt in der Sprache unterstützt werden. Andernfalls ist viel Kesselplattencode erforderlich, um sie zu simulieren. Mustervergleich und Typinferenz sind zwei wichtige Sprachfunktionen, mit denen Optionstypen einfach zu bearbeiten sind. Beispielsweise:

In F #:

//first we create the option list, and then filter out all None Option types and 
//map all Some Option types to their values.  See how type-inference shines.
let optionList = [Some(1); Some(2); None; Some(3); None]
optionList |> List.choose id //evaluates to [1;2;3]

//here is a simple pattern-matching example
//which prints "1;2;None;3;None;".
//notice how value is extracted from op during the match
optionList 
|> List.iter (function Some(value) -> printf "%i;" value | None -> printf "None;")

In einer Sprache wie Java ohne direkte Unterstützung für Optionstypen hätten wir jedoch Folgendes:

//here we perform the same filter/map operation as in the F# example.
List<Option<Integer>> optionList = Arrays.asList(new Some<Integer>(1),new Some<Integer>(2),new None<Integer>(),new Some<Integer>(3),new None<Integer>());
List<Integer> filteredList = new ArrayList<Integer>();
for(Option<Integer> op : list)
    if(op instanceof Some)
        filteredList.add(((Some<Integer>)op).getValue());

Alternative Lösung wie Nachricht essen Null

Die "Nachricht, die nichts isst" von Objective-C ist weniger eine Lösung als vielmehr ein Versuch, den Kopfschmerz der Nullprüfung zu lindern. Anstatt beim Versuch, eine Methode für ein Nullobjekt aufzurufen, eine Laufzeitausnahme auszulösen, wird der Ausdruck grundsätzlich selbst als Null ausgewertet. Wenn man den Unglauben aufhebt, ist es so, als ob jede Instanzmethode mit beginnt if (this == null) return null;. Aber dann gibt es einen Informationsverlust: Sie wissen nicht, ob die Methode null zurückgegeben hat, weil sie ein gültiger Rückgabewert ist oder weil das Objekt tatsächlich null ist. Es ähnelt dem Schlucken von Ausnahmen und macht keine Fortschritte bei der Behebung der zuvor beschriebenen Probleme mit Null.

Stephen Swensen
quelle
Dies ist ein Pet Peeve, aber c # ist kaum eine c-ähnliche Sprache.
Roman A. Taycher
4
Ich habe mich hier für Java entschieden, da C # wahrscheinlich eine bessere Lösung hätte ... aber ich schätze Ihren Ärger. Was die Leute wirklich meinen, ist "eine Sprache mit C-inspirierter Syntax". Ich ging voran und ersetzte die "c-like" -Anweisung.
Stephen Swensen
Mit linq, richtig. Ich dachte an c # und bemerkte das nicht.
Roman A. Taycher
1
Ja, meistens mit c-inspirierter Syntax, aber ich glaube, ich habe auch von imperativen Programmiersprachen wie Python / Ruby gehört, die nur sehr wenig von c-ähnlicher Syntax enthalten, die von funktionalen Programmierern als c-like bezeichnet wird.
Roman A. Taycher
11

Die Versammlung brachte uns Adressen, die auch als untypisierte Zeiger bekannt sind. C ordnete sie direkt als typisierte Zeiger zu, führte jedoch Algols Null als eindeutigen Zeigerwert ein, der mit allen typisierten Zeigern kompatibel ist. Das große Problem mit null in C ist, dass jeder Zeiger ohne manuelle Überprüfung sicher verwendet werden kann, da jeder Zeiger null sein kann.

In höheren Sprachen ist es umständlich, Null zu haben, da es wirklich zwei unterschiedliche Begriffe vermittelt:

  • Zu sagen, dass etwas undefiniert ist .
  • Zu sagen, dass etwas optional ist .

Undefinierte Variablen zu haben ist so gut wie nutzlos und führt zu undefiniertem Verhalten, wann immer sie auftreten. Ich nehme an, jeder wird zustimmen, dass undefinierte Dinge um jeden Preis vermieden werden sollten.

Der zweite Fall ist die Optionalität und wird am besten explizit bereitgestellt, beispielsweise mit einem Optionstyp .


Angenommen, wir sind in einem Transportunternehmen und müssen eine Anwendung erstellen, um einen Zeitplan für unsere Fahrer zu erstellen. Für jeden Fahrer speichern wir einige Informationen wie: den Führerschein und die Telefonnummer, die im Notfall angerufen werden kann.

In C könnten wir haben:

struct PhoneNumber { ... };
struct MotorbikeLicence { ... };
struct CarLicence { ... };
struct TruckLicence { ... };

struct Driver {
  char name[32]; /* Null terminated */
  struct PhoneNumber * emergency_phone_number;
  struct MotorbikeLicence * motorbike_licence;
  struct CarLicence * car_licence;
  struct TruckLicence * truck_licence;
};

Wie Sie sehen, müssen wir bei jeder Verarbeitung über unsere Treiberliste nach Nullzeigern suchen. Der Compiler wird Ihnen nicht helfen, die Sicherheit des Programms hängt von Ihren Schultern ab.

In OCaml würde derselbe Code folgendermaßen aussehen:

type phone_number = { ... }
type motorbike_licence = { ... }
type car_licence = { ... }
type truck_licence = { ... }

type driver = {
  name: string;
  emergency_phone_number: phone_number option;
  motorbike_licence: motorbike_licence option;
  car_licence: car_licence option;
  truck_licence: truck_licence option;
}

Nehmen wir jetzt an, wir möchten die Namen aller Fahrer zusammen mit ihren LKW-Lizenznummern drucken.

In C:

#include <stdio.h>

void print_driver_with_truck_licence_number(struct Driver * driver) {
  /* Check may be redundant but better be safe than sorry */
  if (driver != NULL) {
    printf("driver %s has ", driver->name);
    if (driver->truck_licence != NULL) {
      printf("truck licence %04d-%04d-%08d\n",
        driver->truck_licence->area_code
        driver->truck_licence->year
        driver->truck_licence->num_in_year);
    } else {
      printf("no truck licence\n");
    }
  }
}

void print_drivers_with_truck_licence_numbers(struct Driver ** drivers, int nb) {
  if (drivers != NULL && nb >= 0) {
    int i;
    for (i = 0; i < nb; ++i) {
      struct Driver * driver = drivers[i];
      if (driver) {
        print_driver_with_truck_licence_number(driver);
      } else {
        /* Huh ? We got a null inside the array, meaning it probably got
           corrupt somehow, what do we do ? Ignore ? Assert ? */
      }
    }
  } else {
    /* Caller provided us with erroneous input, what do we do ?
       Ignore ? Assert ? */
  }
}

In OCaml wäre das:

open Printf

(* Here we are guaranteed to have a driver instance *)
let print_driver_with_truck_licence_number driver =
  printf "driver %s has " driver.name;
  match driver.truck_licence with
    | None ->
        printf "no truck licence\n"
    | Some licence ->
        (* Here we are guaranteed to have a licence *)
        printf "truck licence %04d-%04d-%08d\n"
          licence.area_code
          licence.year
          licence.num_in_year

(* Here we are guaranteed to have a valid list of drivers *)
let print_drivers_with_truck_licence_numbers drivers =
  List.iter print_driver_with_truck_licence_number drivers

Wie Sie in diesem trivialen Beispiel sehen können, ist die sichere Version nicht kompliziert:

  • Es ist kurz.
  • Sie erhalten viel bessere Garantien und es ist überhaupt keine Nullprüfung erforderlich.
  • Der Compiler hat sichergestellt, dass Sie mit der Option richtig umgegangen sind

Während in C man einfach einen Nullcheck und einen Boom vergessen hätte ...

Hinweis: Diese Codebeispiele wurden nicht kompiliert, aber ich hoffe, Sie haben die Ideen.

bltxd
quelle
Ich habe es nie versucht, aber en.wikipedia.org/wiki/Cyclone_%28programming_language%29 behauptet, Nicht-Null-Zeiger für c zuzulassen.
Roman A. Taycher
1
Ich bin mit Ihrer Aussage nicht einverstanden, dass sich niemand für den ersten Fall interessiert. Viele Menschen, insbesondere in den funktionalen Sprachgemeinschaften, sind sehr daran interessiert und entmutigen oder verbieten die Verwendung nicht initialisierter Variablen.
Stephen Swensen
Ich glaube, NULLwie in "Referenz, die auf nichts verweist" für eine Algol-Sprache erfunden wurde (Wikipedia stimmt zu, siehe en.wikipedia.org/wiki/Null_pointer#Null_pointer ). Aber natürlich ist es wahrscheinlich, dass Assembly-Programmierer ihre Zeiger auf eine ungültige Adresse initialisiert haben (lesen Sie: Null = 0).
1
@ Stephen: Wir haben wahrscheinlich das Gleiche gemeint. Für mich entmutigen oder verbieten sie die Verwendung von nicht initialisierten Dingen, gerade weil es keinen Sinn macht, undefinierte Dinge zu diskutieren, da wir mit ihnen nichts Vernünftiges oder Nützliches anfangen können. Es hätte überhaupt kein Interesse.
Bltxd
2
als @tc. sagt, null hat nichts mit Montage zu tun. In der Assembly sind Typen im Allgemeinen nicht nullwertfähig. Ein in ein Universalregister geladener Wert kann Null oder eine Ganzzahl ungleich Null sein. Aber es kann niemals null sein. Selbst wenn Sie eine Speicheradresse in ein Register laden, gibt es auf den meisten gängigen Architekturen keine separate Darstellung des "Nullzeigers". Das ist ein Konzept, das in höheren Sprachen eingeführt wurde, wie C.
Jalf
5

Microsoft Research hat ein interessantes Projekt namens

Spec #

Es handelt sich um eine C # -Erweiterung mit dem Typ "Nicht null" und einem Mechanismus, mit dem überprüft wird, ob Ihre Objekte nicht null sind. IMHO ist die Anwendung des Vertrags nach dem Vertragsprinzip jedoch für viele problematische Situationen, die durch Nullreferenzen verursacht werden, geeigneter und hilfreicher.

Jahan
quelle
4

Aus dem .NET-Hintergrund kommend dachte ich immer, null hätte einen Punkt, es ist nützlich. Bis ich von Strukturen erfuhr und wie einfach es war, mit ihnen zu arbeiten, ohne viel Code auf der Kesselplatte zu vermeiden. Tony Hoare, der 2009 auf der QCon London sprach, entschuldigte sich für die Erfindung der Nullreferenz . Um ihn zu zitieren:

Ich nenne es meinen Milliardenfehler. Es war die Erfindung der Nullreferenz im Jahr 1965. Zu dieser Zeit entwarf ich das erste umfassende Typsystem für Referenzen in einer objektorientierten Sprache (ALGOL W). Mein Ziel war es sicherzustellen, dass jede Verwendung von Referenzen absolut sicher ist, wobei die Überprüfung automatisch vom Compiler durchgeführt wird. Aber ich konnte der Versuchung nicht widerstehen, eine Nullreferenz einzugeben, einfach weil es so einfach zu implementieren war. Dies hat zu unzähligen Fehlern, Schwachstellen und Systemabstürzen geführt, die in den letzten vierzig Jahren wahrscheinlich eine Milliarde Dollar an Schmerzen und Schäden verursacht haben. In den letzten Jahren wurde eine Reihe von Programmanalysatoren wie PREfix und PREfast in Microsoft verwendet, um Referenzen zu überprüfen und Warnungen zu geben, wenn das Risiko besteht, dass sie nicht null sind. Neuere Programmiersprachen wie Spec # haben Deklarationen für Nicht-Null-Referenzen eingeführt. Dies ist die Lösung, die ich 1965 abgelehnt habe.

Siehe diese Frage auch bei Programmierern

nawfal
quelle
1

Ich habe Null (oder Null) immer als das Fehlen eines Wertes angesehen .

Manchmal willst du das, manchmal nicht. Dies hängt von der Domain ab, mit der Sie arbeiten. Wenn die Abwesenheit von Bedeutung ist: kein zweiter Vorname, kann Ihre Bewerbung entsprechend handeln. Wenn andererseits der Nullwert nicht vorhanden sein sollte: Der Vorname ist null, dann erhält der Entwickler den sprichwörtlichen Anruf um 2 Uhr morgens.

Ich habe auch gesehen, dass Code mit Überprüfungen auf Null überladen und überkompliziert ist. Für mich bedeutet dies eines von zwei Dingen:
a) einen Fehler weiter oben im Anwendungsbaum
b) schlechtes / unvollständiges Design

Positiv zu vermerken ist, dass Null wahrscheinlich einer der nützlicheren Begriffe ist, um zu überprüfen, ob etwas fehlt, und Sprachen ohne das Konzept von Null werden die Dinge zu kompliziert, wenn es Zeit ist, Daten zu validieren. In diesem Fall setzen die Sprachen normalerweise Variablen auf eine leere Zeichenfolge, 0 oder eine leere Sammlung, wenn eine neue Variable nicht initialisiert wird. Wenn jedoch eine leere Zeichenfolge oder 0 oder eine leere Sammlung gültige Werte für Ihre Anwendung sind, liegt ein Problem vor.

Manchmal wurde dies umgangen, indem spezielle / seltsame Werte für Felder erfunden wurden, um einen nicht initialisierten Zustand darzustellen. Aber was passiert dann, wenn der Sonderwert von einem gut gemeinten Benutzer eingegeben wird? Und lassen Sie uns nicht in das Chaos geraten, das dies bei Datenüberprüfungsroutinen verursacht. Wenn die Sprache das Nullkonzept unterstützen würde, würden alle Bedenken verschwinden.

Jon
quelle
Hallo @Jon, es ist ein bisschen schwierig, dir hier zu folgen. Endlich wurde mir klar, dass Sie mit "speziellen / seltsamen" Werten wahrscheinlich etwas wie "undefiniert" von Javascript oder "NaN" von IEEE meinen. Abgesehen davon sprechen Sie keine der Fragen an, die das OP gestellt hat. Und die Aussage, dass "Null wahrscheinlich der nützlichste Begriff ist, um zu überprüfen, ob etwas fehlt", ist mit ziemlicher Sicherheit falsch. Optionstypen sind eine angesehene, typsichere Alternative zu null.
Stephen Swensen
@Stephen - Wenn ich auf meine Nachricht zurückblicke, denke ich, dass die gesamte 2. Hälfte auf eine noch zu stellende Frage verschoben werden sollte. Aber ich sage immer noch, dass null sehr nützlich ist, um zu überprüfen, ob etwas fehlt.
Jon
0

Vektorsprachen können manchmal davonkommen, wenn sie keine Null haben.

Der leere Vektor dient in diesem Fall als typisierte Null.

Joshua
quelle
Ich glaube, ich verstehe, wovon Sie sprechen, aber können Sie einige Beispiele nennen? Insbesondere beim Anwenden mehrerer Funktionen auf einen möglicherweise Nullwert?
Roman A. Taycher
Das Anwenden einer Vektortransformation auf einen leeren Vektor führt zu einem anderen leeren Vektor. Zu Ihrer Information, SQL ist meist eine Vektorsprache.
Joshua
1
OK, ich kläre das besser. SQL ist eine Vektorsprache für Zeilen und eine Wertesprache für Spalten.
Joshua