Neuerungen gegenüber C


Inhalt <- Einleitung <- D im Vergleich mit C <- Neuerungen gegenüber C -> Zusammenfassung -> Literaturhinweise

Objektorientierte Programmierung

Die objektorientierte Programmierung in D lehnt sich vor allem von der Syntax und vielen Eigenschaften an Java an. Instanzvariablen werden über this angesprochen und der Zugriff auf Attribute und Methoden funktioniert über den Punktoperator. Innerhalb einer Klasse können statische und nicht-statische Attribute, Methoden, Konstruktoren und Destruktoren definiert werden. Jede Klasse besitzt immer eine direkte Oberklasse, von der sie abgeleitet wurde. Wird diese nicht explizit angegeben, handelt es sich dabei um die Klasse "Object". Wie in Java auch, stehen in D nur die Referenzen auf Objekte nach der Instanziierung zur Verfügung. Man verfügt also über keine echte Kopie, was für die gleichen Probleme wie in Java, beispielsweise beim Tiefen kopieren, sorgt.

Möchte man Mehrfachvererbung umsetzen, muss man dem Prinzip der Interfaces aus Java folgen. D unterstützt nicht wie C++ eine echte Mehrfachvererbung. Dies verhindert Namenskonflikte und das damit verbundene Diamantenproblem in Sprachen, die echte Mehrfachvererbung unterstützen. Interfaces können nicht instanziiert werden. Sie stellen nur das Grundgerüst für die abzuleitenden Klassen dar. Jede vordefinierte Methode muss in der erbenden Klasse implementiert werden. Funktionsrümpfe sind in Interfaces daher nicht erlaubt. Ebenso wie Attributdeklarationen. Für Interfaces besteht außerdem nur die Möglichkeit, von anderen Interfaces zu erben.

Für abstrakte Klassen gelten ähnliche Bedingungen. Sobald eine Klasse ein abstraktes Attribut oder eine abstrakte Methode enthält, ist automatisch die gesamte Klasse abstrakt. Abstrakte Klassen dürfen nicht instanziiert werden, können aber Funktionsrümpfe beinhalten. Diese können wie in jeder anderen Klasse Überladen oder Überschrieben werden. Daher ist Polymorphie und dynamisches Binden möglich. Beim dynamischen Binden ist zu beachten, dass die auszuwertende Methode vom Typ des Objektes abhängt, und nicht vom Typ der Referenz. Das nachfolgende Beispiel erläutert das dynamische Binden und generelle Objektorientierung in D anhand Mehrfachvererbung über Interfaces und Methodenüberschreibung.
public interface A {
  public void x();
  public void y();
}
public interface B {
  public void x();
  public void z();
}
public class C : A, B {
  protected int i;
  this(int i) {
    this.i = i;
  }
  public void x() {}
  public void y() {}
  public void z() {
    writefln("%d", this.i);
  }
}
public class D : C {
  this(int i) {
    super(i * 10);
  }
  public void z() {
    writefln("%d", this.i * 10);
  }
}
int main() {
  C c = new C(10);
  D d = new D(10);
  C c1 = new D(10);
  c.z(); // -> 10 (z() aus C)
  c.x(); // x() aus C
  c.y(); // y() aus C
  d.y(); // y() aus C
  d.z(); // -> 1000 (z() aus D)
  c1.z(); // -> 1000 (z() aus D)
  return 0;
}
Der letzte Methodenaufruf verdeutlich das Prinzip des dynamischen Bindens. Der Typ der Referenz c1 ist C, das Objekt, auf welches die Referenz zeigt, ist allerding vom Typ D, weshalb hier der Methodenaufruf aus D stattfindet, und nicht aus C.
Wenn man erreichen möchte, dass eine Klasse nicht mehr abgeleitet werden kann, ist in D das Schlüsselwort final vor class zu benutzen. Die Deklaration von Klassenvariablen oder Klassenmethoden funktioniert wie in Java ebenfalls mit dem Schlüsselwort static.

Templates

