Kapitel 11 Schnittstellen
Schnittstellen stehen in enger Beziehung zu den abstrakten
Klassen und gleichen einer abstrakten Klasse, die ausschließlich
abstrakte Mitglieder aufweist.
11.1 Ein einfaches Beispiel
 
Der folgende Code definiert die Schnittstelle IScalable
und die Klasse TextObject, mit der die Schnittstelle implementiert
wird, d. h., die Klasse enthält alle Versionen sämtlicher
der in der Schnittstelle definierten Funktionen.
public class DiagramObject
{
public DiagramObject() {}
}
interface IScalable
{
void ScaleX(float factor);
void ScaleY(float factor);
}
// Ein Diagrammobjekt, das ebenfalls IScalable implementiert
public class TextObject: DiagramObject, IScalable
{
public TextObject(string text)
{
this.text = text;
}
// ISclalable.ScaleX() implementieren
public void ScaleX(float factor)
{
// Objekt hier skalieren
}
// ISclalable.ScaleY() implementieren
public void ScaleY(float factor)
{
// Objekt hier skalieren
}
private string text;
}
class Test
{
public static void Main()
{
TextObject text = new TextObject("Hello");
IScalable scalable = (IScalable) text;
scalable.ScaleX(0.5F);
scalable.ScaleY(0.5F);
}
}
Über diesen Code wird ein System für Zeichendiagramme implementiert. Alle Objekte werden von DiagramObject
abgeleitet, sodass Sie allgemeine gemeinsame virtuelle Funktionen implementieren können (in diesem Beispiel nicht
dargestellt). Einige der Objekte können skaliert werden, dies wird
durch eine Implementierung der IScalable-Schnittstelle
ausgedrückt.
Das Auflisten des Schnittstellennamens mit dem Basisklassennamen für TextObject
gibt an, dass die Schnittstelle von TextObject implementiert
wird. Dies bedeutet, dass TextObject Funktionen aufweisen
muss, die mit den Funktionen der Schnittstelle übereinstimmen.
Schnittstellenmitglieder besitzen keine Zugriffsmodifikatoren; die Klasse, mit der die Schnittstelle implementiert
wird, legt die Sichtbarkeit der Schnittstellenmitglieder fest.
Wenn ein Objekt eine Schnittstelle implementiert,
kann durch eine Typumwandlung für die Schnittstelle ein Verweis auf die Schnittstelle
abgerufen werden. Dieser Verweis kann anschließend zum Aufrufen
der Funktion für die Schnittstelle eingesetzt werden.
Für dieses Beispiel könnten auch abstrakte
Methoden verwendet werden. Hierzu werden die Methoden ScaleX()
und ScaleY() nach DiagramObject verschoben
und als virtuell deklariert. Im Abschnitt »Entwurfsrichtlinien«
weiter unten in diesem Kapitel wird erläutert, in welchen Situationen
abstrakte Methoden eingesetzt werden und wann eine Schnittstelle verwendet
wird.
11.2 Arbeiten mit Schnittstellen
 
Üblicherweise hat der Code keinerlei Informationen
darüber, ob ein Objekt Unterstützung für eine Schnittstelle
bietet, daher muss geprüft werden, ob das Objekt die Schnittstelle
vor der Typumwandlung implementiert.
using System;
interface IScalable
{
void ScaleX(float factor);
void ScaleY(float factor);
}
public class DiagramObject
{
public DiagramObject() {}
}
public class TextObject: DiagramObject, IScalable
{
public TextObject(string text)
{
this.text = text;
}
// ISclalable.ScaleX() implementieren
public void ScaleX(float factor)
{
Console.WriteLine("ScaleX: {0} {1}", text, factor);
// Objekt hier skalieren
}
// ISclalable.ScaleY() implementieren
public void ScaleY(float factor)
{
Console.WriteLine("ScaleY: {0} {1}", text, factor);
// Objekt hier skalieren
}
private string text;
}
class Test
{
public static void Main()
{
DiagramObject[] dArray = new DiagramObject[100];
dArray[0] = new DiagramObject();
dArray[1] = new TextObject("Text Dude");
dArray[2] = new TextObject("Text Backup");
// Array wird hier mit Klassen initialisiert,
die
// von DiagramObject abgeleitet werden. Einige von diesen implementieren
// IScalable.
foreach (DiagramObject d in dArray)
{
if (d is IScalable)
{
IScalable scalable = (IScalable) d;
scalable.ScaleX(0.1F);
scalable.ScaleY(10.0F);
}
}
}
}
Vor der Typumwandlung wird eine Prüfung durchgeführt,
um den Erfolg der Typumwandlung zu gewährleisten. Falls erfolgreich,
wird das Objekt per cast auf die Schnittstelle gesetzt,
und die Skalierungsfunktionen werden aufgerufen.
Hier wird der Objekttyp zweimal geprüft, einmal über den is-Operator,
das andere Mal als Bestandteil der Typumwandlung. Dies ist überflüssig, da die Typumwandlung
nicht fehlschlagen kann.
Dies kann zwar durch eine Umstrukturierung des Codes
mit Hilfe einer Ausnahmebehandlung umgangen werden, stellt jedoch keine
gute Idee dar, da der Code so weitaus komplexer wird und die Ausnahmebehandlung im Allgemeinen nur für Ausnahmebedingungen
eingesetzt werden sollte. Gleichzeitig ist nicht klar, ob diese Methode
schneller wäre, da die Ausnahmebehandlung auch das Entstehen von
Overhead bewirkt.
11.3 Der as-Operator
 
