## 4.7 Interfaces (Schnittstellen) Das Konzept der Schnittstellen ist am einfachsten zu verstehen, wenn man sich deutlich macht, worin genau der Unterschied zwischen einer Klasse und einem Objekt besteht. Klassen sind Schablonen, in denen Methoden und Eigenschaften definiert sind. Die Methoden manipulieren die Eigenschaften und stellen damit das Verhalten eines Objekts sicher. Ein Objekt wird jedoch nicht durch sein Verhalten, sondern durch seine Daten beschrieben, die über Eigenschaften manipuliert werden. Treiben wir die Abstraktion noch weiter. Wenn sich ein Objekt durch Daten beschreiben lässt und in einer Klasse Eigenschaften und Methoden definiert sind, dann muss es auch ein Extrem geben, das nur Verhaltensweisen festlegt: Genau diese Position nehmen die Schnittstellen ein. Die Aufgaben der Schnittstellen gehen über die einfache Fähigkeit, Verhaltensweisen bereitzustellen, hinaus. Bekanntlich wird in .NET die Mehrfachvererbung nicht unterstützt. Damit sind die .NET-Architekten möglichen Schwierigkeiten aus dem Weg gegangen, die mit der Mehrfachvererbung verbunden sind. Mehrfachvererbung ist nur schwer zu realisieren und wird deshalb in der Praxis auch nur selten eingesetzt. Andererseits hielt man es für erstrebenswert, neben der Basisklasse weitere »Oberbegriffe« zuzulassen, um gemeinsame Merkmale mehrerer ansonsten unabhängiger Klassen beschreiben zu können. Mit der Schnittstelle wurde ein Konstrukt geschaffen, das genau diese Möglichkeiten bietet. Sie müssen sich Schnittstellen wie eine Vertragsvereinbarung vorstellen. Sobald eine Klasse eine Schnittstelle implementiert, hat der Code, der auf ein Objekt dieser Klasse zugreift, die Garantie, dass die Klasse die Verhaltensdefinitionen der Schnittstelle aufweist. Mit anderen Worten: Eine Schnittstelle legt einen Vertragsrahmen fest, den die implementierende Klasse erfüllen muss. ### 4.7.1 Schnittstellendefinition Schnittstellen können Methoden Eigenschaften Ereignisse Indexer vorschreiben. Indexer und Ereignisse waren bisher noch kein Thema und werden erst in Kapitel 6, »Weitere .NET-Datentypen«, behandelt. Das Besondere an einer Schnittstelle ist, dass sie selbst keine Codeimplementierung enthält, sondern ausnahmslos nur abstrakte Definitionen. Schauen wir uns dazu eine einfache, fiktive Schnittstelle an:
    public interface ICopy {
      string Caption {get; set;}
      void Copy();
    }
Die Definition einer Schnittstelle ähnelt der Definition einer Klasse, bei der das Schlüsselwort class gegen das Schlüsselwort interface ausgetauscht worden ist. Fehlt die Angabe eines Zugriffsmodifizierers, gilt eine Schnittstelle standardmäßig als internal. Ansonsten kann eine Schnittstelle noch public sein. Hinter der Definition werden in geschweiften Klammern alle Mitglieder der Schnittstelle aufgeführt. Beachten Sie, dass das von den abstrakten Klassen her bekannte Schlüsselwort abstract in einer Schnittstellendefinition nicht auftaucht.
Hinweis
Konventionsgemäß wird dem Bezeichner einer Schnittstelle ein »I« vorangestellt.
Die Schnittstelle ICopy beschreibt die Eigenschaft Caption sowie die Methode Copy. Weil eine Schnittstelle grundsätzlich nur abstrakte Definitionen bereitstellt, hat kein Mitglied einen Anweisungsblock. Es ist auch kein Zugriffsmodifizierer angegeben. Der C#-Compiler reagiert sogar mit einer Fehlermeldung, wenn Sie einem Schnittstellenmitglied einen Zugriffsmodifizierer voranstellen. ### 4.7.2 Schnittstellenimplementierung Bei der Vererbung wird von Ableitung gesprochen, analog wurde bei den Schnittstellen der Begriff Implementierung geprägt. Eine Schnittstelle ist wie ein Vertrag, den eine Klasse unterschreibt, sobald sie eine bestimmte Schnittstelle implementiert. Das hat Konsequenzen: Eine Klasse, die eine Schnittstelle implementiert, muss ausnahmslos jedes Mitglied der Schnittstelle übernehmen. Eine zu implementierende Schnittstelle wird, getrennt durch einen Doppelpunkt, hinter dem Klassenbezeichner angegeben. In der Klasse wird das Schnittstellenmember (in unserem Beispiel die Methode Copy sowie die Eigenschaft Caption) mit den entsprechenden Anweisungen codiert.
    class Document : ICopy {
      public void Copy() {
        Console.WriteLine("Das Dokument wird kopiert.");
      }
      public string Caption {
        get{...}
        set{...}
      }
      ...
    }
