Wenn null schlecht ist, warum implementieren moderne Sprachen es? [geschlossen]

82

Ich bin sicher, dass Designer von Sprachen wie Java oder C # Probleme mit der Existenz von Nullreferenzen kannten (siehe Sind Nullreferenzen wirklich eine schlechte Sache? ). Auch das Implementieren eines Optionstyps ist nicht viel komplexer als Nullreferenzen.

Warum haben sie sich überhaupt dafür entschieden? Ich bin sicher, dass das Fehlen von Nullreferenzen sowohl von Sprachentwicklern als auch von Benutzern zu einer besseren Codequalität (insbesondere zu einem besseren Bibliotheksdesign) führen (oder diese sogar erzwingen) würde.

Ist es einfach wegen des Konservativismus - "andere Sprachen haben es, wir müssen es auch haben ..."?

mrpyo
quelle
99
null ist großartig. Ich liebe es und benutze es jeden Tag.
Pieter B
17
@PieterB Aber verwenden Sie es für die meisten Referenzen, oder möchten Sie, dass die meisten Referenzen nicht null sind? Das Argument ist nicht, dass es keine nullwertfähigen Daten geben sollte, sondern dass sie explizit sein und sich anmelden sollten.
11
@PieterB Aber wenn die Mehrheit nicht nullfähig sein sollte, macht es dann keinen Sinn, Nullfähigkeit als Ausnahme und nicht als Standard zu definieren? Beachten Sie, dass das übliche Design von Optionstypen darin besteht, explizite Überprüfungen auf Abwesenheit und Entpacken zu erzwingen, aber man kann auch die bekannte Java / C # / ... -Semantik für nullfähige Opt-In-Referenzen verwenden (verwenden, als ob sie nicht nullfähig wären, in die Luft jagen) wenn null). Es würde zumindest einige Fehler verhindern und eine statische Analyse, die sich über fehlende Nullprüfungen beschwert, viel praktischer machen.
20
WTF ist mit euch auf? Von all den Dingen, die mit Software schief gehen können und können, ist der Versuch, eine Null zu dereferenzieren, überhaupt kein Problem. Es erzeugt IMMER einen AV / Seg-Fehler und wird so behoben. Gibt es einen so großen Mangel an Fehlern, dass Sie sich darüber Sorgen machen müssen? Wenn ja, habe ich viel übrig, und keiner von ihnen bringt Probleme mit Nullreferenzen / Zeigern mit sich.
Martin James
13
@MartinJames "Es erzeugt IMMER einen AV / Seg-Fehler und wird so behoben" - nein, nein, tut es nicht.
Detly

Antworten:

97

Haftungsausschluss: Da ich keinen Sprachdesigner persönlich kenne, ist jede Antwort, die ich Ihnen gebe, spekulativ.

Von Tony Hoare selbst:

Ich nenne es meinen Milliarden-Dollar-Fehler. Es war die Erfindung der Nullreferenz im Jahr 1965. Damals entwarf ich das erste umfassende Typensystem 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 einzufügen, einfach weil es so einfach zu implementieren war. Dies hat zu unzähligen Fehlern, Sicherheitslücken und Systemabstürzen geführt, die in den letzten vierzig Jahren wahrscheinlich eine Milliarde Dollar an Schmerzen und Schäden verursacht haben.

Betonung meiner.

Natürlich schien es ihm damals keine schlechte Idee zu sein. Es ist wahrscheinlich, dass es zum Teil aus dem gleichen Grund verewigt wurde - wenn es für den mit dem Turing Award ausgezeichneten Erfinder von Quicksort eine gute Idee war, ist es nicht verwunderlich, dass viele Menschen immer noch nicht verstehen, warum es böse ist. Zum Teil ist dies auch deshalb wahrscheinlich, weil es praktisch ist, wenn neue Sprachen älteren Sprachen ähnlich sind, sowohl aus Marketing- als auch aus Lernkurvengründen. Ein typisches Beispiel:

"Wir waren hinter den C ++ - Programmierern her. Wir haben es geschafft, viele von ihnen auf halbem Weg nach Lisp zu ziehen." -Guy Steele, Co-Autor der Java-Spezifikation

