Ein genauer Blick auf die Sprache aus Embarcadero Prism XE 2

Oxygene for .NET and for Java: The Language
Kommentare

Im ersten Teil haben wir uns mit den generellen Sprachfeatures von Oxygene beschäftigt. Dieser zweite Teil setzt sich mit den Elementen der Sprache auseinander, die einen konkreten Bezug zu einer der unterstützten Plattformen, also entweder .NET oder Java haben. So funktioniert LINQ zum Beispiel derzeit nur unter .NET und die neue Unterstützung von Java im Rahmen des Projekts „Cooper“ hat beispielsweise den Ausschlag für die Implementierung der Inline Interfaces und des Duck Typing gegeben. Dieser Artikel wird diese Features näher beleuchten und die Einsatzmöglichkeiten vorstellen.

Zuerst wollen wir uns einem der Sprachfeatures widmen, das die Softwareentwicklung in den letzten Jahren am meisten beeinflusst und viel Neid auf Seiten anderer Sprachen auf sich gezogen hat: Language Integrated Query oder kurz LINQ. Es erlaubt uns eine SQL-ähnliche Abfrage unmittelbar im Code. Ein konkretes Beispiel dafür ist folgendes:

var addressesToPrint := from entry in AddressList where entry.ZipCode < 12345 and entry.City.StartsWith("A") order by entry.Name select entry.Name, entry.Street, entry.ZipCode, entry.City skip 20 take 10. 

Was diese Zuweisung im Detail macht, sollte aus der Zeile erkennbar sein. Und das ist auch einer der größten Vorteile des Features. LINQ ist hierbei ausschließlich durch Compiler Magic realisiert und setzt das gezeigte Statement intern in andere Sprachelemente um. LINQ verwendet hierzu die folgenden Sprachfeatures: Lambda Expressions als Kurzschreibweise von anonymen Methoden, anonyme Objekte, Generics, Sequenzen beziehungsweise Iteratoren und Extension Methods (diese haben wir bereits im ersten Teil besprochen). Da LINQ alle diese Elemente unmittelbar kombiniert, aber jedes dieser Features auch einzeln für sich ein mächtiges Instrument ist, werden wir auf sie auch einzeln im Detail eingehen.

Allgemeingültiger Code mit Generics

Generics, eingedeutscht Generika, sind Typen, die andere Typen mittels Platzhaltern verwenden. Das beste Beispiel hierfür ist die generische Liste, also eine List<T> (gesprochen „Liste von T“). Das T steht hierbei als Platzhalter für einen allgemeinen Typen, der je nach Bedarf eingesetzt wird. Es kann also letztlich ein Referenztyp wie ein String, ein Wertetyp wie ein DateTime oder auch eine beliebige selbstimplementierte Klasse sein. Die Methoden auf der generischen Klasse sind so implementiert, dass sie den Platzhaltertypen verwenden. So sieht die Signatur der Add-Methode auf der List<T> folgendermaßen aus: method Add(itemToAdd: T);. Das indizierte Property Item, um auf die einzelnen Elemente der Liste zuzugreifen, sieht folgendermaßen aus: property Items[i: Integer]: T;. Generische Klassen werden immer mit einem eindeutig anzugebenden Typen instanziert, zum Beispiel eine List<string> („Liste von Strings“). Bei einer solchen Instanziierung wird der Compiler nun im Hintergrund eine neue Klasse erzeugen, die einen vom Compiler generierten Namen erhält. In dieser konkreten Implementierung wird jedes Vorkommen von T gegen String ersetzt, und kann nun vollkommen typsicher als Liste, die Strings beinhaltet, verwendet werden. Jedes mal, wenn im Code nun an anderer Stelle eine List<string> verwendet wird, wird genau diese vom Compiler generierte Klasse verwendet. Es ist auch ein gängiges Feature, mehrere Platzhalter zu verwenden. Ein gutes Beispiel ist die Klasse Tuple<T, U>, die lediglich zwei Properties namens Item1 und Item2 hat, die jeweils dem Typen des entsprechenden Platzhalters T beziehungsweise U entsprechen. Zudem ist es möglich, Generics auch nur für Methoden zu verwenden. Hierbei ist es egal, ob die Methode auf einer generischen oder auf einer nicht generischen Klasse definiert wird oder ob es sich gar um eine Extension Method handelt. So kann man auf einem beliebigen Objekt zum Beispiel eine Methode TryConvert implementieren, die versucht, über das .NET-interne Type-Conversion-System einen beliebigen Typen in einen anderen umzuwandeln (Listing 1).