C# stellt für diese Situation einen besonderen
Operator bereit, den as-Operator. Mit Hilfe dieses Operators
kann die Schleife folgendermaßen umgeschrieben werden:
using System;
interface IScalable
{
void ScaleX(float factor);
void ScaleY(float factor);
}
public class DiagramObject
{
public DiagramObject() {}
}
public class TextObject: DiagramObject, IScalable
{
public TextObject(string text)
{
this.text = text;
}
// ISclalable.ScaleX() implementieren
public void ScaleX(float factor)
{
Console.WriteLine("ScaleX: {0} {1}", text, factor);
// Objekt hier skalieren
}
// ISclalable.ScaleY() implementieren
public void ScaleY(float factor)
{
Console.WriteLine("ScaleY: {0} {1}", text, factor);
// Objekt hier skalieren
}
private string text;
}
class Test
{
public static void Main()
{
DiagramObject[] dArray = new DiagramObject[100];
dArray[0] = new DiagramObject();
dArray[1] = new TextObject("Text Dude");
dArray[2] = new TextObject("Text Backup");
// Array wird hier mit Klassen initialisiert, die
// von DiagramObject abgeleitet werden. Einige von diesen implementieren
// IScalable.
foreach (DiagramObject d in dArray)
{
IScalable scalable = d as IScalable;
if (scalable != null)
{
scalable.ScaleX(0.1F);
scalable.ScaleY(10.0F);
}
}
}
}
Der as-Operator prüft den Typ des linken Operanden. Wenn dieser
explizit in den rechten Operanden konvertiert werden kann, wird als
Ergebnis der Operatorverwendung das Objekt in den rechten Operanden
konvertiert. Schlägt die Konvertierung fehl, gibt der Operator
null zurück.
Sowohl is- als auch as-Operatoren
können mit Klassen eingesetzt werden.
11.4 Schnittstellen und Vererbung
 
Bei der Konvertierung eines Objekts in eine Schnittstelle
wird die Vererbungshierarchie durchsucht, bis eine Klasse ermittelt wird, in der
die Schnittstelle in der zugehörigen Basisliste aufgeführt
wird. Die richtigen Funktionen allein reichen nicht aus.
using System;
interface IHelper
{
void HelpMeNow();
}
public class Base: IHelper
{
public void HelpMeNow()
{
Console.WriteLine("Base.HelpMeNow()");
}
}
// Keine Implementierung von IHelper, obwohl die richtige
// Form vorliegt.
public class Derived: Base
{
public new void HelpMeNow()
{
Console.WriteLine("Derived.HelpMeNow()");
}
}
class Test
{
public static void Main()
{
Derived der = new Derived();
der.HelpMeNow();
IHelper helper = (IHelper) der;
helper.HelpMeNow();
}
}
Dieser Code erzeugt die folgende Ausgabe:
Derived.HelpMeNow()
Base.HelpMeNow()
Die Derived-Version von HelpMeNow() wird beim Aufruf über
die Schnittstelle nicht aufgerufen, obwohl Derived über
eine Funktion der richtigen Form verfügt. Dies rührt daher,
dass Derived keine Implementierung der Schnittstelle vornimmt.
11.5 Entwurfsrichtlinien
 