(Quelle: http://www.paulgraham.com/icad.html )

Und natürlich hat C ++ null, weil C null hat, und es besteht keine Notwendigkeit, auf die historischen Auswirkungen von C einzugehen. C # löste J ++ ab, das die Java-Implementierung von Microsoft war, und C ++ wurde als bevorzugte Sprache für die Windows-Entwicklung abgelöst.

BEARBEITEN Hier ist ein weiteres Zitat von Hoare, das es wert ist, in Betracht gezogen zu werden:

Programmiersprachen sind insgesamt sehr viel komplizierter als früher: Objektorientierung, Vererbung und andere Merkmale werden vom Standpunkt einer kohärenten und wissenschaftlich fundierten Disziplin oder einer Korrektheitstheorie noch nicht wirklich durchdacht . Mein ursprüngliches Postulat, das ich mein ganzes Leben lang als Wissenschaftler verfolgt habe, ist, dass man die Kriterien der Korrektheit verwendet, um sich auf ein anständiges Programmiersprachendesign zu konzentrieren - eines, das seinen Benutzern keine Fallen stellt, und eines, in dem sie sich befinden Wobei die verschiedenen Komponenten des Programms eindeutig verschiedenen Komponenten seiner Spezifikation entsprechen, so dass Sie kompositorisch darüber nachdenken können. [...] Die Werkzeuge, einschließlich des Compilers, müssen auf einer Theorie basieren, was es bedeutet, ein korrektes Programm zu schreiben. -Oral History Interview von Philip L. Frana, 17. Juli 2002, Cambridge, England; Charles Babbage Institute, Universität von Minnesota. [ Http://www.cbi.umn.edu/oh/display.phtml?id=343]

Wieder meine Betonung. Sun / Oracle und Microsoft sind Unternehmen, und das Endergebnis eines Unternehmens ist Geld. Die Vorteile für sie nullkönnten die Nachteile überwiegen, oder sie hatten einfach eine zu enge Frist, um das Problem vollständig zu prüfen. Als Beispiel für einen anderen Sprachfehler, der wahrscheinlich aufgrund von Fristen aufgetreten ist:

Es ist eine Schande, dass Cloneable kaputt ist, aber es passiert. Die ursprünglichen Java-APIs wurden innerhalb kürzester Zeit erstellt, um ein schließendes Marktfenster zu erreichen. Das ursprüngliche Java-Team hat einen unglaublichen Job gemacht, aber nicht alle APIs sind perfekt. Klonbar ist eine Schwachstelle, und ich denke, die Leute sollten sich ihrer Grenzen bewusst sein. -Josh Bloch

(Quelle: http://www.artima.com/intv/bloch13.html )

Doval
quelle
32
Lieber Downvoter, wie kann ich meine Antwort verbessern?
Doval
6
Sie haben die Frage nicht beantwortet. Sie haben nur einige Zitate über einige nachträgliche Meinungen und einige zusätzliche Handbewegungen über "Kosten" angegeben. (Wenn Null ein Milliarden-Dollar-Fehler ist, sollten die von MS und Java durch Implementierung gesparten Dollar diese Schulden nicht reduzieren?)
DougM,
29
@DougM Was erwartest du von mir, jeden Sprachdesigner der letzten 50 Jahre zu treffen und ihn zu fragen, warum er nullin seiner Sprache implementiert hat ? Jede Antwort auf diese Frage ist spekulativ, es sei denn, sie stammt von einem Sprachdesigner. Ich kenne außer Eric Lippert keine, die diese Seite so häufig besuchen. Der letzte Teil ist aus zahlreichen Gründen ein roter Hering. Die Menge an Code von Drittanbietern, die auf MS- und Java-APIs geschrieben wurde, überwiegt offensichtlich die Menge an Code in der API. Also, wenn Ihre Kunden wollen null, geben Sie ihnen null. Sie nehmen auch an, dass sie akzeptiert haben, nullkostet sie Geld.
Doval
3
Wenn Sie nur eine spekulative Antwort geben können, geben Sie dies in Ihrem ersten Absatz deutlich an. (Sie haben gefragt, wie Sie Ihre Antwort verbessern könnten, und ich habe geantwortet. Alle Klammern sind nur Kommentare, die Sie ignorieren können.
Diese
7
Diese Antwort ist vernünftig; Ich habe einige weitere Überlegungen in meine aufgenommen. Ich stelle fest, dass ICloneablein .NET ähnlich kaputt ist; Leider ist dies ein Ort, an dem die Mängel von Java nicht rechtzeitig gelernt wurden.
Eric Lippert
121

Ich bin sicher, dass Designer von Sprachen wie Java oder C # Probleme im Zusammenhang mit der Existenz von Nullreferenzen kannten

Na sicher.

Auch das Implementieren eines Optionstyps ist nicht viel komplexer als Nullreferenzen.

Ich bin anderer Ansicht! Die Entwurfsüberlegungen, die in C # 2 zu nullwertfähigen Werttypen führten, waren komplex, kontrovers und schwierig. Sie nahmen den Designteams sowohl der Sprachen als auch der Laufzeit viele Monate Debatte, Implementierung von Prototypen usw. ab, und tatsächlich wurde die Semantik des nullbaren Boxens sehr nahe an Versand C # 2.0 geändert, was sehr umstritten war.

Warum haben sie sich überhaupt dafür entschieden?

Jedes Design ist ein Prozess der Auswahl unter vielen subtilen und grob inkompatiblen Zielen. Ich kann nur einen kurzen Überblick über einige der Faktoren geben, die berücksichtigt werden sollten:

  • Die Orthogonalität von Sprachmerkmalen wird im Allgemeinen als eine gute Sache angesehen. C # verfügt über nullfähige Werttypen, nicht nullfähige Werttypen und nullfähige Referenztypen. Nicht nullbare Referenztypen existieren nicht, wodurch das Typensystem nicht orthogonal ist.

  • Die Kenntnis der vorhandenen Benutzer von C, C ++ und Java ist wichtig.

  • Einfache Interoperabilität mit COM ist wichtig.

  • Eine einfache Interoperabilität mit allen anderen .NET-Sprachen ist wichtig.

  • Eine einfache Interoperabilität mit Datenbanken ist wichtig.

  • Die Konsistenz der Semantik ist wichtig; Wenn wir den Verweis TheKingOfFrance gleich null haben, heißt das dann immer "es gibt keinen König von Frankreich im Moment", oder heißt das auch "es gibt definitiv einen König von Frankreich; ich weiß nur nicht, wer es im Moment ist"? oder kann es bedeuten, dass "die Vorstellung, einen König in Frankreich zu haben, unsinnig ist, also stell nicht einmal die Frage!"? Null kann all diese Dinge und mehr in C # bedeuten, und all diese Konzepte sind nützlich.

  • Leistungskosten sind wichtig.

  • Es ist wichtig, für statische Analysen zugänglich zu sein.

  • Die Konsistenz des Typsystems ist wichtig; können wir immer wissen , dass ein Nicht-Nullable - Referenz ist nie unter irgendwelchen Umständen beobachtet ungültig sein? Was ist mit dem Konstruktor eines Objekts mit einem nicht nullwertfähigen Referenzfeldtyp? Was ist mit dem Finalizer eines solchen Objekts, bei dem das Objekt finalisiert wird, weil der Code, der den Verweis ausfüllen sollte, eine Ausnahme ausgelöst hat ? Ein Typensystem, das Sie über seine Garantien belügt, ist gefährlich.

  • Und was ist mit der Konsistenz der Semantik? NULL- Werte werden bei Verwendung weitergegeben, aber NULL- Referenzen lösen bei Verwendung Ausnahmen aus. Das ist inkonsistent; Ist diese Inkonsistenz durch irgendeinen Nutzen gerechtfertigt?

  • Können wir die Funktion implementieren, ohne andere Funktionen zu beeinträchtigen? Welche anderen möglichen zukünftigen Funktionen schließt die Funktion aus?

  • Sie ziehen mit der Armee in den Krieg, die Sie haben, nicht mit der, die Sie wollen. Denken Sie daran, dass in C # 1.0 keine Generika vorhanden waren. Daher ist das Sprechen Maybe<T>als Alternative ein absoluter Nichtstarter. Sollte .NET für zwei Jahre verrutschen, während das Runtime-Team Generika hinzufügte, nur um Nullreferenzen zu beseitigen?

  • Was ist mit der Konsistenz des Typsystems? Sie können Nullable<T>für jeden Werttyp sagen - nein, warten Sie, das ist eine Lüge. Das kannst du nicht sagen Nullable<Nullable<T>>. Solltest du in der Lage sein? Wenn ja, wie lautet die gewünschte Semantik? Lohnt es sich, das gesamte Typensystem nur für diese Funktion mit einem Sonderfall auszustatten?

Und so weiter. Diese Entscheidungen sind komplex.

Eric Lippert
quelle
12
+1 für alles außer vor allem für Generika. Es ist leicht zu vergessen, dass es Zeiträume in der Geschichte von Java und C # gab, in denen Generika nicht existierten.
Doval
2
Vielleicht eine blöde Frage (ich bin nur ein IT-Student) - aber der Optionstyp konnte nicht auf Syntaxebene implementiert werden (CLR weiß nichts davon) als reguläre nullfähige Referenz, die vor der Verwendung eine Überprüfung auf "has-value" erfordert Code? Ich glaube, dass Optionstypen zur Laufzeit keine Prüfungen benötigen.
Mrpyo
2
@ mrpyo: Sicher, das ist eine mögliche Wahl für die Implementierung. Keine der anderen Designentscheidungen geht verloren, und diese Implementierungsentscheidung hat viele eigene Vor- und Nachteile.
Eric Lippert
1
@mrpyo Ich denke, es ist keine gute Idee, eine Überprüfung auf "has-value" zu erzwingen. Theoretisch ist es eine sehr gute Idee, aber in der Praxis würde IMO alle möglichen leeren Prüfungen mit sich bringen, nur um die vom Compiler geprüften Ausnahmen in Java zu befriedigen, und Leute, die es damit täuschen catches, tun nichts. Ich denke, es ist besser, das System in die Luft jagen zu lassen, als den Betrieb in einem möglicherweise ungültigen Zustand fortzusetzen.
NothingsImpossible
2
@voo: Arrays mit nicht löschbaren Referenztypen sind aus vielen Gründen schwierig. Es gibt viele mögliche Lösungen und alle verursachen Kosten für verschiedene Vorgänge. Supercat schlägt vor, zu verfolgen, ob ein Element vor seiner Zuweisung legal gelesen werden kann, was Kosten verursacht. Sie müssen sicherstellen, dass für jedes Element ein Initialisierer ausgeführt wird, bevor das Array angezeigt wird, wodurch andere Kosten entstehen. Also hier ist der Haken: Egal für welche dieser Techniken man sich entscheidet, jemand wird sich beschweren, dass es für sein Haustier-Szenario nicht effizient ist . Dies ist ein schwerwiegender Punkt gegen die Funktion.
Eric Lippert
28

Null dient einem sehr gültigen Zweck, einen Mangel an Wert darzustellen.

Ich werde sagen, dass ich die lautstärkste Person bin, die ich über die Missbräuche von Null und all die Kopfschmerzen und Leiden, die sie verursachen können, weiß, besonders wenn sie großzügig eingesetzt werden.

Mein persönlicher Standpunkt ist, dass Menschen Nullen nur verwenden dürfen , wenn sie begründen können, dass dies notwendig und angemessen ist.

Beispiel zur Begründung von Nullen:

Das Sterbedatum ist in der Regel ein nullwertfähiges Feld. Es gibt drei mögliche Situationen mit Todesdatum. Entweder ist die Person gestorben und das Datum ist bekannt, die Person ist gestorben und das Datum ist unbekannt, oder die Person ist nicht tot und daher gibt es kein Todesdatum.

Das Todesdatum ist auch ein DateTime-Feld und hat keinen "unbekannten" oder "leeren" Wert. Es hat zwar das Standarddatum, das angezeigt wird, wenn Sie eine neue Datumszeit erstellen, die je nach verwendeter Sprache variiert. Technisch gesehen besteht jedoch die Möglichkeit, dass die Person zu diesem Zeitpunkt tatsächlich gestorben ist und als Ihr "leerer Wert" gekennzeichnet wird, wenn Sie dies tun würden Verwenden Sie das Standarddatum.

Die Daten müssten die Situation richtig darstellen.

Person ist verstorben Sterbedatum bekannt (09.03.1984)

Einfach, '3/9/1984'

Person ist Todestag ist unbekannt

Also, was ist am besten? Null , '0/0/0000' oder '01 / 01/1869 '(oder was auch immer Ihr Standardwert ist?)

Person ist nicht tot Sterbedatum ist nicht anwendbar

Also, was ist am besten? Null , '0/0/0000' oder '01 / 01/1869 '(oder was auch immer Ihr Standardwert ist?)

Denken wir also über jeden Wert nach ...

  • Null , es hat Konsequenzen und Bedenken, vor denen Sie vorsichtig sein müssen, wenn Sie versehentlich versuchen, es zu manipulieren, ohne zu bestätigen, dass es nicht null ist. Dies würde zum Beispiel eine Ausnahme auslösen, aber es repräsentiert auch die tatsächliche Situation am besten ... Wenn die Person nicht tot ist das Todesdatum existiert nicht ... es ist nichts ... es ist null ...
  • 0/0/0000 , Dies könnte in einigen Sprachen in Ordnung sein und sogar eine angemessene Darstellung ohne Datum sein. Leider wird dies in einigen Sprachen und bei der Validierung als ungültige Datums- und Uhrzeitangabe abgelehnt, was in vielen Fällen zu einem Fehlschlag führt.
  • 01.01.1869 (oder was auch immer Ihr voreingestellter Datums- / Uhrzeitwert ist) , das Problem hier ist, dass es schwierig wird, damit umzugehen . Sie könnten das als Wertmangel verwenden, außer was passiert, wenn ich alle meine Aufzeichnungen herausfiltern möchte, für die ich kein Sterbedatum habe? Ich könnte leicht Leute herausfiltern, die an diesem Datum tatsächlich gestorben sind, was zu Problemen mit der Datenintegrität führen könnte.

Die Tatsache , manchmal Sie Sie brauchen nichts zu vertreten und sicher manchmal ein Variablentyp eignet sich gut für das, aber oft Variablentypen müssen in der Lage sein , nichts zu vertreten.

Wenn ich keine Äpfel habe, habe ich 0 Äpfel, aber was ist, wenn ich nicht weiß, wie viele Äpfel ich habe?

Auf jeden Fall ist null missbraucht und möglicherweise gefährlich, aber es ist manchmal notwendig. In vielen Fällen ist dies nur die Standardeinstellung, denn bis ich einen Wert gebe, fehlt ein Wert und etwas muss ihn darstellen. (Null)

RualStorge
quelle
37
Null serves a very valid purpose of representing a lack of value.Ein Optionoder Maybe-Typ erfüllt diesen sehr gültigen Zweck, ohne das Typensystem zu umgehen.
Doval
34
Niemand argumentiert, dass es keinen Wertmangel geben sollte, sondern dass Werte, die möglicherweise fehlen, explizit als solche gekennzeichnet werden sollten , anstatt dass jeder Wert möglicherweise fehlt.
2
Ich denke, RualStorge sprach in Bezug auf SQL, weil es Lager gibt, die besagen, dass jede Spalte als markiert werden sollte NOT NULL. Meine Frage hatte jedoch nichts mit RDBMS zu
tun
5
+1 für die Unterscheidung zwischen "kein Wert" und "unbekannter Wert"
David
2
Wäre es nicht sinnvoller, den Staat einer Person zu trennen? Dh ein PersonTyp hat ein stateTypfeld State, das eine diskriminierte Vereinigung von Aliveund ist Dead(dateOfDeath : Date).
Jon-Hanson
10

Ich würde nicht so weit gehen, wie "andere Sprachen es haben, wir müssen es auch haben ...", als wäre es eine Art Schritt mit den Joneses. Ein wichtiges Merkmal jeder neuen Sprache ist die Fähigkeit, mit vorhandenen Bibliotheken in anderen Sprachen zusammenzuarbeiten (siehe: C). Da C Nullzeiger hat, muss für die Interoperabilitätsschicht unbedingt das Konzept von Null gelten (oder ein anderes "nicht vorhandenes" Äquivalent, das bei Verwendung explodiert).

Die Sprache Designer könnten haben gewählt verwenden Option - Typen und zwingen Sie den Null - Pfad zu behandeln überall , dass die Dinge null sein könnten. Und das würde mit ziemlicher Sicherheit zu weniger Fehlern führen.

Die Verwendung von Optionstypen für diese Interoperabilitätsschicht hätte jedoch (insbesondere für Java und C # aufgrund des Zeitpunkts ihrer Einführung und ihrer Zielgruppe) wahrscheinlich geschadet, wenn ihre Annahme nicht torpediert worden wäre. Entweder wird der Optionstyp komplett durchgereicht, was die C ++ - Programmierer der mittleren bis späten 90er-Jahre nervt - oder die Interoperabilitätsschicht wirft Ausnahmen, wenn sie auf Nullen stößt, was die C ++ - Programmierer der mittleren bis späten 90er-Jahre nervt. ..

Telastyn
quelle
3
Der erste Absatz ergibt für mich keinen Sinn. Java hat kein C-Interop in der von Ihnen vorgeschlagenen Form (es gibt JNI, aber es springt bereits durch ein Dutzend Rahmen für alles , was sich auf Referenzen bezieht, und es wird in der Praxis selten verwendet), dasselbe gilt für andere "moderne" Sprachen.
@delnan - Entschuldigung, ich bin eher mit C # vertraut, das diese Art von Interop hat. Ich bin eher davon ausgegangen, dass viele der grundlegenden Java-Bibliotheken auch JNI verwenden.
Telastyn
6
Sie machen ein gutes Argument für das Zulassen von Null, können aber trotzdem Null zulassen, ohne dies zu fördern . Scala ist ein gutes Beispiel dafür. Es kann nahtlos mit Java-APIs zusammenarbeiten, die null verwenden. Es wird jedoch empfohlen, es in eine Datei Optionfür die Verwendung in Scala zu packen, was so einfach ist wie val x = Option(possiblyNullReference). In der Praxis dauert es nicht lange, bis die Menschen die Vorteile eines Option.
Karl Bielefeldt
1
Optionstypen gehen Hand in Hand mit (statisch verifizierten) Musterübereinstimmungen, die in C # leider nicht vorhanden sind. F # tut es aber und es ist wunderbar.
Steven Evers
1
@SteveEvers Es ist möglich, es mit einer abstrakten Basisklasse mit privatem Konstruktor, versiegelten inneren Klassen und einer MatchMethode zu fälschen , die Delegaten als Argumente verwendet. Dann übergeben Sie Lambda-Ausdrücke an Match(Bonuspunkte für die Verwendung benannter Argumente) und Matchrufen die richtige auf.
Doval
7

Ich denke, wir sind uns alle einig, dass ein Konzept der Nichtigkeit notwendig ist. Es gibt Situationen, in denen wir das Fehlen von Informationen darstellen müssen.

Das Zulassen von nullReferenzen (und Zeigern) ist nur eine Implementierung dieses Konzepts und möglicherweise die beliebteste, obwohl Probleme bekannt sind: C, Java, Python, Ruby, PHP, JavaScript usw. verwenden alle ein ähnliches Konzept null.

Warum ? Nun, was ist die Alternative?

In funktionalen Sprachen wie Haskell haben Sie den Typ Optionoder Maybe; diese bauen jedoch auf:

  • parametrische Typen
  • algebraische Datentypen

Hat das ursprüngliche C, Java, Python, Ruby oder PHP eine dieser Funktionen unterstützt? Nein. Javas fehlerhafte Generika sind neu in der Geschichte der Sprache und ich bezweifle irgendwie, dass die anderen sie überhaupt implementieren.

Hier hast du es. nullist einfach, parametrische algebraische Datentypen sind schwieriger. Die Leute entschieden sich für die einfachste Alternative.

Matthieu M.
quelle
+1 für "null ist einfach, parametrische algebraische Datentypen sind schwieriger." Aber ich denke, es war nicht so sehr ein Problem der parametrischen Typisierung, und die ADTs waren schwieriger. Es ist nur so, dass sie nicht als notwendig wahrgenommen werden. Wenn Java hingegen ohne ein Objektsystem ausgeliefert hätte, wäre es fehlgeschlagen. OOP war ein "Showstop" -Feature. Wenn Sie es nicht hatten, war niemand interessiert.
Doval
@Doval: Nun, OOP war vielleicht für Java notwendig, aber nicht für C :) Aber es ist wahr, dass Java darauf abzielte, einfach zu sein. Leider scheinen die Leute anzunehmen, dass eine einfache Sprache zu einfachen Programmen führt, was ein bisschen seltsam ist (Brainfuck ist eine sehr einfache Sprache ...), aber wir sind uns sicher einig, dass auch komplizierte Sprachen (C ++ ...) kein Allheilmittel sind Sie können unglaublich nützlich sein.
Matthieu M.
1
@MatthieuM .: Reale Systeme sind komplex. Mit einer gut gestalteten Sprache, deren Komplexität dem zu modellierenden realen System entspricht, kann das komplexe System mit einfachem Code modelliert werden. Versuche, eine Sprache zu stark zu vereinfachen, übertragen die Komplexität einfach auf den Programmierer, der sie verwendet.
Supercat
@supercat: da könnte ich nicht mehr zustimmen. Oder wie Einstein umschreibt: "Mach alles so einfach wie möglich, aber nicht einfacher."
Matthieu M.
@MatthieuM .: Einstein war in vielerlei Hinsicht weise. Sprachen, die anzunehmen versuchen, dass "alles ein Objekt ist, in dem ein Verweis gespeichert werden kann Object", erkennen nicht, dass praktische Anwendungen nicht gemeinsam genutzte veränderbare Objekte und gemeinsam nutzbare unveränderliche Objekte (die sich beide wie Werte verhalten sollten) sowie gemeinsam nutzbare und nicht gemeinsam nutzbare Objekte benötigen Entitäten. Die Verwendung eines einzigen ObjectTyps für alles macht solche Unterscheidungen nicht überflüssig. es macht es nur schwieriger, sie richtig zu verwenden.
Supercat
5

Null / nil / none selbst ist nicht böse.

Wenn Sie seine irreführenden Namen berühmte Rede Schau „Milliarden - Dollar - Fehler“, Tony Hoare spricht darüber , wie so dass jede Variable der Lage sein , null zu halten , war ein großer Fehler. Die Alternative - mit Optionen - nicht nicht in der Tat von null Referenzen loszuwerden. Stattdessen können Sie angeben, welche Variablen null enthalten dürfen und welche nicht.

Tatsächlich unterscheiden sich Null-Dereferenzierungsfehler bei modernen Sprachen, die eine ordnungsgemäße Ausnahmebehandlung implementieren, nicht von anderen Ausnahmen - Sie finden sie, Sie beheben sie. Einige Alternativen zu Null-Referenzen (zum Beispiel das Null-Objekt-Muster) verbergen Fehler und führen dazu, dass Dinge erst viel später stillschweigend fehlschlagen. Meiner Meinung nach ist es viel besser, schnell zu scheitern .

Die Frage ist also, warum Sprachen Optionen nicht implementieren können. Tatsächlich kann die wohl beliebteste Sprache aller Zeiten in C ++ Objektvariablen definieren, die nicht zugewiesen werden können NULL. Dies ist eine Lösung für das "Null-Problem", das Tony Hoare in seiner Rede erwähnte. Warum hat die am weitesten verbreitete Schriftsprache Java sie nicht? Man könnte fragen, warum es im Allgemeinen so viele Mängel hat , insbesondere in seinem Typensystem. Ich glaube nicht, dass man wirklich sagen kann, dass Sprachen diesen Fehler systematisch begehen. Manche tun es, manche nicht.

BT
quelle
1
Eine der größten Stärken von Java aus Sicht der Implementierung, aber die Schwächen aus Sicht der Sprache besteht darin, dass es nur einen nicht-primitiven Typ gibt: die Promiscuous Object Reference. Dies vereinfacht die Laufzeit enorm und ermöglicht einige extrem leichte JVM-Implementierungen. Dieser Entwurf bedeutet jedoch, dass jeder Typ einen Standardwert haben muss, und für eine Promiscuous Object Reference ist der einzig mögliche Standardwert null.
Supercat
Nun, jedenfalls ein nicht-primitiver Wurzeltyp . Warum ist dies aus sprachlicher Sicht eine Schwäche? Ich verstehe nicht, warum diese Tatsache erfordert, dass jeder Typ einen Standardwert hat (oder umgekehrt, warum mehrere Stammtypen zulassen, dass Typen keinen Standardwert haben), und warum das eine Schwäche ist.
BT
Welche andere Art von nicht-primitivem Element könnte ein Feld oder ein Array-Element enthalten? Die Schwäche besteht darin, dass einige Referenzen verwendet werden, um die Identität zu kapseln, und einige, um die Werte zu kapseln, die in den dadurch identifizierten Objekten enthalten sind. Dies nullist die einzig sinnvolle Standardeinstellung für Variablen vom Referenztyp, die zum Einkapseln der Identität verwendet werden . Referenzen, die zum Einkapseln von Werten verwendet werden, können jedoch in Fällen, in denen ein Typ eine sinnvolle Standardinstanz haben würde oder konstruieren könnte, ein sinnvolles Standardverhalten aufweisen. Viele Aspekte, wie sich Referenzen verhalten sollten, hängen davon ab, ob und wie sie den Wert verkapseln, aber ...
supercat
... das Java-Typ-System kann das nicht ausdrücken. Wenn dies fooder einzige Verweis auf ein int[]Containing ist {1,2,3}und Code fooeinen Verweis auf ein int[]Containing enthalten {2,2,3}möchte, ist das Inkrementieren der schnellste Weg, dies zu erreichen foo[0]. Wenn Code einer Methode mitteilen möchte, dass sie foogültig ist {1,2,3}, ändert die andere Methode das Array nicht und behält einen Verweis nicht über den Punkt hinaus bei foo, an dem er geändert werden soll. Der schnellste Weg, dies zu erreichen, besteht darin, einen Verweis an das Array zu übergeben. Wenn Java einen "ephemeren Nur-Lese-Verweistyp" hatte, dann ...
supercat
... das Array könnte sicher als kurzlebige Referenz übergeben werden, und eine Methode, die seinen Wert beibehalten möchte, würde wissen, dass es kopiert werden muss. In Abwesenheit eines solchen Typs besteht die einzige Möglichkeit, den Inhalt eines Arrays sicher verfügbar zu machen, darin, entweder eine Kopie davon zu erstellen oder ihn in ein Objekt zu kapseln, das nur für diesen Zweck erstellt wurde.
Supercat
4

Denn Programmiersprachen sind in der Regel eher praktisch als technisch korrekt. Tatsache ist, dass nullZustände aufgrund von fehlerhaften oder fehlenden Daten oder aufgrund eines noch nicht festgelegten Zustands häufig vorkommen. Die technisch überlegenen Lösungen sind umso unhandlicher, als Nullzustände zuzulassen und die Tatsache, dass Programmierer Fehler machen, in den Hintergrund zu rücken.

Wenn ich zum Beispiel ein einfaches Skript schreiben möchte, das mit einer Datei funktioniert, kann ich Pseudocode wie folgt schreiben:

file = openfile("joebloggs.txt")

for line in file
{
  print(line)
}

und es wird einfach fehlschlagen, wenn joebloggs.txt nicht existiert. Die Sache ist, dass für einfache Skripte, die wahrscheinlich in Ordnung sind, und für viele Situationen in komplexerem Code ich weiß, dass sie existieren und der Fehler nicht auftreten wird, was mich dazu zwingt, meine Zeit zu verschwenden. Die sichereren Alternativen erreichen ihre Sicherheit, indem sie mich zwingen, richtig mit dem potenziellen Ausfallstatus umzugehen, aber oft möchte ich das nicht, ich möchte einfach weiter.

Jack Aidley
quelle
13
Und hier haben Sie ein Beispiel gegeben, was genau mit Nullen falsch ist. Eine korrekt implementierte "openfile" -Funktion sollte eine Ausnahme (für fehlende Dateien) auslösen, die die Ausführung genau dort mit einer genauen Erklärung dessen, was passiert ist, anhält. Wenn es stattdessen null zurückgibt, wird die Weitergabe von (nach for line in file) fortgesetzt und eine bedeutungslose Nullreferenzausnahme ausgelöst. Dies ist für ein so einfaches Programm in Ordnung, verursacht jedoch echte Fehlerbehebungsprobleme in viel komplexeren Systemen. Wenn es keine Nullen gäbe, könnte der Designer von "openfile" diesen Fehler nicht machen.
Mrpyo
2
+1 für "Weil Programmiersprachen in der Regel eher praktisch als technisch korrekt sind"
Martin Ba
2
Jeder mir bekannte Optionstyp ermöglicht es Ihnen, das Failing-on-Null mit einem einzigen kurzen zusätzlichen Methodenaufruf durchzuführen (Rust-Beispiel:) let file = something(...).unwrap(). Abhängig von Ihrem POV ist es eine einfache Möglichkeit, Fehler oder eine prägnante Behauptung, dass Null nicht auftreten kann, zu vermeiden. Die Zeit verschwendet ist minimal, und Sie Zeit an anderen Orten sparen , weil Sie nicht aus Figur haben, ob etwas kann null sein. Ein weiterer Vorteil (der für sich genommen den zusätzlichen Aufruf wert sein kann) ist, dass Sie den Fehlerfall explizit ignorieren. Wenn es fehlschlägt, gibt es kaum Zweifel, was schief gelaufen ist und wo das Update hin muss.
4
@mrpyo Nicht alle Sprachen unterstützen Ausnahmen und / oder Ausnahmebehandlung (a la try / catch). Und auch Ausnahmen können missbraucht werden - "Ausnahme wie Flusskontrolle" ist ein weit verbreitetes Anti-Muster. Dieses Szenario - eine Datei existiert nicht - ist AFAIK das am häufigsten genannte Beispiel für dieses Anti-Pattern. Anscheinend ersetzen Sie eine schlechte Praxis durch eine andere.
David
8
@mrpyo if file exists { open file }leidet unter einer Racebedingung . Die einzige zuverlässige Methode, um festzustellen, ob das Öffnen einer Datei erfolgreich ist, besteht darin, sie zu öffnen.
4

Der Zeiger NULL(oder niloder Niloder nulloder Nothingoder was auch immer in Ihrer bevorzugten Sprache genannt wird) kann eindeutig und praktisch verwendet werden .

Für Sprachen ohne Ausnahmesystem (z. B. C) kann ein Nullzeiger als Fehlermarke verwendet werden, wenn ein Zeiger zurückgegeben werden soll. Zum Beispiel:

char *buf = malloc(20);
if (!buf)
{
    perror("memory allocation failed");
    exit(1);
}

Hier wird ein NULLzurückgegebener von malloc(3)als ein Marker des Scheiterns verwendet.

Wenn es in Methoden- / Funktionsargumenten verwendet wird, kann es den Standardwert für das Argument angeben oder das Ausgabeargument ignorieren. Beispiel unten.

Selbst für Sprachen mit Ausnahmemechanismus kann ein Nullzeiger als Hinweis auf einen weichen Fehler verwendet werden (dh auf Fehler, die behoben werden können), insbesondere wenn die Ausnahmebehandlung teuer ist (z. B. Objective-C):

NSError *err = nil;
NSString *content = [NSString stringWithContentsOfURL:sourceFile
                                         usedEncoding:NULL // This output is ignored
                                                error:&err];
if (!content) // If the object is null, we have a soft error to recover from
{
    fprintf(stderr, "error: %s\n", [[err localizedDescription] UTF8String]);
    if (!error) // Check if the parent method ignored the error argument
        *error = err;
    return nil; // Go back to parent layer, with another soft error.
}

In diesem Fall führt der Soft-Fehler nicht zum Absturz des Programms, wenn er nicht abgefangen wird. Dies beseitigt den verrückten Try-Catch, den Java hat, und hat eine bessere Kontrolle über den Programmfluss, da weiche Fehler nicht unterbrochen werden (und die wenigen verbleibenden harten Ausnahmen sind normalerweise nicht behebbar und bleiben unberührt).

Maxthon Chan
quelle
5
Das Problem ist, dass es keine Möglichkeit gibt, Variablen, die niemals enthalten sollten, nullvon denen zu unterscheiden, die enthalten sollten. Wenn ich zum Beispiel einen neuen Typ möchte, der 5 Werte in Java enthält, könnte ich eine Aufzählung verwenden, aber was ich erhalte, ist ein Typ, der 6 Werte enthalten kann (die 5, die ich wollte + null). Es ist ein Fehler im Typensystem.
Doval
@Doval Wenn dies der Fall ist, weisen Sie NULL einfach eine Bedeutung zu (oder wenn Sie eine Standardeinstellung haben, behandeln Sie sie als Synonym für den Standardwert), oder verwenden Sie NULL (die niemals an erster Stelle stehen sollte) als Marker für einen weichen Fehler (dh Fehler, aber zumindest noch nicht abgestürzt)
Maxthon Chan
1
@MaxtonChan Nullkann nur dann eine Bedeutung zugewiesen werden, wenn die Werte eines Typs keine Daten enthalten (z. B. Aufzählungswerte). Sobald Ihre Werte etwas komplizierter sind (z. B. eine Struktur), nullkann keine für diesen Typ sinnvolle Bedeutung zugewiesen werden. Es gibt keine Möglichkeit, a nullals Struktur oder Liste zu verwenden. Und das Problem bei der Verwendung nullals Fehlersignal ist wiederum, dass wir nicht sagen können, was null zurückgeben oder null akzeptieren könnte. Jede Variable in Ihrem Programm kann eine Variable sein, nulles sei denn, Sie überprüfen jede einzelne Variable nullvor jeder einzelnen Verwendung mit äußerster Sorgfalt , was niemand tut.
Doval
1
@Doval: Es wäre keine besondere inhärente Schwierigkeit, einen unveränderlichen Referenztyp nullals verwendbaren Standardwert zu betrachten (z. B. den Standardwert " stringVerhalten als leere Zeichenfolge" wie im vorherigen Common Object Model). Alles, was nötig gewesen wäre, wäre gewesen, dass Sprachen verwendet würden, callanstatt callvirtnicht-virtuelle Mitglieder aufzurufen.
Supercat
@supercat Das ist ein guter Punkt, aber müssen Sie jetzt keine Unterstützung für die Unterscheidung zwischen unveränderlichen und nicht unveränderlichen Typen hinzufügen? Ich bin mir nicht sicher, wie trivial es ist, einer Sprache etwas hinzuzufügen.
Doval
4

Es gibt zwei verwandte, aber leicht unterschiedliche Probleme:

  1. Sollte nulles überhaupt geben? Oder solltest du immer verwenden, Maybe<T>wo null nützlich ist?
  2. Sollten alle Referenzen nullbar sein? Wenn nicht, welche sollte die Standardeinstellung sein?

    Wenn nullbare Verweistypen explizit als string?oder ähnlich deklariert werden müssen, werden die meisten (aber nicht alle) Probleme nullvermieden, ohne sich zu stark von den Programmierern zu unterscheiden, an die sie gewöhnt sind.

Ich stimme Ihnen zumindest zu, dass nicht alle Verweise nullwertfähig sein sollten. Aber die Vermeidung von Null ist nicht ohne Komplexität:

.NET initialisiert alle Felder auf, default<T>bevor verwalteter Code zum ersten Mal auf sie zugreifen kann. Dies bedeutet, dass Sie für Referenztypen nulloder etwas Äquivalentes Werttypen auf eine Art von Null initialisieren können, ohne Code auszuführen. Während beide schwerwiegende Nachteile haben, hat die Einfachheit der defaultInitialisierung diese Nachteile möglicherweise überwogen.

  • Zum Beispiel Feldern können Sie durch die Forderung der Initialisierung von Feldern dieses Problem umgehen , bevor der Belichtungs thisZeiger auf verwalteten Code. Spec # ist diesen Weg gegangen und hat im Vergleich zu C # eine andere Syntax als die Konstruktorkettung verwendet.

  • Stellen Sie bei statischen Feldern sicher, dass dies schwieriger ist, es sei denn, Sie schränken die Ausführung von Code in einem Feldinitialisierer stark ein, da Sie den thisZeiger nicht einfach ausblenden können .

  • Wie initialisiere ich Arrays von Referenztypen? Betrachten Sie ein, List<T>das durch ein Array mit einer Kapazität größer als die Länge gesichert ist. Die übrigen Elemente , müssen sie etwas Wert.

Ein weiteres Problem ist, dass Methoden wie " bool TryGetValue<T>(key, out T value)which return" nicht zugelassen werden, default(T)als valueob sie nichts finden würden. In diesem Fall ist es jedoch leicht zu argumentieren, dass der out-Parameter in erster Linie ein schlechtes Design ist und diese Methode eine diskriminierende Union oder ein Vielleicht zurückgeben sollte.

All diese Probleme können gelöst werden, aber es ist nicht so einfach wie "Null verbieten und alles ist in Ordnung".

CodesInChaos
quelle
Das List<T>ist meiner Meinung nach das beste Beispiel, denn es würde entweder erfordern , dass jeder Teinen Standardwert haben, dass jedes Element in dem Zusatzspeicher sein ein Maybe<T>mit einem zusätzlichen „isValid“ Feld, auch wenn Tein Maybe<U>oder dass der Code für die List<T>verhalten sich unterschiedlich , je ob Tes sich um einen nullbaren Typ handelt. Ich würde Initialisierung der berücksichtigen T[]Elemente auf einen Standardwert das kleinste Übel dieser Entscheidungen sein, aber es natürlich bedeutet , dass die Elemente zu benötigen haben einen Standardwert.
Supercat
Rust folgt Punkt 1 - überhaupt keine Null. Ceylon folgt Punkt 2 - standardmäßig nicht null. Referenzen, die null sein können, werden explizit mit einem Unionstyp deklariert, der entweder eine Referenz oder null enthält, aber null kann niemals der Wert einer einfachen Referenz sein. Daher ist die Sprache absolut sicher und es gibt keine NullPointerException, da sie semantisch nicht möglich ist.
Jim Balter
2

Die meisten nützlichen Programmiersprachen ermöglichen das Schreiben und Lesen von Datenelementen in willkürlichen Sequenzen, so dass es häufig nicht möglich ist, die Reihenfolge der Lese- und Schreibvorgänge vor dem Ausführen eines Programms statisch zu bestimmen. Es gibt viele Fälle, in denen Code tatsächlich nützliche Daten in jedem Slot speichert, bevor er gelesen wird, aber es schwierig ist, dies zu beweisen. Daher ist es häufig erforderlich, Programme auszuführen, bei denen es zumindest theoretisch möglich ist, dass der Code versucht, etwas zu lesen, das noch nicht mit einem nützlichen Wert geschrieben wurde. Unabhängig davon, ob dies für Code zulässig ist oder nicht, gibt es keine allgemeine Möglichkeit, den Versuch von Code zu stoppen. Die Frage ist nur, was passieren soll, wenn das passiert.

Unterschiedliche Sprachen und Systeme verfolgen unterschiedliche Ansätze.

  • Ein Ansatz wäre zu sagen, dass jeder Versuch, etwas zu lesen, das nicht geschrieben wurde, einen sofortigen Fehler auslöst.

  • Ein zweiter Ansatz besteht darin, dass Code an jeder Stelle einen Wert bereitstellen muss, bevor er gelesen werden kann, auch wenn der gespeicherte Wert semantisch nicht sinnvoll wäre.

  • Ein dritter Ansatz besteht darin, das Problem einfach zu ignorieren und alles "natürlich" geschehen zu lassen.

  • Ein vierter Ansatz ist zu sagen, dass jeder Typ einen Standardwert haben muss, und jeder Slot, der nicht mit etwas anderem geschrieben wurde, wird standardmäßig auf diesen Wert gesetzt.

Ansatz 4 ist weitaus sicherer als Ansatz 3 und im Allgemeinen billiger als Ansatz 1 und 2. Dann bleibt die Frage, wie der Standardwert für einen Referenztyp lauten soll. Für unveränderliche Referenztypen ist es in vielen Fällen sinnvoll, eine Standardinstanz zu definieren und zu sagen, dass der Standard für jede Variable dieses Typs ein Verweis auf diese Instanz sein sollte. Für veränderbare Referenztypen wäre dies jedoch nicht sehr hilfreich. Wenn versucht wird, einen veränderlichen Referenztyp zu verwenden, bevor er geschrieben wurde, gibt es im Allgemeinen keine sichere Vorgehensweise, außer zum Zeitpunkt des Verwendungsversuchs zu fangen.

Semantisch gesehen, wenn man ein Array customersvom Typ Customer[20]hat und es versucht, Customer[4].GiveMoney(23)ohne etwas gespeichert zu haben Customer[4], muss die Ausführung abfangen. Man könnte argumentieren, dass ein Customer[4]Leseversuch sofort abfangen sollte, anstatt zu warten, bis der Code versucht GiveMoney, aber es gibt genügend Fälle, in denen es nützlich ist, einen Slot zu lesen, herauszufinden, dass er keinen Wert enthält, und diesen dann zu nutzen Informationen, die besagen, dass der Leseversuch selbst fehlschlägt, sind oft ein großes Ärgernis.

In einigen Sprachen kann angegeben werden, dass bestimmte Variablen niemals Null enthalten dürfen, und jeder Versuch, eine Null zu speichern, sollte einen sofortigen Trap auslösen. Das ist eine nützliche Funktion. Im Allgemeinen muss jedoch jede Sprache, die es Programmierern ermöglicht, Arrays von Referenzen zu erstellen, entweder die Möglichkeit von Null-Array-Elementen zulassen oder die Initialisierung von Array-Elementen auf Daten erzwingen, die möglicherweise nicht aussagekräftig sind.

Superkatze
quelle
Würde ein Maybe/ Optiontype das Problem mit # 2 nicht lösen, da Sie, wenn Sie noch keinen Wert für Ihre Referenz haben, aber in Zukunft einen haben werden, diesen einfach Nothingin einem speichern können Maybe <Ref type>?
Doval
@Doval: Nein, es würde das Problem nicht lösen - zumindest nicht ohne die erneute Einführung von Nullreferenzen. Sollte sich ein "Nichts" wie ein Mitglied des Typs verhalten? Wenn ja, welches? Oder sollte es eine Ausnahme auslösen? In welchem ​​Fall geht es Ihnen besser, als einfach nullrichtig / sinnvoll zu verwenden?
CHAO
@Doval: Sollte der Hintergrundtyp von a List<T>a T[]oder a sein Maybe<T>? Was ist mit dem Backing-Typ eines List<Maybe<T>>?
Supercat
@supercat Ich bin mir nicht sicher, wie ein Backing-Typ MaybeSinn macht, Listda er Maybeeinen einzelnen Wert enthält. Meinten Sie Maybe<T>[]?
Doval
@cHao Nothingkann nur Werten vom Typ zugewiesen werden Maybe, es ist also nicht ganz wie beim Zuweisen null. Maybe<T>und Tsind zwei verschiedene Typen.
Doval