uses
  System.ComponentModel;

extension method Object.TryConvert<T>(out target: T): Boolean;
begin
  var sourceType := typeOf(self);
  var destinationType := typeOf(T);

  var sourceConverter: TypeConverter := TypeDescriptor.GetConverter(self);
  if assigned(sourceConverter) and sourceConverter.IsValid(self)
    and sourceConverter.CanConvertTo(destinationType) then
  begin
    try
      target := T(sourceConverter.ConvertTo(self, destinationType));
      exit true;
    except
    end; 
  end; 

  var destinationConverter := TypeDescriptor.GetConverter(destinationType);
  if assigned(destinationConverter)
    and destinationConverter.CanConvertFrom(sourceType) then
  begin
    try
      target := T(destinationConverter.ConvertFrom(self));
      exit true;
    except
    end; 
  end; 

  exit false;
end;

Eine weitere Möglichkeit, die Verwendung von Generics noch etwas mächtiger zu gestalten, liegt in den so genannten Generic Constraints, also in den Einschränkungen auf den Platzhaltern. So kann zum Beispiel durch die Angabe von where T has constructor, T is MyBaseClass, IEquatable eingeschränkt werden, dass jeder Typ, der in den Platzhalter T geschoben wird, einen parameterlosen Konstruktor besitzen muss, der das Interface IEquatable erfüllen und von der Klasse MyBaseClass ableiten muss. Wird eine dieser Bedingungen nicht erfüllt, so gibt es einen Fehler bei der Kompilierung. Die Einschränkungen sind insofern wichtig, als dass man hierdurch innerhalb des generischen Typs auch neue Instanzen von T erzeugen und Methoden beziehungsweise Properties verwenden kann, die auf MyBaseClass und IEquatable zur Verfügung stehen. Somit kann man zum Beispiel sehr einfach Factories für bestimmte Typen erstellen, die neben dem Erzeugen neuer Instanzen diese auch gleich mit bestimmten Werten auf den durch die Basisklasse und/oder Interface bekannten Properties initialisieren.

Aufmacherbild: Bubbles underwater. von Shutterstock / Urheberrecht: Gumenyuk Dmitriy

[ header = Immer schön der Reihe nach ]

Immer schön der Reihe nach

Das nächste Element, das LINQ so mächtig macht, sind (generische) Sequenzen. Eine Sequenz ist eine Menge von Elementen, über die iteriert werden kann: also beispielsweise ein Array, eine List<T>, ein Dictionary oder allgemein gesprochen alles, was das Interface IEnumerable beziehungsweise IEnumerable<T> erfüllt. Um diese Bedingung zu erfüllen, haben wir zwei Möglichkeiten. Zum einen können wir einen Enumerator selbst implementieren (der logischerweise das Interface IEnumerator beziehungsweise IEnumerator<T> erfüllen muss) oder es von einem Compiler automatisch erledigen lassen. An dieser Stelle ist anzumerken, das Iteratoren, die Ergebnisse mit yield herausreichen, einer besonders starken Compiler Magic unterliegen und das dahinter liegende Konzept nicht unbedingt trivial ist. Listing 2 zeigt eine Beispielimplementierung.

method MyClass.GetEvenNumbers: sequence of Int32; iterator;
begin
  for i: Int32 := 0 to Int32.MaxValue step 2 do
    yield i;
end;

method MyClass.UseEvenNumbers;
begin
  for each number in self.GetEvenNumbers do
  begin
    Console.WriteLine(number.ToString());
    if number > 500 then
      break;
  end;

  // alternative Schreibweise zur Erläuterung:
  var enumerator := self.GetEvenNumbers().GetEnumerator();
  while enumerator.MoveNext do
  begin
    Console.WriteLine(enumerator.Current.ToString());
    if enumerator.Current > 500 then
      break;
  end;
end;