Schnittstellen und abstrakte Klassen weisen ein ähnliches
Verhalten auf und können in ähnlichen Situationen eingesetzt
werden. Aufgrund ihrer unterschiedlichen Funktionsweise jedoch eignen
sich Schnittstellen in einigen, abstrakte Klassen in anderen Situationen
besser. Nachfolgend ein paar Richtlinien, anhand derer Sie entscheiden
können, ob Sie eine Schnittstelle oder eine abstrakte Klasse verwenden
sollten.
Als Erstes sollten Sie prüfen, ob das Objekt
mit Hilfe einer »Ist-Ein(e)«-Beziehung ausgedrückt werden
kann. Mit anderen Worten, handelt es sich bei der Fähigkeit um
ein Objekt und sind die abgeleiteten Klassen Beispiele dieses Objekts?
Eine weitere Möglichkeit besteht darin, sich
klarzumachen, welche Art von Objekten diese Fähigkeit einsetzen
würde. Ist die Fähigkeit für verschiedene Objekte nützlich,
die nicht wirklich zueinander in Beziehung stehen, stellt eine Schnittstelle
die richtige Wahl dar.
Bei der Verwendung von Schnittstellen müssen
Sie bedenken, dass für eine Schnittstelle keine Unterstützung
für die Versionssteuerung vorhanden ist. Wird einer Schnittstelle eine Funktion
hinzugefügt, nachdem diese bereits durch die Benutzer verwendet
wurde, führt dies zu einer Unterbrechung des Benutzercodes zur Laufzeit. Die zugehörigen Klassen können
die Schnittstelle erst ordnungsgemäß implementieren, wenn
geeignete Änderungen vorgenommen wurden.
11.6 Mehrfachimplementierung
 
Im Gegensatz zur Objektvererbung kann eine Klasse
mehr als eine Schnittstelle implementieren.
interface IFoo
{
void ExecuteFoo();
}
interface IBar
{
void ExecuteBar();
}
class Tester: IFoo, IBar
{
public void ExecuteFoo() {}
public void ExecuteBar() {}
}
Dies funktioniert prima, wenn keine Namenskollisionen zwischen den Funktionen der Schnittstellen auftreten.
Wenn das Beispiel jedoch leicht abgeändert wird, kann ein Problem
auftreten:
// Fehler
interface IFoo
{
void Execute();
}
interface IBar
{
void Execute();
}
class Tester: IFoo, IBar
{
// IFoo- oder IBar-Implementierung?
public void Execute() {}
}
Wird über Tester.Execute() IFoo.Execute() oder
IBar.Execute() implementiert?
Dies ist unklar, daher gibt der Compiler einen Fehler
aus. Steuert der Benutzer eine der Schnittstellen, könnte einer
der Schnittstellennamen geändert werden, auch wenn dies keine optimale
Lösung ist. Warum sollte IFoo den Namen der zugehörigen
Funktion ändern, nur weil IBar den gleichen Namen
verwendet?
Mal im Ernst, wenn IFoo und IBar
Produkte unterschiedlicher Hersteller sind, können sie nicht geändert
werden.
Die .NET-Laufzeitumgebung und C# unterstützen eine Technik, die als explizite
Schnittstellenimplementierung bezeichnet wird. Mit dieser Technik kann angegeben
werden, welches Schnittstellenmitglied implementiert wird.
11.6.1 Explizite Schnittstellenimplementierung
 
