Subtyping für Werte: Mehr Ausdrucksstärke, mehr Sicherheit

Vererbung, das zentrale Merkmal objektorientierter Sprachen, kann erhebliche Probleme aufwerfen. Dieser Artikel zeigt, wie eine geschickte Kombination vorhandener Sprachmittel sie verhindert und das Sprachdesign ausdrucksstärker und sicherer gestaltet.

Sprachen  –  10 Kommentare
Subtyping für Werte: Mehr Ausdrucksstärke, mehr Sicherheit

Ein früherer Beitrag zeigte, dass viele objektorientierte Sprachen Programmierer aufs Glatteis führen und eine spezielle Sorte Fehler geradezu provozieren: die Anwendung von Vererbung bei Werten. Ursache des Problems ist, dass Vererbung aus einer Verquickung zweier verschiedener Sprachbausteine besteht: Subclassing und Subtyping. Subclassing (das automatische, implizite Übertragen der Implementierung der Basisklasse auf die Subklasse) verträgt sich aber schlecht mit Werten (unveränderlichen Abstraktionen mit fester Wertemenge): Subclassing führt bei Werten nicht zu einer Subtypbeziehung und verursacht bei ihnen oft Paradoxien und widersprüchlichen Code.

Subtyping bei Werten durch implizite Konvertierung

Subtypbeziehungen können im Prinzip aber auch bei Werten vorkommen. Zum Beispiel lassen sich Ganzzahlen als Subtyp der Gleitkommazahlen betrachten, diese wiederum als Subtyp der komplexen Zahlen. Das ist nicht nur eine theoretische Sichtweise, sondern hat konkrete Auswirkungen. Wenn sich ein Ganzzahlausdruck überall dort verwenden lässt, wo ein Gleitkomma-Ausdruck erwartet wird, dann vereinfacht das die Programmierung erheblich. Es erlaubt zum Beispiel gemischte Ausdrücke wie 1 + 3.25, 5.25 * 3 oder sqrt (5). Tatsächlich sind sie in vielen Sprachen zulässig; es wäre umständlich, sie nicht direkt darstellen zu können.

Bei vordefinierten Datentypen wie int und float funktioniert die Ersetzbarkeit aufgrund impliziter Typumwandlung, je nach Sprache auch als "coercion" oder "conversion" bezeichnet. Vordefinierte Datentypen sind Bestandteil der Programmiersprache. Welche impliziten Konvertierungen zwischen ihnen zulässig sind und wie sie funktionieren, ist ebenfalls in der Sprachspezifikation festgelegt.

In einigen Sprachen gibt es implizite Konvertierungen nicht nur bei vordefinierten Datentypen, sie lassen sich auch von Programmierern zwischen beliebigen Klassen definieren. Scala und C# unterstützen "user-defined implicit conversions", Eiffel "type conversions". Beispielsweise definiert in Scala die folgende Methode Double2Complex eine implizite Konvertierung von Double in
Complex:

implicit def Double2Complex(value : Double) = new Complex(value, 0.0)

Mit einer impliziten Typumwandlung lässt sich zwischen zwei Werttypen eine Subtypbeziehung herstellen. Aufgrund der eben definierten Umwandlung Double2Complex können Entwickler jeden Ausdruck vom Typ Double dort verwenden, wo eine komplexe Zahl zulässig ist; Double wird auf diese Weise zu einem Subtyp von Complex.

Aber nicht jede Typumwandlung entspricht einer Subtypbeziehung; sie werden oft auch für andere Zwecke eingesetzt. Beispielsweise nehmen einige Sprachen implizite Umwandlungen von Gleitkommazahlen in Ganzzahlen vor ("narrowing conversion") – durch Runden oder durch Abschneiden der Nachkommastellen. C++ konvertiert sogar ungerührt int in bool und umgekehrt.

Einer der Gründe für den Einsatz einer impliziten Konvertierung ist Bequemlichkeit: der Wunsch, den Aufruf expliziter Konvertierungsroutinen einzusparen und so den Code zu verkürzen. Java konvertiert zum Beispiel jeden Ausdruck implizit in einen String, wenn er über einen Operator "+" mit einem anderen String verknüpft wird. Beispiel:

int result = ....
System.out.print("Ergebnis: " + result);

Implizite Konvertierungen kommen auch zum Einsatz, um eine Klasse um zusätzliche Operationen anzureichern; beispielsweise ist das in Scala ein übliches und verbreitetes Verfahren ("pimp my library"). Zum Beispiel werden Methoden, die der Klasse String fehlen – etwa einen String rückwärts ausgeben oder auf das Vorhandensein von Großbuchstaben prüfen – in einer zusätzlichen Klasse RichString programmiert, dazu eine implizite Konvertierung von String in RichString. Die Methode stringWrapper (definiert im Objekt PreDef) ist eine solche implizite Konvertierung von String in RichString:

implicit def stringWrapper(x: String) = new runtime.RichString(x)

Auf diese Weise stehen die Operationen von RichString auch für String zur Verfügung. Den gleichen Effekt können Programmierer aber statt durch implizite Konvertierung auch mit Mechanismen wie Erweiterungsmethoden erreichen, wie C# oder Kotlin sie unterstützen.

Ganzzahltypen wie int bilden keinen Subtyp von String. Selbst der Zusammenhang zwischen String und RichString lässt sich allenfalls als "degenerierte" Subtypbeziehung interpretieren: Strings bilden keine echte Teilmenge von RichStrings, letztere verfügen lediglich über zusätzliche Operationen.

Das Herstellen einer Subtypbeziehung zwischen zwei Werttypen ist also nur ein Anwendungsfall für implizite Konvertierungen unter vielen. Dazu kommt, dass die oben genannten Sprachen, die programmierdefinierte Konvertierungen unterstützen, ihren Einsatz uneingeschränkt für alle Typen erlauben; sie begrenzen ihn nicht auf "wertartige" Typen wie Complex oder String.