Die erste Methode GetEvenNumbers nutzt das iterator– und das yield-Keyword. Das Ergebnis ist, dass der Rückgabewert dieser Methode eine Sequenz von Ganzzahlen ist, die vom Compiler auf eine ganz bestimmte Art und Weise in einen IEnumerator<Int32> umgewandelt wird. Wenn wir uns das Interface IEnumerator im Detail ansehen, finden wir eine Methode MoveNext, die einen Boolean zurückgibt, eine Methode Reset, die den Enumerator wieder zurück auf das erste Element setzen soll und ein Property namens Current, die das aktuelle Element zurückgibt. Die for-each-Schleife in der UseEvenNumbers-Methode verwendet den Enumerator intern also genau so, wie es unter der alternativen Schreibweise angegeben ist.
Die eigentliche Magie hinter dem Iterator ist nun, dass der Compiler um die Schleife in GetEvenNumbers einen Zustandsautomaten generiert. Er führt bei einem Aufruf von MoveNext() den Code in der Methode GetEvenNumbers genau so weit aus, bis ein Wert mittels yield zurückgegeben und in das Current Property geschrieben wird. Der aktuelle Zustand, in diesem Fall also der aktuelle Wert der inneren Schleifenvariable, wird ebenfalls gespeichert, und die Methode wird beendet. Bei dem nächsten Aufruf von MoveNext der äußeren Schleife wird der Zustand der inneren Schleifenvariable aus dem Zustandsautomaten wiederhergestellt und der Code in GetEvenNumbers wieder bis zum nächsten yield ausgeführt. Das passiert bei jedem weiteren Aufruf wieder. Der Vorteil hierbei ist, dass die innere Schleife in GetEvenNumbers auch nur exakt so oft ausgeführt wird, wie sie benötigt wird. In dem Beispiel also 251 Mal, bis die äußere Schleife MoveNext nicht mehr aufruft. Es wird also nur so viel berechnet wie notwendig. Dieser Ansatz ist insbesondere dann spannend, wenn die Berechnung etwas mehr Ressourcen benötigt, um so unnötige Arbeit wie ein Vorberechnen von mehreren Werten zu sparen. Die Kombination aus iterator / yield ist also ein sehr wertvolles Werkzeug bei der Arbeit mit Sequenzen.

Who’s on first?

Die Bezeichnung „anonyme Methode“ für unser nächstes Thema kommt nicht von ungefähr, denn die hier besprochenen Methoden haben keinen Namen im Code und existieren für den Entwickler nur anonym im Speicher der Applikation. Aufgerufen wird sie ganz normal wie ein Delegat (in etwa vergleichbar mit einem Pointer auf diese Methode). Der Compiler wird im Hintergrund aus jeder anonymen Methode allerdings eine real implementierte Methode generieren, die mit einem vom Compiler generierten Namen versehen wird. Wie sieht eine anonyme Methode aber nun in Oxygene aus? Im Prinzip schreibt sich eine anonyme Methode genau so wie eine normale Methode (konsequenterweise ohne Angabe eines Namens), jedoch befindet sie sich innerhalb eines Codeblocks und wird in aller Regel entweder einer Variablen oder einem Event Handler zugewiesen oder aber unmittelbar als Parameter an eine andere Funktion übergeben. Ein einfaches Beispiel mit der Zuweisung an eine Variable ist in Listing 3 aufgeführt.

method MyClass.CreateAndUseAnAnonymousMethod: Integer;
begin
  // Deklaration der anonymen Methode:
  var theAnonymousMethod := method(a: integer; b: integer): integer;
  begin
    exit a + b;
  end;

  // Der Compiler mach daraus eine eigene Methode und die Zuweisung:
  // var theAnonymousMethod := @self.CompilerGeneratedMethodName;

  // Verwendung bzw. Aufruf:
  result := theAnonymousMethod(1, 2);
end;

