Monday, October 25, 2010

Safe De-Reference? NoPE!

Die herrliche Nullpointerexception (auch "NPE" bzw. "NoPE" von ihren Fans genannt) ist einer der treuesten und langlebigsten Begleiter eines Java-Entwicklers. Auch andere Sprachen wie C#, C und ähnlichen Sprachen kennen das Problem, zum Teil sogar mit noch fataleren Folgen. Die Frage die sich aber doch stellt ist: Warum ist es so? Warum ist die NoPE immer und überall König der Exceptions / Abstürze / Speicherfehler?

Ursachen
  1. Null ist in vielen Sprachen wie Java vereinbarter Kontrakt "optionaler" Werte, d.h. von Werten die vllt nicht da sind. Ein Beispiel dafür sind Lookup-Operationen, bei denen ich mit einem Schlüssel nach einem Wert zu suche: Diese Operation muss nicht immer einen Wert zurückliefern - falls es zu dem Schlüssel keinen Wert gibt muss ich "nichts" zurücklieferen. => null. (Bsp: java.util.Map.get(key))
  2. Nicht initialisierte Werte: Null wird auch dann benutzt wenn ein Wert eines Datentyps, z.B. die Property von einem Objekt, im Konstruktor nicht gesetzt wurde. Standardmäßig setzen viele Programmiersprachen in diesem Fall den default "null" => wieder "nichts".
  3. Designfehler: Während die beiden bisherigen Varianten meist in der Sprache vereinbart sind, gibt es auch noch den dritten Fall: Hilflosigkeit. Viele Menschen, so auch Entwickler, scheinen Fehler lieber vertuschen zu wollen statt um Hilfe zu schreien. In Programmcode ausgedrückt bedeutet dies, dass sie in Objektfeldern null setzen oder aus Methoden null zurückgeben wenn etwas fehlgeschlagen ist dass funktionieren muss. Ein Beispiel dafür sind kritische Initialisierungsroutinen ohne die das Programm nicht durchgeführt werden kann, die aber z.B. durch Fehler in Dateien dennoch fehlschlagen können. Was ist die Konsequenz: => in irgendeinem Feld lauert jetzt.. null.
Das sind die drei Standardfälle in denen Null verwendet wird:
  •  Leerer Defaultwert
  • "Nichts"-Rückgabewert
  • Ergebnis einer fehlgeschlagenenen Operationen.
Alle diese Fälle ziehen einen Rattenschwanz von Problemen nach, denn ob ein Wert null sein kann ist für die Entwickler, die die Routinen verwenden meist kaum zu erkennen. Die meisten Routinen dokumentieren zudem mögliche null-Werte nicht oder führen sie erst nach einem Refactoring ein. Eine NoPE kann so selbst dann eintreten, wenn der Entwickler, der die Funktion benutzt, eine für Entwickler unübliche Vorsicht hat walten lassen. Tja.. und was machen wir nun?

Werden wir für immer dazu verdammt sein, unseren Code mit Nullchecks an den merkwürdigsten Stellen auszustatten und vor jedem Aufruf einer Funktion Stoßgebete an den Vater der Maschine senden müssen? 

Hm. Ich denke es gibt durchaus erfolgversprechendere Lösungen.

Warum ist null kein Fehlerwert, und was macht man stattdessen?
Eliminieren wir von den zuerst genannten drei Fällen direkt die Nummer #3.
null als Fehlerwert zu verwenden, wenn etwas fehlschlägt ist:
(a) die häufigste Ursache von NoPE'. Man rechnet nicht mit Nullwerten an diesen Stellen und kann meist gar nicht weitermachen, wenn bestimmte kritische Werte auf einmal null sind. Eine Nullprüfung würde hier meist nicht weiterhelfen.
(b) Sachlich falsch. Was vorliegt, ist meist einfach nur ein Fehler, und kein leerer Wert.. Die einzig vernünftige Reaktion auf einen Fehler ist aber das Werfen einer Exception (=> eskalieren statt aussitzen). Wenn etwas unerwartet fehlschlägt, ist dies die einzige Art und Weise (in Java und C#) dies eindeutig zu kommunizieren und auch direkt klarzumachen, von wo der Fehler gekommen ist.
Also: Werft eine (checked) Exception wenn etwas nicht behandelbar ist!
Vergesst null für diesen Fall! Der Caller wird meist dazu gezwungen sein, auf null mit einer Exception zu reagieren. Da könnt ihr ihm gleich zuvorkommen.

Warum ist Null als "Nichts" einfach nur falsch?
Es bleiben nur noch zwei Fälle übrig, die semantisch aber effektiv dasselbe Konstrukt behandeln: Null wird hier gleichgesetzt mit "nichts", d.h. null repräsentiert die Unterscheidung zwischen einem gesetzten Wert und dem Fehlen desselben. Dieses Verhalten ist, auch wenn es sich in fast allen Programmiersprachen durchgesetzt hat, sachlich vollkommen falsch.

Ein Hinweis darauf, dass null nicht "nichts" íst, liefert schon der Compiler: Er ist nämlich in allen Sprachen, die null-referenzen erlauben, nicht in der Lage zwischen null und einem gesetzten Wert zur Compilezeit zu unterscheiden. Ergo: Die falsche Verwendung von null kann nicht durch einen Compiler geprüft werden! Den besten Hinweis darauf liefert die Tatsache, dass die Verwendung von null als Wert zu einem Laufzeitfehler führt, nämlich zu einer NoPE!

Laufzeitfehler sind aber, der "hochgelobten" Typprüfung zum Trotz, nichts anderes als Hinweise darauf dass hier das Typsystem der statischen Sprachen wie Java schlicht und ergreifend unvollständig ist. Das mag einer der Gründe dafür sein dass dynamische Sprachen sich einer solchen Beliebheit erfreuen: Die Typsysteme der meisten statischen Sprachen sind an den kritischsten Stellen löchrig: Es gibt Konstrukte, die Compilersicherheit auch bei optionalen Werten ermöglichen. (Dazu später mehr). Dennoch wurde sich bei Sprachen wie Java von vornherein dagegen entschieden diese anzubieten.
Aber warum sollte man ein statisches Typsystem verwenden, dass einen an den wichtigen Stellen unnötig im Stich lässt?

Ich werde die korrekte und triviale "Nichts"-Interpretation, die für optionale Werte nötig ist, in meinen nächsten Post vorstellen. Sie stammt ursprünglich und zwangsläufig aus den funktionalem Umfeld Haskells, das den Wert "null" für Referenzen gar nicht kennt.

No comments:

Post a Comment