Ach, was wäre doch nur Programmieren ohne dem Lieblingsbug der .NET Entwickler: die NullReferenceException. So gut wie jeder Programmierer hat sie mindestens einmal gesehen und sich gewundert, warum beispielsweise ein bestimmtes Objekt nicht initialisiert ist. Oder schlimmer: Warum eine Methode null anstelle einer Objektreferenz zurückgibt.
Der Wert null ist ein wichtiger Bestandteil des .NET
Frameworks, da dies der Standardwert eines jeden Referenztypen ist. Solange man
einer Variable eines Referenztypen keinen Wert zuweist, muss man auf irgendeine
Art und Weise dessen Abwesenheit im Programmcode repräsentieren. Genau dies
wird mit null erreicht. Ein weiterer
häufiger Anwendungsfall sind die sogenannten "Nullable Value Types", bei denen es
möglich ist, einem Wertetypen null zuzuweisen. Dies ist insbesondere beim
Arbeiten mit Datenbanktabellen praktisch, da in vielen Datenbanken die
Abwesenheit eines Wertes in einer Tabelle mit null repräsentiert wird. Diese
„Abwesenheit“ eines Wertes kann beispielsweise in einer normalen Int32 Variable
nicht ohne weiteres abgebildet werden.
Beispiel: Die Spalte Alter in einer Tabelle hat für die Person X keinen
Eintrag. Soll das Alter der Person auf „0“ gesetzt werden, um die Abwesenheit
des Wertes in der Datenbank zu repräsentieren ? Wäre „-1“ ein geeigneter Wert,
und wenn ja, hat man den Setter des Properties für diesen Sonderfall angepasst?
Und wie würde das Problem bei dem Datentyp byte statt Int32 aussehen ? Fragen
über Fragen…
Während das Setzen auf null für Wertetypen ein reines Opt-In
ist, kommen wir bei Referenztypen oftmals nicht drum herum, null zu verwenden.
Standardmäßig kann man in den meisten Fällen aber davon ausgehen, dass die zur
Laufzeit verwendeten Objekte initialisiert sind und zur Verwendung zur
Verfügung stehen. Ab und zu schleicht sich aber doch ein kleiner Implementierungsfehler
ein, der im weiteren Programmablauf zu einer NullReferenceException führt.
Mein persönlicher Favorit ist das unabsichtlich vergessene Instanziieren von
Objekten, viele kostbare Minuten sind bei mir schon auf der Suche nach diesem
simplen Fehler draufgegangen.
Grundsätzlich kann man in sehr vielen Fällen davon ausgehen,
dass ein Objekt zum Zeitpunkt der Verwendung initialisiert ist und uns zur
Verfügung steht. Gelegentlich kann es aber auch vorkommen, dass wir das nicht-Vorhandensein
eines Objektes durch den Wert null repräsentieren müssen. Ein (leider)
beliebtes Antipattern: Eine Methode, die, aus welchem Grund auch immer, null
anstelle der in der Signatur definierten Objektinstanz zurückliefert.
Wenn man sich bewusst auf dieses Antipattern einlässt, muss man im weiteren
Programmablauf dauernd prüfen, ob die Rückgabe eine funktionierende
Objektreferenz oder doch null ist. Der Code wird dann meist mit lauter
Verzweigungen zugepflastert, die auf null prüfen. Die Einführung des
"Null-Conditional-Operators" in C# 6.0 hat diese Situation glücklicherweise ein
wenig verbessert.
Doch was kann man nun tun ? Gänzlich auf die Verwendung von
Referenztypen verzichten ? Null komplett verbieten ? Die funktionale
Programmiersprache F# hat hierfür einen interessanten Ansatz:
Alles ist per default Immutable und null ist als regulärer Wert nicht
zugelassen.
Für den „Es ist kein Wert vorhanden“ – Fall kann man beispielsweise
"Option-Types" verwenden:
Die Rückgabe einer Funktion kann für den Datentyp T „Some(T)“ oder „None“ sein.
Mithilfe von Pattern-Matching ist der Programmierer dann in weiterer Folge dazu
gezwungen, beide Fälle zu behandeln. Ein erzwungener Null-Check sozusagen.
Schauen wir uns das einmal genauer an:
Der folgende C#-Code kann ohne Probleme kompilieren, wird aber zur Laufzeit eine NullReferenceException werfen. Das liegt daran, dass null ein gültiger Wert für einen String ist.
In F# hingegen können wir null nicht direkt übergeben. Um
einen „fehlenden Wert“ zu repräsentieren, verwendet man sogenannte
Option-Types. Die Möglichkeiten hierfür wären "Some" (= ein Wert ist vorhanden)
und "None" (= kein Wert vorhanden). Pointer oder „nicht initialisierte Variablen“
gibt es in der funktionalen Sprache F# nicht.
Beim direkten Zuweisen von "None" merken wir sofort, dass der
Compiler das Auslesen der Length-Eigenschaft nicht zulässt.
Wenn wir also mit dem String arbeiten wollen, müssen wir das
sogenannte Pattern-Matching verwenden:
Hier sehen wir, dass wir den Wert des Option-Types durch das
Pattern-Matching sauber herausbekommen: Wenn in der Variable "text2" ein Wert
vorhanden ist, greift der „Some“ – Fall und die Länge des Strings wird
ermittelt. Falls kein Wert vorhanden ist, greift der „None“ – Fall und es wird
0 zurückgegeben.
Wo liegt nun aber der Unterschied zu C# ? Dasselbe Problem
können wir in C# auch mit folgendem Code lösen:
In C# ist ein „null String“ vom Datentyp her immer noch ein
String. Zum Kompilierzeitpunkt kann daher nicht unterschieden werden, ob eine
gültige Zeichenkette wie „Hallo Welt“ oder null drinnensteht. Erst zur Laufzeit
bekommen wir im schlimmsten Fall eine NullReferenceException.
Wir können natürlich wie im obrigen Beispiel dauernd auf null prüfen und
Alternativwerte angeben, doch das ist komplett optional. Wenn wir in unserer
Businesslogik nur ein paar Nullchecks unabsichtlich vergessen, könnte es
irgendwann einmal zu einer NullReferenceException kommen.
In F# hingegen würden man beispielsweise Option-Types
nutzen. Mit denen hat man den Vorteil, dass man nicht direkt auf Properties wie
Length oder Methoden des darunterliegenden Strings zugreifen kann. Somit muss
man mithilfe des Pattern-Matchings den Wert sozusagen erst „auspacken“, ein
erzwungener Null-Check. Alle potentiellen Fehler oder Versäumnisse bekommen wir
also entweder in der IDE oder beim kompilieren mit.
C# 8.0
Wozu also nun die Gegenüberstellung von C# und F# ? Aktuell wird
für die nächste große Sprachversion von C# an einem Feature gearbeitet, welches
das Problem der ständigen NullChecks lösen könnte: „Nullable ReferenceTypes“
Hierbei wird das Konzept der NullableTypes, welches bei Wertetypen bereits
existiert, auch auf Referenztypen ausgedehnt: Ein Referenztyp darf u.U nicht
mehr null sein !
Eine möglicher Anwendungsfall könnte beispielsweise so
aussehen:
Im Vergleich zu F# bekommt man hier aktuell nur Warnungen,
wenn zum Zeitpunkt X eine Variable null sein könnte. Insbesondere in
Großprojekten kann dieses Feature durchaus praktisch sein. Anfangs wird man
sicher bei der Umstellung mit Warnungen überflutet, doch die eine oder andere
Warnung könnte doch zu einer Stelle führen, wo man unerlaubterweise null als
Wert verwendet hat.
Und jeder Bug weniger bringt uns alle einen großen Schritt
weiter zu sauberem Code und fehlerfreien Projekten !
Für detailiertere Informationen und eine Preview des geplanten Features empfehle ich folgende GitHub - Seite: The C# Nullable Reference Types Preview
Wer mehr über die vielen Möglichkeiten von C# und den neuen Sprachfeatures erfahren möchte, kann auch einen passenden Kurs dazu besuchen.