Das Konzept, das in C++ als Templates und in Java als Generics bekannt ist, ist auch in D wiederzufinden. Die Templates ermöglichen das Erstellen von Strukturen, Klassen, Interfaces oder Funktionen, ohne dabei einen expliziten Datentyp angeben zu müssen. Der Datentyp muss erst bei der Verwendung, bzw. bei der Initialisierung des Templates angegeben werden. Templates stellen dem Programmierer also ein Werkzeug zur Verfügung, das Verhalten eines Programms unabhängig vom Datentyp zu implementieren, so dass sich Datenstrukturen und Funktionen, die bei verschiedenen Typen auf gleiche Art und Weise funktionieren, entwickeln lassen. In D lassen sich bei der Initialisierung sowohl primitive Typen als auch komplexe Typen angeben (z.B. Objekte).
Etwas gewöhnungsbedürftig ist die Syntax zum Initialisieren eines Templates. Vor allem das Ableiten einer abstrakten Templateklasse möchte ich explizit erwähnen, da die offizielle Dokumentation dies nicht erläutert.
// Templates anhand einer abstrakten Stackklasse erklärt
public abstract class Stack(T) {	
  public void push(T e);
  public T pop();
}

// Ableiten der abstrakten Templateklasse
// Es ist der Ableitungsoperator ! und die zweifache
// Angabe des Platzhalters T zu beachten
public class StackAsArray(T) : Stack!(T) {
  private T[] stack;		
  public void push(T e) {
    this.stack ~= e;
  }
	
  public T pop() {
    T e;
    int size = stack.length;
    assert(size > 0);
    e = stack[size - 1]; 
    this.stack.length = size - 1;	
    return e;	
  }
}

// ...

// Initialisierung als char[] oder als int
Stack!(char[]) s = new StackAsArray!(char[]);
Stack!(int) s2 = new StackAsArray!(int);

s.push("Text");
s2.push(1);
	
writefln(s.pop());
writefln(s2.pop());	

Design by Contract

Design by Contract ist ein Konzept, dass zur Zeit leider nur in wenigen Programmiersprachen zur Verfügung steht. Es beschreibt das Entwerfen einer Funktion oder eines ganzen Programmes aufgrund vorgegebenen Bedingungen. Diese Bedingungen werden, wie der Name schon sagt, in einem "Vertrag" festgehalten. Dabei sind prinzipiell zwei verschiedene Bedingungen zu unterscheiden. Die erste Bedingung ist die Vorbedingung, die erfüllt sein muss, damit das Codestück ausgeführt werden kann. Die Vorbedingung ist in der Regel vom Benutzer des Codestücks zu erfüllen. Die zweite Bedingung ist die Nachbedingung. Diese muss gelten, wenn das Codestück verlassen wird, und somit ist der Entwickler des Codes für die Erfüllung der Nachbedingung verantwortlich.
In vielen anderen Programmiersprachen kann man sich diese Funktionalität "hinzubasteln", indem man Assertions an den Anfang und das Ende einer Funktion hinzufügt. D bietet für Design by Contract allerdings eigene Sprachmittel, die eine konsequente Validierung eines Programmstücks konzeptionell unterstützen.
Das Folgende Programmstück umschreibt die oben genannte pop()-Routine des Stacks und ergänzt sie um Design by Contract Sprachmittel.
public T pop()
  // Vorbedingung: Der Stack darf nicht leer sein
  in {
    assert(stack.length > 0);
  }
  // Nachbedingung: Es muss ein Element zurueckgeliefert werden
  out (result) {
    assert(result);
  }
  body {	
    T e;
    int size = stack.length;		
    e = stack[size - 1]; 
    this.stack.length = size - 1;
    return e;
  }	
Wie hier zu sehen ist, beschreiben die Schlüsselwörter in, out und body die Invarianten, die für die Vorbedingung und für die Nachbedingung gelten müssen und den Funktionsrumpf. Im Gegensatz zum obigen Beispiel ist hier die Assertion in der Mitte der Funktion nicht mehr erforderlich, da die Vorbedingung, dass der Stack nicht leer sein darf, bereits vor dem Aufruf der Funktion gelten muss und hier in den Sprachmitteln von D bereits abgefragt wird.
Design by Contract dient vor allem der Überprüfung eines korrekt ablaufenden Programms. In der endgültigen Releaseversion sollten diese Prüfungen, vor allem aus Performancegründen bei komplexeren Bedingungen, abgeschaltet werden. Die beiden Compiler stellen dafür die Optionen -frelease (gdc) und -release (dmd) bereit.
Nachteilig an diesem Konzept ist, dass ein nachträgliches Umsetzen von Design by Contract selbstverständlich mit dem Umschreiben der Funktionen verbunden ist.

Exceptions