Die Signatur der Methode wird in diesem Fall direkt durch die Deklaration vorgegeben, der Compiler kann den Typen dieser Methode daher erraten. Wird eine anonyme Methode als Parameter an eine andere Funktion übergeben, wird die Signatur der neu zu definierenden Methode ja bereits durch die Signatur der Methode definiert, an die dieser Delegatentyp übergeben werden soll. Das Erraten von Typen durch den Compiler wird Type Inference genannt und kann im Übrigen auch an vielen anderen Stellen elegant eingesetzt werden. Das kann auch durch die schon angesprochenen Generics gesteuert werden. Hierzu definiert das .NET Framework bereits einige Signaturen vor. So gibt es die Prozeduren (als Actions) Action (kein Eingabeparameter), Action<T1> mit einem Eingabeparameter, Action<T1, T2> mit zwei Parametern und so fort. Analog zu den Prozeduren ohne Rückgabewert gibt es die Funktionen, die zuerst die optionalen Eingabe- und zuletzt den Typen des Ausgabeparameters definieren: Func<[T1, [T2, [T3…]]], TResult>. Auch bestimmte Arten von Funktionen wie EventHandler sind bereits so vordefiniert: Action<Object, EventArgs> beziehungsweise auch etwas konkreter der EventHandler<T> als Action<Object, T>; where T is EventArgs. Somit kann der generische EventHandler auch konkretisierte Typen annehmen, die sich aus EventArgs ableiten. Viel interessanter für unseren Anwendungsfall sind aber insbesondere die Prädikate: In LINQ wird beispielsweise eine Extension Method namens .Where(Predicate<T>) auf eine Sequence<T> definiert. Das heißt, ist die Sequenz eine Liste von Strings, so nimmt das Prädikat einen String als Eingabeparameter an und einen Boolean als Ausgabeparameter. Schließlich ist ein Predicate<T> als Func<T, Boolean> definiert. Wozu ist das gut? Sehen wir uns einmal eine Extension Method namens MyWhere einmal im Detail in Listing 4 an.

uses
  System.Collections.Generic;

extension method IEnumerable<T>.MyWhere(condition: Predicate<T>): IEnumerable<T> iterator;
begin
  for each element in self do
  begin
    if condition(element) then
       yield element;
  end; 
end; 

method MyClass.UseEvenNumbers;
begin
  for each number in self.GetEvenNumbers.MyWhere(n -< n < 30) do
  begin
    Console.WriteLine(number.ToString());
    if number < 50 then
      break; 
  end; 
end; 

Diese Methode erweitert IEnumerable<T>, also das Interface, das hinter jeder Sequenz steht, um eine Methode .MyWhere(Predicate<T>). Der einzige Parameter dieser Methode ist also eine Funktion, die ein Element eines beliebigen Typs aus einer beliebigen Sequenz annimmt und einen Boolean zurückliefert. Er gibt an, ob die entsprechende Bedingung, also das Prädikat, erfüllt ist oder nicht. Die Methode iteriert in ihrer Implementierung nun über die Sequenz, auf der diese Methode aufgerufen wird (die for each-Schleife über die Referenz auf self, also das zu erweiternde Objekt) und führt die übergebene Prädikatsfunktion auf jedem Element aus. Liefert das Prädikat true zurück, so wird das jeweilige Element mit yield als Rückgabewert zurückgegeben. Trifft die Bedingung nicht zu, wird so lange weitergemacht, bis das nächste Element passt. Die Methode lässt sich auch folgendermaßen erklären: Die Elemente, auf die das Prädikat zutrifft, werden einfach „durchschleift“, die anderen ignoriert. Sie implementiert also einen Filter auf die ursprüngliche Sequenz.

[ header = Das Lambda-Kalkül ]

Das Lambda-Kalkül

In der Methode UseEvenNumbers in Listing 2 wird die Kurzschreibweise einer anonymen Methode verwendet, ein so genannter Lambda-Ausdruck (Lambda Expression). Die Idee (und natürlich auch der Name) hinter dieser Schreibweise im Code stammt aus der Definition formaler Sprachen, dem Lambda-Kalkül. Die Schreibweise n -> n > 30 definiert eine Funktion. Vor dem Pfeil stehen die Eingabeparameter, hier ein einzelner Eingabeparameter mit dem Namen n (steht hier für Number) und nach dem Pfeil die Ausgabe: in diesem Fall der boolesche Ausdruck n größer als 30. Lambda-Ausdrücke können nicht nur Funktionen, sondern auch Prozeduren abbilden. Ein EventHandler könnte als Lambda-Ausdruck zum Beispiel auch folgendermaßen geschrieben werden: (s, e) -> MessageBox.Show(s(Button).Text);. Die Eingaben heißen hier also s (Sender) und e (EventArgs), danach folgt die Implementierung. Die Methode Show der MessageBox hat keinen Rückgabewert, also erstellt der Compiler im Hintergrund eine anonyme Methode als Action<Object, EventArgs>. Will man mehrere Anweisungen in einem Lambda-Ausdruck ausführen, ist es selbstverständlich möglich, einen Anweisungsblock mit begin … end auch in einem Lambda-Ausdruck einzufassen, aber man sollte sich fragen, ob es nicht vielleicht besser oder leserlicher wäre, eine „echte“ anonyme Methode hierfür zu definieren.