Grundsätzlich können Sie jeden beliebigen Code in die Schnittstellenmethode schreiben. Das ist aber nicht der Sinn und Zweck von Schnittstellen. Stattdessen sollten Sie sich streng an der Schnittstellenbeschreibung in der Dokumentation halten. Das bedeutet im Umkehrschluss aber auch, dass eine Schnittstelle ohne Dokumentation wertlos ist. Nur die Dokumentation gibt darüber Auskunft, was die Methode leisten soll und wie ihre Rückgabewerte zu interpretieren sind. Eine Klasse ist nicht nur auf die Implementierung einer Schnittstelle beschränkt, es dürfen, im Gegensatz zur Vererbung, auch mehrere Schnittstellen sein, die durch ein Komma voneinander getrennt werden.
    class Document : ICopy, IDisposable {
      /* ... */
    }
Eine Klasse, die eine oder mehrere Schnittstellen implementiert, darf durchaus eine explizit angegebene Basisklasse haben. Dabei wird die Basisklasse vor der Liste der Schnittstellen aufgeführt. Im folgenden Codefragment bildet Frame die Basis von Document.
    class Document : Frame, ICopy, IDisposable {
      /* ... */
    }
Schnittstellen dürfen nach der Veröffentlichung nicht mehr verändert werden, da sowohl der Client als auch die implementierende Klasse in einem Vertragsverhältnis zueinander stehen und die Bedingungen des Vertrags von beiden Vertragspartnern erfüllt werden müssen.
Hinweis
Sollten Sie nach dem Veröffentlichen einer Schnittstelle Änderungen oder Ergänzungen vornehmen wollen, sollten Sie eine neue Schnittstelle bereitstellen.
Mit der Veröffentlichung einer Schnittstelle erklärt sich eine Klasse bereit, die Schnittstelle exakt so zu implementieren, wie sie entworfen wurde. Die von der Klasse übernommenen Mitglieder der Schnittstelle müssen daher in jeder Hinsicht identisch zu ihrer Definition sein: Der Name muss dem Namen in der Schnittstelle entsprechen. Der Rückgabewert und die Parameterliste dürfen nicht von denen in der Schnittstellendefinition abweichen. Ein aus einer Schnittstelle stammendes Member darf nur public sein. Zulässig sind außerdem die Modifizierer abstract und virtual, während static und const nicht erlaubt sind. Aus Schnittstellen stammende Methoden zeigen immer polymorphes Verhalten. Das setzt sich jedoch nicht bei den Subklassen durch. Eine Subklasse kann daher die Schnittstellenmethode nur erben oder mit new verdecken. Soll die Methode den ableitenden Klassen polymorph angeboten werden, muss sie mit virtual signiert werden. #### Die Unterstützung von Visual Studio Insbesondere dann, wenn eine Klasse eine Schnittstelle mit vielen Membern bereitstellen soll, lohnt es sich, diese mithilfe von Visual Studio der implementierenden Klasse hinzuzufügen. Gehen Sie dazu mit dem Mauszeiger auf den Schnittstellenbezeichner, und öffnen Sie das Kontextmenü, wie in Abbildung 4.6 gezeigt. In diesem wird Ihnen Schnittstelle implementieren und Schnittstelle explizit implementieren angeboten. Die explizite Schnittstellenimplementierung werden wir weiter unten noch behandeln. Hier klicken, um das Bild zu vergrößern Abbildung 4.6 Die Unterstützung von Visual Studio bei der Schnittstellenimplementierung Wenn Sie Schnittstelle implementieren wählen, erzeugt das Visual Studio den nachfolgend gezeigten Code automatisch.
    class Document : ICopy
    {
      #region ICopy Members
      public string Caption {
        get {
          throw new NotImplementedException();
        }
        set {
          throw new NotImplementedException();
        }
      }
      public void Copy() {
        throw new NotImplementedException();
      }
      #endregion
    }