In C gibt es in der Sprachdefinition keine Exceptions. D bietet diese Art der Ausnahmebehandlung und agiert dabei ähnlich wie Java. Die Exceptions werden dabei in die klassischen Blöcke try, catch und finally gegliedert. Neue Exceptions können mit throw erzeugt werden.
Sinn und Zweck der Ausnahmebehandlung ist die Entwicklung fehlertoleranterer Programme. Während man in C mühevoll per Hand eigene Fehlerbehandlung von Grund auf selber entwickeln muss, bietet das Konzept der Exceptions ein Sprachmittel, das Fehler an übergeordnete Programmabläufe geben kann, so dass flexibel auf die Fehler reagiert werden kann, ohne dass man z.B. Funktionsköpfe ändern muss, um Ausnahmen zu übergeben.
try {
    try {
        throw new Exception("first");
        } finally {
          writefln("finally");
          throw new Exception("second");
        }
    } catch(Exception e) {
      writefln("catch %s", e.msg);
    }
    writefln("done");
Wie hier zu sehen ist, wird im ersten try-Block gleich in den zweiten gesprungen. Dort wird mit throw gleich die erste Exception "first" erzeugt. Hier erkennt man auch, dass es für die Ausnahmebehandlung die eigens dafür zuständige Klasse "Exception" gibt, welche die Oberklasse für jede erzeugte Exception bildet. Da beim Auslösen einer Ausnahme immer in den finally-Teil gesprungen wird (sofern vorhanden), wird in diesem Beispiel die Ausgabe "finally" erzeugt. Schließlich wird im finally-Teil eine neue Exception ausgelöst ("second"). Die alte Exception "first" wird dabei überschrieben. Der catch-Block dient dann der eigentlichen Fehlerbehandlung. Die zuletzt abgespeicherte Exception wird hier schließlich in "e" gespeichert (es sind andere Namen und speziellere Exceptions denkbar) und über .msg ausgegeben. Natürlich sind deutlich komplexere Fehlerbehandlungen denkbar. Die endgültige Ausgabe dieses Codestückes wäre also:

finally
catch second
done

Cross-Compiling

Da D eine Sprache ist, die durch einen Compiler übersetzt wird, und nicht durch einen Interpreter bzw. eine virtuelle Maschine, muss das immer wichtiger werdende Kriterium der Plattformunabhängigkeit anders realisiert werden, bzw. dem auf andere Art und Weise ein Stück näher gekommen werden.
In D bietet das Konzept des Cross-Compilings eine Interessante Möglichkeit, von einem Entwicklungssystem mehrere Programme für unterschiedliche Zielsysteme und Prozessoren zu übersetzen. Wenn zum Beispiel ein Programm auf einer Windowsmaschine entwickelt wurde, und man für ein Linuxbetriebssystem oder sogar für ein Betriebssystem das Programm übersetzen möchte, für welches es keinen D Compiler gibt, kann man auf das Konzept des Cross Compilings zurückgreifen. Dabei greift man auf einen Compiler zurück, der von einem expliziten Entwicklungssystem auf ein explizites Zielsystem übersetzen kann. Dieses macht vor allem für Embedded-Systems sinn, auf denen die eigentliche Compilierung aufgrund geringerer Rechenkapazität sehr viel aufwändiger wäre als auf einer leistungsstärkeren Maschine.
Natürlich gibt es zur Zeit nur einige wenige Cross-Compiler mit den entsprechenden Kombinationen aus Entwicklungssystem und Zielsystem. Theoretisch ist es allerdings möglich, sich mit der gdc und dem gcc einen Compiler zu entwickeln. Ein interessantes Tool, welches die Erstellung eines Cross-Compilers unterstützt, ist unter http://www.kegel.com/crosstool/ zu finden.
Sicherlich macht der Einsatz eines Cross-Compilers für einige Anwendungsgebiete sinn, doch im produktiven Einsatz eines Programmes auf verbreiteten Prozessoren und Betriebssystem wie Intel und Linux kommt man um ausführliche Tests auf dem Zielsystem nicht herum. Für diese Gebiete macht der Cross Compiler daher keinen Sinn, da es keinen Unterschied machen würde, auf welchem System die Binaries erzeugt werden. Sollte es allerdings für das Zielsystem keinen Compiler geben, bzw. handelt es sich um Embedded-Systems, lohnt es sich auf jeden Fall, einen Cross-Compiler in Betracht zu ziehen, bevor ein Eigenständiger entwickelt wird.


Inhalt <- Einleitung <- D im Vergleich mit C <- Neuerungen gegenüber C -> Zusammenfassung -> Literaturhinweise