Ein Jahr JavaScript-Konkurrent Dart

Seite 2: Große Änderungen

Inhaltsverzeichnis

Ein weiterer Block mit mehreren Veränderungen betrifft das Exception Handling. Zur Vermeidung von Fehlern ist es beispielsweise nicht mehr gestattet, eine null zu werfen. Sie geben dem Fänger einer Exception keine gute Hilfestellung zur Fehlerbehebung. Das Werfen haben die Entwickler dafür vereinfacht. Nun ist throw ein Ausdruck und lässt sich daher in Einzeilern nutzen. Das zeigt sich zum Beispiel in

throw const NotImplementedException();

für noch in Arbeit befindliche Methoden, sodass sich frühzeitig Interfaces erfüllen lassen (siehe oben). Gleichzeitig hat sich für das Fangen spezieller Exceptions die Syntax verändert. Sie lautet nun

try { 
...
} on NotImplementedException catch (ex) { ... }

Die Spezialisierung, was gefangen werden soll, wird somit besser von der Bindung an eine Variable getrennt. Das vermeidet die in der Vergangenheit durch die optionale Typisierung aufgetretene Unklarheit, ob ein catch (foo) jede gefangene Exception an die Variable foo binden oder eine Exception vom Typ foo fangen soll.

Eine Eigenschaft der Sprache, die Anhängern von Smalltalk gefallen dürfte, ist die Nutzung der benannten Parameter als Ergänzung zu positionierten Parametern. Beide ließen sich unter Verwendung einer gemeinsamen Syntax bisher optional angeben. So erlaubt die Definition

setRange([num min, num max]) { ... } 

den Aufruf mit beiden, einem oder keinem Parameter. Durch die gemeinsame Syntax für benannte und positionierte Parameter ist

setRange(4711); 

erlaubt und bedeutet, dass min den Wert 4711 und max den Wert null hat. Der erste Schritt zur Verbesserung ist die Differenzierung zwischen benannten und positionierten Parameter, erstere in geschweiften Klammern, letztere wie bisher in eckigen Klammern. Die obige Definition lässt sich in

setRange({num min, num max}) { ... } 

ändern, und der Aufruf lautet nun zum Beispiel

setRange(max: 4711); 

Eine weitere Verbesserung bringen Standardwerte mit sich, die bei der Definition mit einem Doppelpunkt an die Variable gehängt werden.

setRange({num min: 0, num max: 10000}) { ... } 

Nun hätte min bei einem Aufruf wie oben nicht den Wert null, sondern 0. Doch was ist, wenn der Standardwert nicht definiert ist? Wie ist nun zu unterscheiden, ob ein optionaler Wert erlaubterweise mit null gesetzt oder einfach nicht angegeben wurde? Hierfür haben die Entwickler das Fragezeichen als Testoperator eingeführt. Im folgenden Listing lässt sich etwa die Übergabe eines Ports prüfen.

Database open(String addr, [num port]) {
if (?port) {
...
} else {
...
}
}

Eine weitere für Smalltalk-Entwickler bekannte Neuerung ist das Cascading von Methodenaufrufen. Eine Methode ist nicht mehr gezwungen, die eigene Instanz zurückzugeben, um eine Verkettung herzustellen. Vielmehr lassen sich mit dem einfachen Konstrukt zweier Punkte mehrere Methodenaufrufe auf der gleichen Instanz ausführen.

list..add('the')..add('quick')..add('brown')..add('fox'); 

Eine interessante Verhaltensänderung ist das Lazy Init von Variablen mit dem Modifier final. So lässt sich auf oberster Ebene

final cache = new Cache(); 
var addressFile = new AddressFile(cache);

definieren. Erst beim ersten Zugriff auf addressFile, beispielsweise mit addressFile.add(anAddress), werden die beiden Variablen initialisiert. Das Gleiche gilt für Felder mit dem Modifier static final. Auch sie werden erst beim Zugriff initialisiert und sind dann final.

class Transaction { 
static final startTime = new DateTime.now()
}

Nun bedeutet final nicht, dass die Werte konstant sind, nur die Variablen beziehungsweise Felder sind es. Was also tun, wenn man einen konstanten Wert benötigt? Hier erlaubt Dart die Verwendung des Modifiers const, sodass nun

class MyClass {
static const myConstValue = 42;
static const myConstValueList = const [myConstValue];
}

erlaubt ist. Ebenso lassen sich Funktionen der obersten Ebene und statische Methoden in konstanten Ausdrücken nutzen. Bisher wurde darauf verzichtet, weil Closures oder Methoden auf einem externen und nicht konstanten Zustand beruhen. Das trifft jedoch nicht auf Funktionen und statische Methoden zu:

pong() => print('pong');

class Pinger {
static const defaultPinger = const Pinger(pong);

final pinger;
const Pinger(this.pinger);
}

Als weitere Änderung rund um Variablen und Felder im ersten Meilenstein der Sprache lassen sich Instanz-Felder nun auch im Gegensatz zu den bisherigen Versionen mit nicht konstanten Werten initialisieren. So ist die Definition

class Order { 
List<Item> items = <Item>[]
}

erlaubt, und items wird automatisch bei der Instanziierung des Objekts initialisiert. Das geschieht vor der Ausführung des Konstruktors. Die Verwendung von this innerhalb der Feld-Initialisierungen ist nicht gestattet, da eventuell weitere Initialisierungen noch nicht stattgefunden haben.

Mit diesem Milestone implementiert die Klasse Object das Interface Hashable. Damit verfügt jedes Objekt über die Methode hashCode(), auch ohne sie explizit zu implementieren. Als Folge lassen sich alle Objekte als Schlüssel in Maps verwenden. Einzig Map-Literale im Quelltext erwarten einen String als Schlüssel.

Strings haben auch ein paar Veränderungen erfahren. Eine auffallende ist die nun nicht mehr erlaubte Konkatenation mit dem Operator +, da das in gemischten Ausdrücken zu unerwarteten Ergebnissen führen konnte. Das bevorzugte Mittel der Wahl ist jetzt die Interpolation, beispielsweise "Name: ${foo}" oder "Summe: ${1 + 1}". Die Darstellung von Objekten als String lässt sich über die Implementierung der Methode toString() definieren, was dann in der Interpolation zum Einsatz kommt. Werden im Code sogenannte Raw Strings benötigt, greift die letzte Änderung bezüglich der Strings. Bisher wurden sie mit @ eingeleitet. Das Zeichen ist aber nun der Definition von Metadaten vorbehalten. Dafür werden Raw Strings jetzt mit dem kleinen Buchstaben r gekennzeichnet. So führt

var raw = r'Raw String mit \n und ${1 + 1}.'; 

den Zeilenumbruch und die Interpolation nicht aus. Das auf diesem Wege frei gewordene @ kommt dann für Metadaten zum Einsatz, die aus dem Zeichen und einem konstanten Ausdruck bestehen. Sie lassen sich vor der Deklaration von Klassen, Typedefs, Konstruktoren, Fabrikmethoden, Funktionen, Feldern, Parametern oder Variablen aufführen. Die Ausdrücke selbst sind in den Formen @native und @42, aber auch @const <int>[1, 2, 3, 4] möglich. Das Auslesen erfolgt über die noch zu definierende Reflection API. Erste Definitionen wie deprecated und override finden sich in der meta-Bibliothek.