Die Direktiven #region und #endregion, die dabei in den Code eingefügt werden, ignoriert der Compiler. Sie dienen im Codeeditor nur dazu, bestimmte Codeabschnitte zusammenzufassen, die mit dem (-)-Knoten am linken Rand reduziert werden können. #### Zugriff auf die Schnittstellenmethoden Der Aufruf einer aus einer Schnittstelle stammenden Methode unterscheidet sich nicht vom Aufruf einer Methode, die in der Klasse implementiert ist:
    Document doc = new Document();
    doc.Copy();
Sie instanziieren zuerst die Klasse und rufen auf dem Objekt die Methode auf. Es gibt aber auch noch eine andere Variante, die ich Ihnen nicht vorenthalten möchte:
    Document doc = new Document();
    ICopy copy = doc;
    copy.Copy();
Auch hierbei ist zunächst ein Objekt vom Typ Document notwendig. Dessen Referenz weisen wir aber anschließend einer Variablen vom Typ der Schnittstelle ICopy zu. Auf Letzterer wird dann die Methode Copy aufgerufen. #### Mehrdeutigkeiten mit expliziter Implementierung vermeiden Wenn eine Klasse mehrere Schnittstellen implementiert, kann es passieren, dass in zwei oder mehr Schnittstellen ein gleichnamiges Mitglied definiert ist. Diese Mehrdeutigkeit wird durch die explizite Implementierung eines Schnittstellenmembers aus der Welt geschafft. Eine explizite Implementierung ist der vollständig kennzeichnende Name eines Schnittstellenmitglieds. Er besteht aus dem Namen der Schnittstelle und dem Bezeichner des implementierten Mitglieds, getrennt durch einen Punkt. Nehmen wir an, in den beiden Schnittstellen ICopy und IAdresse wäre jeweils eine Methode Caption definiert:
    public interface ICopy {
      string Caption { get; set; }
      void Copy();
    }
    public interface IAdresse {
      string Caption { get; set; }
    }
In einer Klasse Document, die beide Schnittstellen implementiert, könnten die Methoden, wie im folgenden Codefragment gezeigt, explizit implementiert werden, um sie eindeutig den Schnittstellen zuzuordnen:
    class Document : ICopy, IAdresse {
      void ICopy.Caption() {
        Console.WriteLine("Caption-Methode in ICopy");
      }
      void IAdresse.Caption() {
        Console.WriteLine("Caption-Methode in IAdresse");
      }
      ...
    }
Es müssen nicht zwangsläufig beide Caption-Methoden explizit implementiert werden. Um eine eindeutige Schnittstellenzuordnung zu gewährleisten, würde in diesem Fall eine explizite Implementierung vollkommen ausreichen. Explizit implementierte Schnittstellenmember haben keinen Zugriffsmodifizierer, denn im Zusammenhang mit der expliziten Schnittstellenimplementierung ist eine wichtige Regel zu beachten:
Regel
Bei der expliziten Implementierung eines Schnittstellenmembers darf weder ein Zugriffsmodifizierer noch einer der Modifikatoren abstract, virtual, override oder static angegeben werden.
Auf die explizite Implementierung eines Schnittstellenmembers kann nur über eine Schnittstellenreferenz zugegriffen werden, wie im folgenden Codefragment gezeigt wird:
    Document doc = new Document();
    ICopy copy = doc;
    copy.Caption = "Dokumentkopie";
    IAdresse adresse = doc;
    adresse.Caption = "Bremen";
#### Schnittstellen, die selbst Schnittstellen implementieren Mehrere Schnittstellen können zu einer neuen Schnittstelle zusammengefasst werden. Das folgende Codefragment zeigt, wie die Schnittstelle ICopy die Schnittstelle ICompare implementiert:
    public interface ICompare {
      bool Compare(Object obj);
    }
    public interface ICopy : ICompare {
      void Copy();
    }