Sie können festlegen, welche Schnittstelle eine
Mitgliedsfunktion implementiert. Sie bestimmen die Mitgliedsfunktion,
indem Sie den Schnittstellennamen dem Mitgliedsnamen voranstellen.
Hier noch einmal das vorangegangene Beispiel, diesmal
mit expliziter Schnittstellenimplementierung:
using System;
interface IFoo
{
void Execute();
}
interface IBar
{
void Execute();
}
class Tester: IFoo, IBar
{
void IFoo.Execute()
{
Console.WriteLine("IFoo.Execute implementation");
}
void IBar.Execute()
{
Console.WriteLine("IBar.Execute implementation");
}
}
class Test
{
public static void Main()
{
Tester tester = new Tester();
IFoo iFoo = (IFoo) tester;
iFoo.Execute();
IBar iBar = (IBar) tester;
iBar.Execute();
}
}
Der Code erzeugt diese Ausgabe:
IFoo.Execute implementation
IBar.Execute implementation
Genau dies haben wir erwartet. Aber was macht die
folgende Testklasse?
// Fehler
using System;
interface IFoo
{
void Execute();
}
interface IBar
{
void Execute();
}
class Tester: IFoo, IBar
{
void IFoo.Execute()
{
Console.WriteLine("IFoo.Execute implementation");
}
void IBar.Execute()
{
Console.WriteLine("IBar.Execute implementation");
}
}
class Test
{
public static void Main()
{
Tester tester = new Tester();
tester.Execute();
}
}
Wird IFoo.Execute() oder wird IBar.Execute()
aufgerufen?
Die Antwort lautet, dass keine von beiden aufgerufen
wird. Es ist in der Tester-Klasse kein Zugriffsmodifikator
für die Implementierungen von IFoo.Execute() und IBar.Execute()
vorhanden, daher sind die Funktionen privat und können nicht aufgerufen
werden.
Dieses Verhalten ist nicht darauf zurückzuführen,
dass kein public-Modifikator für die Funktion verwendet
wurde, sondern darauf, dass die Verwendung von Zugriffsmodifikatoren bei expliziten Implementierungen nicht zulässig
ist. Die einzige Möglichkeit, auf eine Schnittstelle zuzugreifen,
besteht darin, dass Objekt per cast auf die geeignete Schnittstelle
zu setzen.
Zur Offenlegung einer der Funktionen wird Tester
eine Weiterleitungsfunktion hinzugefügt:
using System;
interface IFoo
{
void Execute();
}
interface IBar
{
void Execute();
}
class Tester: IFoo, IBar
{
void IFoo.Execute()
{
Console.WriteLine("IFoo.Execute implementation");
}
void IBar.Execute()
{
Console.WriteLine("IBar.Execute implementation");
}
public void Execute()
{
((IFoo)this).Execute();
}
}
class Test
{
public static void Main()
{
Tester tester = new Tester();
tester.Execute();
}
}
Nun führt das Aufrufen der Execute()-Funktion
für eine Instanz von Tester zu einer Weiterleitung
an Tester.IFoo.Execute().
Diese Möglichkeit der Ausblendung kann auch
für andere Zwecke eingesetzt werden, wie im nächsten Abschnitt
genauer erläutert wird.
11.6.2 Ausblenden der Implementierung
 
Es kann Fälle geben, in denen es sinnvoll ist,
die Implementierung einer Schnittstelle vor den Benutzern einer Klasse
zu verbergen, einmal deshalb, weil dies im Allgemeinen vorzuziehen ist, oder einfach nur, um
die Mitgliederzahl gering zu halten. Die Objektverwendung kann auf diese
Weise erheblich vereinfacht werden. Beispiel:
using System;
class DrawingSurface
{
}
interface IRenderIcon
{
void DrawIcon(DrawingSurface surface, int x, int y);
void DragIcon(DrawingSurface surface, int x, int y, int x2, int
y2);
void ResizeIcon(DrawingSurface surface, int xsize, int ysize);
}
class Employee: IRenderIcon
{
public Employee(int id, string name)
{
this.id = id;
this.name = name;
}
void IRenderIcon.DrawIcon(DrawingSurface surface, int x, int y)
{
}
void IRenderIcon.DragIcon(DrawingSurface surface, int x, int y,
int x2, int y2)
{
}
void IRenderIcon.ResizeIcon(DrawingSurface surface, int xsize, int
ysize)
{
}
int id;
string name;
}
Wenn die Schnittstelle auf normale Weise implementiert
worden wäre, wären die Mitgliedsfunktionen DrawIcon(),
DragIcon() und ResizeIcon() als Teil von Employee
sichtbar, was die Benutzer der Klasse verwirren könnte. Durch das
explizite Implementieren der Funktionen kann nur über die Schnittstelle
auf sie zugegriffen werden.
11.7 Auf Schnittstellen basierende Schnittstellen
 
Schnittstellen können zu neuen Schnittstellen
kombiniert werden. Die Schnittstellen ISortable und ISerializable
können miteinander kombiniert werden, sodass neue Mitglieder hinzugefügt
werden können.
using System.Runtime.Serialization;
using System;
interface IComparableSerializable :
IComparable, ISerializable
{
string GetStatusString();
}
Eine Klasse, die IComparableSerializable
implementiert, müsste alle Mitglieder von IComparable,
ISerializable und der Funktion GetStatusString()
implementieren, die in IComparableSerializable eingeführt
werden.
|