
Der britische Informatiker Sir Charles Antony Richard Hoare, besser bekannt als Tony Hoare, gilt als Erfinder des Quicksort-Algorithmus. Zu seinen weiteren Errungenschaften zählen das Hoare-Kalkül, die formale Sprache CSP (Communicating Sequential Processes) und seine Arbeit an der Programmiersprache ALGOL W, in der er die Nullreferenz einführte.
Auf der QCon-Konferenz in London im Jahr 2009 reflektierte er über seine Erfindung der Nullreferenz und bezeichnete sie als „Billion Dollar Mistake“:
I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
Tony Hoare
Warum überhaupt eine Nullreferenz
Eine Variable zeigt auf einen Wert. Sie referenziert ihn. Referenziert eine Variable null
, so kennzeichnet dies die Abwesenheit eines Wertes. Es kann im Speziellen bedeuten, dass eine Variable bereits deklariert, aber noch nicht initialisiert wurde. Eine Nullreferenz kann sich positiv auf die Performance und den Speicherbedarf eines Programmes auswirken, da in Abwesenheit eines Wertes kein zusätzlicher Speicher belegt und keine Objektrepräsentation geladen wird.
Mit einer Nullreferenz kann in einer Bedingung sehr einfach und leicht erkennbar auf die Abwesenheit eines Wertes geprüft werden, wie die folgenden Beispiele verdeutlichen:
In C:
if (foo == NULL)
In C#:
if (foo == null)
In Go:
if foo == nil
In Java:
if (foo == null)
In JavaScript:
if (foo == null)
In Python:
if foo is None:
Eine Nullreferenz kann auch zur verzögerten (lazy
) Instanziierung verwendet werden, bei der ein Objekt erst dann erstellt wird, wenn es tatsächlich benötigt wird. In einigen Sprachen wie C und Go wird null
auch zur Fehlerprüfung verwendet: Gibt eine Funktion, die beispielsweise eine Datenbankverbindung aufbauen soll, null
zurück, so ist bei der Verarbeitung des Befehls ein Fehler aufgetreten. Über die allgemeine Abwesenheitsprüfung hinaus hat null
in verschiedenen Kontexten domänenspezifische Bedeutungen:
- In einer verketteten Liste zeigt
null
das Ende der Liste an, da der letzte Knoten keine weitere Referenz besitzt. Diese Konvention hilft dabei, die Struktur korrekt zu durchlaufen, ohne über das Ende hinauszugehen. - In Go blockiert ein
nil
-Channel jegliche Operationen, wodurch er effektiv deaktiviert ist. Dies wird häufig genutzt, um bestimmteselect
-Bedingungen gezielt auszuschalten, ohne den Code selbst zu ändern. - Wenn ein Cache-Eintrag abläuft, wird er oft auf
null
gesetzt, um ihn als ungültig zu markieren. So können Programme erkennen, dass der Wert neu geladen werden muss, anstatt veraltete Daten zu verwenden. - In React wird
null
häufig verwendet, um Komponenten bedingt nicht zu rendern. Durch die Rückgabe vonnull
können Entwickler verhindern, dass eine Komponente in die Benutzeroberfläche geladen wird, falls sie nicht benötigt wird.
Die Fallstricke der Nullreferenz
Der vermeintliche Performancegewinn und Komfort durch die Nullreferenz hat jedoch einen hohen Preis. Der bekannteste Stolperstein ist wohl die unabsichtliche Null-Dereferenzierung. In den meisten Sprachen führt dies dazu, dass der betroffene Thread abstürzt oder eine Exception ausgelöst wird, die ohne Behandlung zum gleichen Resultat führt.
Dies bedeutet, dass theoretisch jede Variable vor ihrer Nutzung auf null
geprüft werden müsste, was die Lesbarkeit des Codes stark beeinträchtigt. Es lässt sich argumentieren, dass diese Prüfungen nur an öffentlichen Schnittstellen einer Unit erforderlich sind, wenn intern kein null
genutzt wird. Jedoch handelt es sich hierbei um einen impliziten Vertrag, der jederzeit gebrochen werden kann – besonders, da Projektmitglieder wechseln und das Wissen über den Code fluktuiert. Dies macht den Code anfällig für Fehler und schwer zu warten, da Nullprüfungen schnell übersehen werden können.
Tools wie JaCoCo, die die Testabdeckung messen, erkennen nicht automatisch alle Randfälle, in denen null
als Wert möglich ist. Dies kann eine trügerische Testabdeckung suggerieren, die Schwachstellen übersieht.
Kommt es zu einer NullPointerException
, wie sie in der Programmiersprache Java heißt, so ist selbst bei Vorliegen eines Stacktraces nicht garantiert, dass sich aus dem protokollierten Fehler die tatsächliche Ursache eindeutig erschließt.
In Summe führt das Konzept der Nullreferenz zu erheblichem Mehraufwand in Entwicklung, Testing und Debugging und stellt eine häufige Fehlerquelle dar.
Die Nullreferenz als Gegenpol zu Software Craftsmanship
Software Craftmanship, geprägt von Persönlichkeiten wie Robert C. Martin, Dave Thomas und Andy Hunt, versteht Softwareentwicklung als Handwerk. Zentral dabei sind technische Exzellenz, stetiges Streben nach Verbesserung und das Schaffen hochwertigen, sauberen und wartbaren Codes.
In vielen Fällen widerspricht der Einsatz von null
diesen Grundsätzen direkt. Der Wert null
verschleiert die eigentliche Absicht und erfordert Kommentare seitens des Autors oder Annahmen seitens der Person, die versucht, den Code zu verstehen. null
stellt eine Art unausgesprochenen Vertrag dar, der von Entwicklerseite Annahmen verlangt, statt klare, explizite Bedingungen festzulegen. Während Software Craftsmanship Leitsätze wie das Single Responsibility Principle hochhält, wird null
für alle möglichen Zwecke verwendet, wenn es gerade passt.
Viele Entwickler preisen die Vorteile statischer Typisierung an: Klare Typdeklarationen fördern das Verständnis. Typfehler werden bereits zur Entwicklungs- oder Kompilierzeit erkannt. Typangaben fungieren als Dokumentation, erleichtern zukünftige Änderungen und verringern das Risiko typbasierter Fehler zur Laufzeit. Alle diese Vorteile werden durch die Möglichkeit einer Nullreferenz abgeschwächt, da jederzeit damit gerechnet werden muss, dass der Aufruf einer Methode scheitert, weil es sich um eine Nullreferenz handelt.
In Summe untergräbt null
viele der Prinzipien, die Software Craftsmanship zu fördern sucht. Der Wert null
steht oft im Widerspruch zu Klarheit, Vorhersehbarkeit und technischer Exzellenz im Code. Während Software Craftsmanship auf eindeutige Verträge, saubere Schnittstellen und Robustheit setzt, bleibt null
ein vages Konzept, das in vielen Fällen Fehlerquellen schafft, statt sie zu vermeiden.
Nicht-null-fähige Typen
Viele moderne Sprachen bieten standardmäßig nicht-null
-fähige Typen an, unterstützen aber weiterhin das Konzept der Nullreferenz. Das bietet einen großen Vorteil: Als Entwickler kann ich mich bewusst für den Einsatz von null
entscheiden. Ist ein Typ null
-fähig, so zeigt dies, dass der Einsatz von null
bewusst in das Design integriert wurde. In Sprachen wie Kotlin ermöglichen null
-fähige Typen eine bessere Kompatibilität mit Java-Frameworks, die null
voraussetzen, etwa in der Serialisierung oder Dependency Injection.
Im Folgenden werden Beispiele für die Deklaration und Zuweisung einer null
– und nicht-null
-fähigen Variablen in verschiedenen Sprachen gezeigt. Viele Sprachen verwenden das Fragezeichen als Kennzeichnung für null
-fähige Typen.
In Dart:
String? nullable = null;
String nonNullable = "foo";
In Kotlin:
var nullable: String? = null
var nonNullable: String = "foo"
In Swift:
var nullable: String? = nil
var nonNullable: String = "foo"
In TypeScript mit strictNullChecks:
let nullable: string | null = null;
let nonNullable: string = "foo";
Damit ermöglichen null
-fähige Typen eine bewusste und klare Entscheidung für oder gegen null
und fördern so eine fehlertolerantere Codebasis.
Das Nullobjekt
Bei dem Nullobjekt handelt es sich um ein Entwurfsmuster, das eine sicher derefenzierbare Alternative zu null
darstellt. Zur Implementierung leitet man eine Klasse ab, die alle Operationen so überschreibt, dass das Objekt nichts tut. Eine Umsetzung als Singleton-Objekt reduziert den Speicherbedarf.
Das Nullobjekt bietet eine solide Alternative, um die Abwesenheit eines Wertes darzustellen. Dennoch ist es oft nicht die beste Wahl, insbesondere im Vergleich zur nun folgenden Lösung.
Optionale Typen
Eine elegante Lösung für den Umgang mit fehlenden Werten stammt aus der Kategorientheorie, einem Teilgebiet der Mathematik. Mit dem Aufleben funktionaler Programmierung in den letzten Jahren hat sie zunehmend an Popularität gewonnen.
Monaden, vereinfacht gesprochen, lassen sich als Container für Werte und Berechnungen beschreiben. Der Typ Maybe
(in vielen Sprachen auch als Option
oder Optional
bekannt) bietet einen solchen Container, der entweder einen Wert enthält oder leer ist.
In Sprachen wie Java bieten solche Container Methoden, um festzustellen, ob sie einen Wert enthalten, diesen zu extrahieren oder bei Abwesenheit einen Standardwert zurückzugeben. In vielen Sprachen können Monaden mit Pattern Matching kombiniert werden, was die Lesbarkeit und Erweiterbarkeit des Codes verbessert, aber auch die Vollständigkeit der behandelten Randfälle durch den Compiler garantieren kann, wenn kein catch-all-Fall implementiert wird. In Sprachen wie Java und Kotlin lässt sich dies durch den Einsatz von Sealed Classes erreichen.
Die folgenden Beispiele demonstrieren die Verwendung des Typs in unterschiedlichen Sprachen:
In F#:
let foo: string option = Some "foo"
match foo with
| Some _ -> printfn "enthält einen Wert"
| None -> printfn "ist leer"
In Haskell:
checkFoo :: Maybe String -> String
checkFoo (Just _) = "enthält einen Wert"
checkFoo Nothing = "ist leer"
main :: IO ()
main = do
let foo = Just "foo"
putStrLn (checkFoo foo)
In Java:
Optional<String> foo = Optional.of("foo");
foo.ifPresentOrElse(
value -> System.out.println("enthält einen Wert"),
() -> System.out.println("ist leer")
);
In Rust:
let foo: Option<&str> = Some("foo");
match foo {
Some(_) => println!("enthält einen Wert"),
None => println!("ist leer"),
}
In Scala:
val foo: Option[String] = Some("foo")
foo match {
case Some(_) => println("enthält einen Wert")
case None => println("ist leer")
}
Dass optionale Typen kein Alleinstellungsmerkmal moderner High-Level-Sprachen mit einem hohen Maß an Abstraktion sein müssen, demonstriert die Sprache Rust, die ähnlich performant wie C und C++ ist und mittlerweile offiziell im Linux-Kernel Einzug erhalten hat.
Fazit
Die Nullreferenz hat in der Softwareentwicklung über Jahrzehnte hinweg ihren Platz behauptet, obwohl sie eine häufige Fehlerquelle darstellt. Ihre Einführung mag ursprünglich aus praktischen Gründen erfolgt sein, doch das „Billion Dollar Mistake“ zeigt die weitreichenden Probleme, die mit ihrer Verwendung verbunden sind.
Moderne Programmiersprachen bieten inzwischen vielseitige Alternativen: Nicht-null
-fähige Typen, das Nullobjekt-Entwurfsmuster und optionale Typen ermöglichen es, fehlende Werte sicher und leserlich zu behandeln und robusteren und besser verständlichen Code zu schaffen, von dem am Ende alle profitieren.