Eine Klasse, die sich die Dienste der Schnittstelle ICopy sichern möchte, muss ihrerseits beide Methoden bereitstellen: die der Schnittstelle ICompare und die spezifische der Schnittstelle ICopy:
    class Document : ICopy {
      public void Copy() {
        ...
      }
      public bool Compare(object obj) {
        ...
      }
    }
### 4.7.3 Interpretation der Schnittstellen Schnittstellen zu codieren ist sehr einfach. Da werden Sie mir zustimmen. Aber wahrscheinlich werden Sie sich nun fragen, welchen Sinn bzw. welche Aufgabe eine Schnittstelle hat. Schließlich ließen sich die Schnittstellenmember doch auch direkt in einer Klasse codieren, ohne vorher den Umweg über die Implementierung eines interface-Typs gehen zu müssen. Natürlich steckt hinter einem interface nicht die Absicht, das Coding unnötig komplex zu gestalten. Tatsächlich lässt sich die Existenz durch zwei Punkte rechtfertigen: Mit einer Schnittstelle wird die fehlende Mehrfachvererbung ersetzt, ohne deren gravierende Nachteile in Kauf nehmen zu müssen. Mit einer Schnittstelle kann ein Typ vorgegeben werden, dessen exakte Typangabe nicht bekannt ist. Der letzte Punkt ist nur eine logische Konsequenz des zuerst aufgeführten. Beide Aussagen möchte ich Ihnen im Folgenden beweisen. #### Schnittstellen als Ersatz der Mehrfachvererbung Weiter oben haben wir die folgenden beiden Anweisungen im Programmcode geschrieben:
    Document doc = new Document();
    ICopy copy = doc;
Kommt Ihnen das nicht bekannt vor? Sehr ähnlich sahen zwei Anweisungen aus, die wir in Abschnitt 4.3.1, »Implizite Typumwandlung von Objektreferenzen«, geschrieben hatten:
    Zeppelin zpln = new Zeppelin();
    Luftfahrzeug lfzg = zpln;
Die beiden Anweisungen bildeten die Grundlage für die Aussage, dass Sie eine Subklassenreferenz einer Basisklassenreferenz zuweisen können. Der Referenz auf ein Interface können Sie aber auch die Referenz eines Objekts übergeben, das die entsprechende Schnittstelle implementiert. Das führt zu der folgenden Aussage:
Merksatz
Im Programmcode kann eine Schnittstelle genauso behandelt werden, als würde es sich um eine Basisklasse handeln.
Die daraus resultierende Konsequenz und Interpretation wollen wir am Beispiel des Projekts GeometricObjectsSolution weiter verdeutlichen. Erinnern Sie sich bitte an die Aussage, dass alle abgeleiteten Klassen gleichzeitig auch vom Typ der Basisklasse sind. Das bedeutet mit anderen Worten: Bei Objekten vom Typ Circle, Rectangle, GraphicCircle und GraphicRectangle handelt es sich um geometrische Objekte. GeometricObject spannt demnach eine Familie aller geometrischen Objekte auf, weil die ableitenden Klassen alle Member der gemeinsamen Basisklasse GeometricObject aufweisen. Betrachten wir nun die beiden Klassen GraphicCircle und GraphicRectangle. Beide weisen mit der Methode Draw ein gemeinsames Merkmal auf. Wir können nun die Methode Draw aber auch durch eine Schnittstelle bereitstellen, die von GraphicCircle und GraphicRectangle implementiert wird.
    public interface IDraw {
      void Draw();
    }
    public class GraphicCircle : Circle, IDraw {
      ...
      public virtual void Draw() {
        Console.WriteLine("Der Kreis wird gezeichnet");
      }
    }
    public class GraphicRectangle : Rectangle, IDraw {
      ...
      public virtual void Draw() {
        Console.WriteLine("Das Rechteck wird gezeichnet");
      }
    }
