C# 7 – Stand der Dinge und Ausblick

Seite 2: Algebraische Datentypen

Inhaltsverzeichnis

Einen starken Fokus setzt das Roslyn-Team auf Konzepte aus der Welt der funktionalen Programmiersprachen. Daher werden algebraische Datentypen Einzug in die neue Sprachversion halten. In der objektorientierten Welt werden Methoden, die auf Datentypen arbeiten, im Vorfeld definiert. Wenn Entwickler die Methoden auf bestimmte Typen von Objekten anwenden, markieren sie die entsprechenden Methoden als virtuell, was zur Weiterleitung der Aufrufe an entsprechende Subtypen führt. Die Menge der Methoden muss daher im Vorfeld definiert sein. Damit können Entwickler anschließend eine unbestimmte Anzahl verschiedener Typen auf unterschiedlichen Abstraktionsniveaus definieren.

In der funktionalen Welt sieht es etwas anders aus: Hier definieren die Entwickler Daten als eine Menge von Typen. Die Funktionen, die auf diesen Daten arbeiten, definieren sie separat, wobei jede Operation eine Implementierung für jeden Typ in der Typ-Hierarchie anbietet. Das ist sinnvoll, wenn der Autor von Datentypen sämtliche Ausprägungen im Vorfeld definieren kann oder muss. Daraus resultiert die Möglichkeit, eine offene, unbestimmte Menge von Operationen definieren zu können, die auf diesen Daten arbeiten. Genau diese oft nützliche Option bietet C# bisweilen nicht an.

Algebraische Datentypen und Call-by-Pattern-Evaluierung (siehe Pattern Matching) wurden erstmals in der Sprache NPL vorgestellt. Bedeutung erlangten die Konzepte jedoch erst durch die Sprache Hope, die in den 1970er Jahren an der Edinburgh Universität veröffentlicht wurde. Heutzutage sind algebraische Datentypen fester Bestandteil vieler funktionaler Sprachen wie Miranda, Haskell und F#.

Algebraische Datentypen sind eine Art Kompositionstyp. Man kann ihn sich als eine Kombination aus mehreren Typen vorstellen. Es existieren zwei Klassen algebraischer Datentypen. Auf der einen Seite gibt es Produkttypen wie Tupel und Records. Die Werte eines Produkttyps enthalten in der Regel mehrere Werte, die Felder heißen. Alle Werte dieses Typs haben die gleiche Kombination von Feldtypen. Die Menge aller möglichen Werte eines Produkttypen ist das sogenannte mengentheoretische Produkt der Mengen aller möglichen Werte seiner Feldtypen.

Auf der anderen Seite gibt es die Summentypen, auch bekannt als Varianttypen. Die Werte dieses Typs sind in der Regel in verschiedene Klassen gruppiert, die Varianten genannt werden. Sein Wert wird normalerweise mit einem Konstruktor erstellt. Jede Variante hat dabei einen eigenen Konstruktor, der eine bestimmte Anzahl typisierter Argumente entgegennimmt. Die Menge aller möglichen Werte eines Summentyps ist die mengentheoretische Summe.

Nach der theoretischen Übersicht stellt sich die Frage, wie die Umsetzung in C# 7 erfolgen soll.

Das Entwicklerteam der .NET Compiler Platform diskutiert für Tupel-Typen syntaktische Darstellungen wie die folgende:

(int x, int y) t1 = (23, 45);
(int a, int b) t2 = t1; // Identitätskonvertierung
(int, int) t3 = t1; // Identitätskonvertierung

Mit der obigen Darstellung können Tupel von Daten dargestellt werden. Dabei spielen die Namen der einzelnen Werte nur eine zweitrangige Rolle, denn es gibt keinen wirklich guten Grund, warum (int x, int y) einen anderen Typ als (int a, int b) darstellen soll. Lediglich der Zugriff auf die Werte über die Namen hat eine Bedeutung:

Console.WriteLine("{0}, {1}", t1.x, t1.y);

Wie bei Parameterlisten von Methoden spielt die Reihenfolge der Typen eine größere Rolle. Eine Methode wird von einer anderen Methode überschrieben, wenn die Parameterliste dieselben Typen an der gleichen Stelle besitzt. Und so sollen auch Tupel mit denselben Typen an der gleichen Stelle als äquivalent betrachtet werden. Diese Art der Betrachtung erlaubt es ebenfalls, anonyme Tupel zu definieren. t3 im obigen Beispiel zeigt solch eine anonyme Darstellung. Unter der Haube stellt das System die Tupel mit demselben Typ dar. Dazu übersetzt der Compiler eine Tupel-Definition in einen generischen Typ, genauer in einen generischen Wertetyp.

Bezüglich der Typinferenz sehen die Regeln folgendermaßen aus: Stimmen die Namen, Typen und die Reihenfolge aller Parameter der betrachteten Tupel überein, werden die Namen für den abgeleiteten Typ übernommen. Andernfalls bleiben sie unbenannt.

var a1 = new[] { t1, t1 };    // leitet (int x, int y)[] ab,
// da alle Namen übereinstimmen
var a2 = new[] { t1, t2 }; // leitet (int, int)[] ab,
// da nicht alle Namen übereinstimmen
var a3 = new[] { t1, t3 }; // leitet (int x, int y)[] ab,
// da alle mit einem Namen übereinstimmen

Zusätzlich können Tupel in benannter und unbenannter Form verwendet werden. Sie können Zieltypen für die einzelnen Werte definieren, oder mit einem Wert daherkommen, aus dem dann der Typ abgeleitet wird. Der folgende Code soll das veranschaulichen:

var t1 = ("Hello", "World");           // leitet (string, string) ab
var t2 = (first: "John", last: "Doe"); // leitet (string first,
// string last) ab
var t3 = ("Hello", null); // führt zu einem Fehler,
// da null keinen Typ besitzt
var t4 = (first: "John", last: null); // führt zu einem Fehler,
//da null keinen Typ besitzt
(string, string) t5 = ("Hello", null); // setzt den resultierenden Typ
// auf (string, string)
(string first, string last) t6 = ("John", null); // setzt den
// resultierenden Typ auf (string first, string last)

Zwar gibt es bereits seit dem .NET Framework 4 die generische Klasse Tuple. Sie ist allerdings auf Tupel der Größe 8 beschränkt. Die native Unterstützung von Tupeln als Sprachelement in C# 7 ermöglicht eine bessere Lesbarkeit durch kürzeren Code und weitere Optimierungen durch den Compiler.