Wer bist du?

In die gleiche Kerbe wie anonyme Methoden schlagen auch anonyme Objekte. Dank ihnen müssen bei LINQ nicht immer komplette Datensätze im Speicher vorgehalten werden, sondern nur das, was auch wirklich benötigt wird. Ein anonymes Objekt wird einfach mit new class (PropertyName := Wert, Property2Name := OtherWert) definiert. Auch hier baut der Compiler im Hintergrund eine komplette Klasse mit generiertem Namen, den angegebenen Properties und deren Typen, die aus den zugewiesenen Typen erraten werden. Es steht mit allen seinen Eigenschaften in dem Scope zur Verfügung, in dem es definiert wurde. Wird ein anonymes Objekt allerdings aus seinem Scope (z. B. einer Methode) herausgereicht, ist es nur möglich, dieses Objekt mit dem Typen object zu verwenden. Ein Zugriff auf die Eigenschaften ist nur noch mittels Reflection möglich, da kein Name für den Typen existiert, zu dem das Objekt gecastet werden könnte. Ein Beispiel für anonyme Objekte wird in Listing 5 gegeben, dort werden auch mehrere der oben genannten Features eingesetzt.

class method ConsoleApp.Main(args: array of String);
begin
  for each elem in typeOf(String).Assembly.GetTypes()
    .Where(t -> t.IsPublic)
    .Select(t -> new class(
        Name := t.Name,
        AssemblyName := t.Assembly.FullName,
        IsAbstract := t.IsAbstract
      )) do
  begin
    Console.WriteLine(if elem.IsAbstract
      then 'Abstract TYPE'
      else String.Empty
      + elem.Name + ", " + elem.AssemblyName);
  end;
  Console.ReadKey;
end; 

In der Schleife wird durch alle Typen iteriert, die im gleichen Assembly wie die Klasse System.String liegen. Ein Filter mit der LINQ-Extension-Methode .Where lässt nur die Elemente durch, die public sind. Die .Select()-Methode erstellt für jedes der Elemente in der Sequenz ein neues, anonymes Objekt mit den Properties Name und AssemblyName als String sowie dem Boolean-Property IsAbstract und gibt sie intern mittels yield zurück. Wie man sieht, werden auf ein einziges Objekt auch Properties gesetzt, die ursprünglich auf einem weiteren Property sitzen (t.Assembly.FullName). Auch dafür kann ein anonymes Objekt gut verwendet werden. Bei der Ausgabe bietet die Type Inference einen direkten, typsicheren Zugriff auf diese Properties und auch die IDE unterstützt uns für das neue Objekt mit Intellisense-Codevervollständigung. Man kann also sehr elegant damit arbeiten und die Daten genau so zusammenstellen, wie man sie später benötigt.

All together now

Diese kleinen Hilfsmittel bilden das Grundgerüst für LINQ. LINQ selber ist nun die bereits zu Anfang gezeigte Abfragesprache direkt im Code. Nehmen wir uns das Beispiel noch einmal grob vor: Es begann mit from entry in AddressList where entry.ZipCode < 12345 and …. Die AddressList ist eine sequence of T, wobei T hier ein Objekt ist, das eine Adresse repräsentiert. Der erste Teil from entry in ersetzt für die weiteren Teile der Abfrage (konkret das where, order by und select) den ersten Teil des Lambda-Ausdrucks vor dem Pfeil. Der Compiler baut daraus also einfach nur ein AddressList.Where(entry -> entry.ZipCode < 12345 and entry.City.StartsWith(„A“)). Die anderen Punkte wie order by, select, skip und take werden hierbei dann analog auf die entsprechenden Extension-Methoden aus dem System.Linq Namespace übergeben, und für jeden einzelnen Aufruf wird der gleiche Name des Eingabeparameters generiert. In Listing 6 ist das Statement noch einmal in LINQ-Schreibweise und einmal als Kombination der entsprechenden Extension-Methoden aufgeführt. Wie man sieht, ist der Unterschied nicht groß und der Ansatz sehr geradlinig. LINQ verbindet also lediglich die einzelnen Features auf eine elegante Weise. Als persönliche Anmerkung möchte ich allerdings hinzufügen, dass LINQ kein Allheilmittel ist. Durch eine falsche Reihenfolge kann einiges an Performance verloren gehen. Man sollte zum Beispiel versuchen, möglichst früh in der Kette der Anweisungen möglichst viele nicht benötigte Elemente herausfiltern.