Ein erster Blick auf den überarbeiteten Programmcode scheint uns eher Nachteile als Vorteile zu bescheren, denn er ist komplexer geworden. Nun betrachten Sie bitte Abbildung 4.7. Entgegen der ansonsten üblichen Darstellungsweise wird hier die Schnittstelle IDraw wie eine Basisklasse dargestellt. GeometricObject beschreibt alle geometrischen Objekte und bildet damit eine Familie. In gleicher Weise beschreibt IDraw alle Objekte, die gezeichnet werden können. Dazu gehören GraphicCircle und GraphicRectangle. Die beiden letztgenannten sind damit sogar Mitglieder von zwei ganz unterschiedlichen Familien. Hier klicken, um das Bild zu vergrößern Abbildung 4.7 Die Interpretation einer Schnittstelle als Basisklasse und deren Folgen Nutzen können wir daraus erst ziehen, wenn wir eine weitere Klasse codieren – nennen wir sie Auto –, die ebenfalls die Schnittstelle IDraw implementiert.
    public class Auto : IDraw {
      ...
      public void Draw() {
        Console.WriteLine("Das Auto wird gezeichnet");
      }
    }
Was hat nun die Klasse Auto mit beispielsweise GraphicCircle zu tun? Eigentlich nichts! Dennoch haben beide ein gemeinsames Merkmal: Objekte dieser beiden Klassen lassen sich zeichnen, weil beide dieselbe Basis haben und die Methode Draw haben. Sie könnten nun Objekte vom Typ Auto, GraphicCircle und GraphicRectangle in ein Array vom Typ IDraw stecken und in einer Schleife die allen gemeinsame Methode Draw aufrufen, z. B. so:
    IDraw[] arr = new IDraw[5];
    arr[0] = new GraphicCircle();
    arr[1] = new GraphicRectangle();
    arr[2] = new Auto();
    arr[3] = new GraphicCircle();
    arr[4] = new Auto();
    foreach (IDraw item in arr)
      item.Draw();
Die Laufvariable in der Schleife ist vom Typ IDraw. Auf die Referenz der Laufvariablen wird im Anweisungsblock der Schleife die Methode Draw aufgerufen. Da GraphicCircle, GraphicRectangle und Auto die Schnittstelle IDraw implementieren und das damit verbundene Vertragsverhältnis erfüllen, wird der Code fehlerfrei ausgeführt. Natürlich erfolgt der Methodenaufruf polymorph. Nichts anderes haben wir schon gemacht, als wir Flugzeug-, Zeppelin- und Hubschrauber-Objekte einem Array vom Typ der Basisklasse Luftfahrzeug hinzugefügt haben, um anschließend die allen gemeinsame Methode Starten und Landen aufzurufen. Hier noch einmal zum Vergleich der angesprochene Code aus Abschnitt 4.4, »Polymorphie«:
    Luftfahrzeug[] arr = new Luftfahrzeug[5];
    arr[0] = new Flugzeug();
    arr[1] = new Zeppelin();
    arr[2] = new Hubschrauber();
    ...
    foreach (Luftfahrzeug temp in arr) {
      temp.Starten();
      temp.Landen();
    }
Anhand dieser beiden Beispiele bestätigt sich die Aussage, dass Schnittstellen eine Alternative zu der von .NET nicht unterstützten Mehrfachfachvererbung darstellen. #### Schnittstellen als Ersatz exakter Typangaben Nun wollen wir mehrere verschiedene geometrische Objekte miteinander vergleichen und dabei eine Liste erstellen, in der die Objekte der Größe nach sortiert sind. Als Kriterium der Größe soll uns die Fläche der Objekte dienen, sodass wir auch Rechtecke mit Kreisen vergleichen können. Wir müssen nicht unbedingt eine eigene Methode mit einem Sortieralgorithmus schreiben, wir können dabei nämlich auf Methoden zurückgreifen, die in der .NET-Klassenbibliothek zu finden sind. Jetzt stellt sich sofort die Frage: Wie soll das denn geschehen, denn die Architekten der .NET-Klassenbibliothek wussten doch nicht, dass wir mehrere Objekte vom Typ GeometricObject einem Vergleich unterziehen wollen? Wir hätten unsere Klassen Circle, GraphicCircle, Rectangle und GraphicRectangle auch ganz anders benennen können. Treiben wir die Idee noch weiter: Was ist, wenn die Maximalgeschwindigkeit oder der Verkaufspreis mehrerer Autos miteinander verglichen und in eine entsprechende Reihenfolge gebracht werden soll? Zur Lösung des Problems bieten sich mehrere Wege an, die sich aber alle in einem Punkt gleichen: Schnittstellen spielen die alles entscheidende Rolle. Ich möchte Ihnen das an einem Beispiel zeigen, in dem die zu sortierenden Objekte in einem Array zusammengefasst werden:
    GeometricObject[] arr = new GeometricObject[5];
    arr[0] = new Circle(34);
    arr[1] = new Rectangle(10, 230);
    ...
Mithilfe der Klasse Array, die uns die statische Methode Sort zur Verfügung stellt, können wir unsere geometrischen Objekte sortieren. Die Methode ist vielfach überladen. Für uns ist die folgende Überladung von Interesse:
    public static void Sort(Array array, IComparer comparer)
Dem ersten Parameter übergeben wir das zu sortierende Array, in unserem Fall also arr. Der zweite Parameter ist vom Typ der Schnittstelle IComparer. Natürlich können Sie dem Methodenaufruf keine Instanz vom Typ IComparer übergeben, da Schnittstellen nicht instanziierbar sind. So ist die Typangabe des zweiten Parameters auch nicht zu verstehen. Stattdessen verlangt der zweite Parameter lediglich, dass das ihm übergebene Argument ein Objekt ist, das die Schnittstelle IComparer implementiert – egal, ob das Objekt vom Typ DemoClass, Circle, Auto oder Person ist. Denken Sie noch einmal an die Aussagen in diesem Kapitel: Das Objekt einer abgeleiteten Klasse ist gleichzeitig auch ein Objekt der Basisklasse. Außerdem kann eine Schnittstelle wie eine Basisklasse betrachtet werden. Dadurch, dass ein Parameter vom Typ einer Schnittstelle definiert ist, wird uns lediglich vorgeschrieben, dass die Member der Schnittstelle von der Klasse implementiert sind. Im Fall von IComparer handelt es sich um die Methode Compare, die zwei Objekte des angegebenen Arrays miteinander vergleicht. Welche weiteren Member sich noch in der Klassendefinition tummeln, interessiert in diesem Zusammenhang nicht. Sehen wir uns nun die Definition der Schnittstellenmethode an:
    int Compare(Object x, Object y)
Die Methode Sort der Klasse Array kann natürlich nicht wissen, nach welchen Kriterien zwei zu vergleichende Objekte als größer oder kleiner eingestuft werden sollen. Dies ist die Aufgabe des Codes in der Schnittstellenmethode, den Sie schreiben müssen. Anhand des Rückgabewerts (siehe auch Tabelle 4.1) werden die Objekte im Array nach einem internen Algorithmus in Sort umgeschichtet, und zwar so lange, bis alle Array-Elemente in der richtigen Reihenfolge stehen.

Tabelle 4.1 Die Rückgabewerte der statischen Methode »Sort« der Klasse »Array«

Wert | Bedingung
< 0 | x ist kleiner als y.
0 | x und y sind gleich groß.
> 0 | x ist größer als y.
Das folgende Beispielprogramm zeigt den kompletten Code. Er enthält mit CompareClass eine separate Klasse, die nur zur Implementierung der Schnittstelle IComparer dient. Man könnte diese Klasse auch als Vergleichsklasse bezeichnen.
    // ------------------------------------------------------------------
    // Beispiel: ...\Kapitel 4\Sorting
    // ------------------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        GeometricObject[] arr = new GeometricObject[5];
        arr[0] = new Circle(34);
        arr[1] = new Rectangle(10, 230);
        arr[2] = new GraphicCircle(37);
        arr[3] = new Circle(20);
        arr[4] = new GraphicRectangle(12,70);
        Array.Sort(arr, new CompareClass());
        foreach (GeometricObject item in arr)
          Console.WriteLine(item.ToString());
        Console.ReadLine();
      }
    }
    class CompareClass : IComparer {
      public int Compare(object x, object y) {
        return ((GeometricObject)x).CompareTo((GeometricObject)y);
      }
    }
In der Methode Compare kommt uns die Methode Bigger zugute, die in der Klasse GeometricObject enthalten ist und zwei geometrische Objekte miteinander vergleicht. Bigger liefert uns genau den Rückgabewert, den wir der Methode Sort zur Weiterverarbeitung übergeben können.