var addressesToPrint := from entry in AddressList
    where entry.ZipCode < 12345 and entry.City.StartsWith("A")
    order by entry.Name
    select entry.Name, entry.Street, entry.ZipCode, entry.City
    skip 20
    take 10;
var addresses := AddressList
  .Where(a -> a.ZipCode < 12345 and a.City.StartsWith("A")
  .OrderBy(a -> a.Name)
  .Select(a -> new class(Name := a.Name, Street := a.Street, City := a.City))
  .Skip(20)
  .Take(10);

Der Autor selber setzt die LINQ-Features ausschließlich mittels der Extension-Methodenaufrufe ein und nicht die eigentliche Abfragesyntax direkt in der Sprache. Ein Hintergrund ist, dass tatsächlich nur die gängigsten Methoden in LINQ zur Verfügung stehen, aber die Methode .FirstOrDefault() zum Beispiel kann darüber nicht abgebildet werden. Sie liefert aus einer Sequenz das erste Element, wenn es existiert, oder den Default-Wert des angegeben Typen (bei Objekten eben nil) und wird gerne verwendet, um eine Exception zu vermeiden, wenn man über eine leere Sequenz iteriert. Meist wird dann der LINQ-Ausdruck geklammert und mit .FirstOrDefault() beendet, aber dieses Mischen von LINQ-Syntax und Methodenaufruf trägt nicht wirklich zur Lesbarkeit des Codes bei. Zudem nimmt .FirstOrDefault() auch gleich ein Prädikat an und kann auch unmittelbar als Ersatz für ein .Where() verwendet werden und spart somit auch nochmal etwas Code.

[ header = Eine Insel mit zwei Bergen ]

Eine Insel mit zwei Bergen

Nachdem wir uns nun erst mit dem .NET-spezifischen LINQ auseinandergesetzt haben, fliegen wir auf die Insel Java und schauen uns die Sprachfeatures an, die von den dortigen Eingeborenen den Weg in unsere Codekultur gefunden haben. Beginnen wir mit einem Feature, das so ähnlich funktioniert wie die gerade behandelten anonymen Objekte in Kombination mit anonymen Methoden: Inline Interfaces. Java kennt das Konzept von Delegaten nicht in der Art und Weise, wie Delphi-Entwickler zum Beispiel Methoden an Events hängen oder in .NET ein Event Handler verwendet wird. Zwar nennen Java-Entwickler ihre Art von Event Handling auch Delegationsmodell, aber es funktioniert anders als man es intuitiv erwarten würde. Ein Delegat in Java ist keine Referenz auf eine Methode, sondern ein normales Objekt, das ein bestimmtes Interface implementiert wie den ActionListener (Interfaces in Java werden nicht mit dem Präfix I ausgewiesen wie in Delphi oder .NET und sind daher nicht sofort als solche erkennbar). Das Interface ActionListener definiert eine Methode actionPerformed (in Java werden Methodennamen in CamelCase notiert, also klein begonnen), und sie wird aufgerufen, wenn das Event auftritt. Analog zu Multicast-Events in .NET können auch in Java mehrere Delegatobjekte für ein Event an das auslösende Objekt gehängt werden. Dazu gibt es auf dem jeweiligen Objekt beispielsweise Methoden wie addActionListener und removeActionListener.

Um für diese Art des Programmierens nun einen Event Handler zu implementieren, ist es entweder notwendig, für jeden Handler ein eigenes Objekt zu erstellen und dort das entsprechende Interface für diesen Eventtypen zu implementieren, oder das Interface auf der aktuellen Klasse zu implementieren. Das macht es aber schwieriger, Handler für mehrere Eventauslöser zu erstellen, da sie alle dieselbe Methode aufrufen. Um das elegant zu lösen, gibt es nun die Inline Interfaces. Sie erlauben es, analog zu anonymen Objekten mit Properties auch Klassen zu erzeugen, die ein Interface implementieren, und dabei auch gleich die Methoden zuzuweisen. Ein Beispiel ist in Listing 7 aufgezeigt. Wie man sieht, wird im ersten Fall der anonym erzeugten Interfaceimplementation eine Referenz auf die normal als Event Handler erstellte reguläre Methode HandleActionPerformed übergeben, und zwar an die Interfacemethode actionPerformed. Im zweiten Fall wird der Handler komplett als Lambda-Ausdruck beziehungsweise anonyme Methode an das Interface übergeben. Es funktioniert also beides und bietet die Möglichkeit, sich die in Java benötigten Delegatenklassen zu ersparen.

method MyClass.AssignEventHandlers;
begin
  var btn := GetReferenzeToJavaSwingButton();
  // erster Fall: Methode auf diesem Objekt als handler
  var eventHandlerOnThisObject := new interface
    ActionListener(actionPerformed := @self.HandleActionPerformed); 
  btn.addActionListener(eventHandlerOnThisObject);

  // zweiter Fall: Komplett inline, inkl. Implementierung
  btn.addActionListener(new interface ActionListener(actionPerformed :=
    ae -> Button(ae.GetSource()).Text := 'Clicked'));
end;

method MyClass.HandleActionPerformed(ae: ActionEvent);
begin
   Button(ae.GetSource()).Text := 'Clicked';
end;

Und nochmal delegieren

Um die Verwirrung mit Delegaten in .NET- und Delegat-Objekten in Java komplett zu machen, gibt es in Oxygene genauso wie in Delphi auch noch die so genannte Interface Delegation. Hiermit ist es möglich, eine Art mixin-Funktionalität zu erreichen und die Implementierung eines bestimmten Interface auf ein anderes Objekt, das dieses Interface erfüllt, zu übertragen. Wie auch in Delphi wird hierbei das Schlüsselwort implements verwendet. Interessant wird dieses Feature, wenn zum Beispiel eine Standardfunktionalität für ein bestimmtes Interface wiederverwendet werden soll, und das aus Gründen der Vererbungshierarchie nicht möglich ist, weil zum Beispiel Klassen aus unterschiedlichen verwendeten Bibliotheken einheitlich ergänzt werden sollen und sie nicht voneinander ableiten. Die einfachste Implementierung ist in Listing 8 zu sehen. Dort werden gleich zwei Interfaces von jeweils anderen Klassen implementiert. Der Compiler wird in diesem Fall die Methoden, Events und Eigenschaften des Interface ISomeInterface automatisch implementieren (und jeweils als public markieren). Diese automatisch generierten Implementierungen werden jeden Aufruf einfach auf die Klasse, die dem Feld fHolder zugewiesen ist, weiterleiten (delegieren). Ähnliches passiert bei den Methoden und Eigenschaften des ISomeOtherInterface und fOtherHolder, jedoch werden diese Interface-Member nicht als public implementiert, sondern sind nur sichtbar, wenn die Klasse zu ISomeOtherInterface gecastet wird. Interface Delegation geht auch noch darüber hinaus und kann zum einen mehrere Interfaces gleichzeitig delegieren oder gar Methoden mit einem anderen Namen anweisen, ein bestimmtes Interface zu implementieren (z. B. kann der Originalname schon mit einer anderen Funktionalität belegt sein, und das Interface wird erst später hinzugefügt). Der Compiler wird für die letzte Zeile im Beispiel eine Methode DoFoo erzeugen, die auch nur beim Casten auf das Interface IFoo sichtbar ist, und jeden Aufruf an die Methode DoSomething auf der Klasse weiterleiten.

MyClass = class(ISomeInterface, ISomeOtherInterface, IFoo)
  private
    var fHolder := new SomeInterfaceImplementer; readonly;
      public implements ISomeInterface;
    var fOtherHolder := SomeOtherInterfaceImplementer; readonly;
      implements ISomeOtherInterface;
    method DoSomething; implements IFoo.DoFoo; 
end;

[ header = Ein softes Quack & Fazit ]

Ein softes Quack

Als letztes wollen wir uns dem wohl mächtigsten neuen Feature in Oxygene 5 zuwenden, dem Duck Typing und den dazugehörigen so genannten Soft Interfaces. Das Duck Typing hat seinen Namen von der Annahme, dass alles was watschelt wie eine Ente und quackt wie eine Ente wohl auch eine Ente sein wird. Nehmen wir ein Interface IDuck an, das die zwei Methoden Walk und Talk definiert. Durch die strenge Typisierung in .NET und Java reicht es nicht aus, dass eine Klasse wie zum Beispiel ToyDuck zwei Methoden namens Walk und Talk bereitstellt, sondern sie muss ausdrücklich vom Interface IDuck ableiten, um als Ente innerhalb des Codes verwendet zu werden. Schwierig wird es hier, wenn die Spielzeugentenklasse zum Beispiel aus einer fremden Bibliothek stammt und als sealed markiert ist, sodass wir nicht davon ableiten und IDuck implementieren können. Das Duck Typing mit der Compiler-Magic-Funktion duck<T>(object) where T is Interface lockert diese Anforderung. Die Methode duck kann einmal statisch aufgerufen werden (duck<IDuck>(MyToyDuckObject)) und liefert einen Compilerfehler zurück, wenn das übergebene Objekt nicht alle Methoden, Events und Properties des Interface erfüllt. Nehmen wir an, IDuck enthält auch eine Methode Fly, die von ToyDuck nicht angeboten wird. Das führt also beim Kompilieren zu einem Fehler. Die zweite Möglichkeit bietet hier dynamisches oder „schwaches“ Duck Typing mit einem erweiterten Aufruf: duck<IDuck>(MyToyDuckObject, DuckTypingMode.Weak). In diesem Fall prüft der Compiler nicht zur Compile-Zeit, ob das Objekt das Interface vollumfänglich unterstützt. Wird auf der Ente nur Walk oder Talk aufgerufen funktioniert das also einwandfrei, aufrufe von Fly werden bei unserer Spielzeugente allerdings zur Laufzeit mit einer NotImplementedException quittiert. Man tauscht hier also Typsicherheit gegen potenzielle Laufzeitfehler ein.

Das letzte Feature, die Soft Interfaces, erlauben uns sogar, auf den ausdrücklichen Aufruf von duck zu verzichten. Nehmen wir als Beispiel wieder eine Klasse (z. B. ein Control) aus einer fremden Bibliothek, die wir nicht ändern können oder wollen. Dieses Control bietet die gleiche Funktionalität wie ein bereits verwendeter Button. Die relevanten Properties heißen gleich, auch das Event, das beim Anklicken ausgelöst wird, heißt gleich, aber die Klassen teilen weder ein Interface noch eine gemeinsame Basisklasse (außer object). Wir können sie also nicht einheitlich verwenden. Oxygene bietet uns dafür die Möglichkeit, ein Interface als soft zu deklarieren (Listing 9). Auch wenn keine einzige Klasse dieses Interface direkt implementiert, so können wir dennoch jedes Objekt, das diese Eigenschaften, Methoden und Events bereitstellt, an eine Variable des Typs ISoftButton zuweisen oder an Parameter dieses Typs übergeben. Der Compiler übernimmt das Duck Typing an jeder dieser Stellen automatisch für uns.

ISoftButton = soft interface 
  property Caption: String;
  property Enabled: Boolean;
  event OnClick: EventHandler;
end;

Fazit

Wir haben gesehen, wie man generische Klassen und Methoden verwenden und die Platzhalter auf bestimmte Typen oder Interfaces einschränken kann, um sie noch mächtiger zu gestalten. Danach haben wir uns anonymen Methoden gewidmet und deren Kollegen, den Lambda-Ausdrücken. Zusammen mit anonymen Objekten und den Sequenzen, denen wir auf den yield-Zahn gefühlt haben, bieten uns diese Elemente das Handwerkzeug, das auch der Compiler verwendet, um uns die volle Funktionalität von LINQ zur Verfügung zu stellen. Nach diesem intensiven Einblick in die .NET-Features haben wir uns noch Inline Interfaces und das Duck Typing inklusive der Soft Interfaces angesehen, die primär aus der Java-Ecke kamen und durch das bereits länger in der Sprache verfügbare Feature Interface Delegation abgerundet werden. Zusammengefasst stehen uns mit all diesen Features ungeheuer mächtige Elemente zur Verfügung, die unsere Produktivität massiv steigern können, wenn sie sinnvoll und wohldosiert eingesetzt werden. Tiefergehende Informationen zu den Sprachfeatures von Oxygene finden Sie unter [1].

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -