Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


C# von Eric Gunnerson
Die neue Sprache für Microsofts .NET-Plattform
C# - Zum Katalog
gp Kapitel 11 Schnittstellen
  gp 11.1 Ein einfaches Beispiel
  gp 11.2 Arbeiten mit Schnittstellen
  gp 11.3 Der as-Operator
  gp 11.4 Schnittstellen und Vererbung
  gp 11.5 Entwurfsrichtlinien
  gp 11.6 Mehrfachimplementierung
  gp 11.7 Auf Schnittstellen basierende Schnittstellen

Kapitel 11 Schnittstellen

Schnittstellen stehen in enger Beziehung zu den abstrakten Klassen und gleichen einer abstrakten Klasse, die ausschließlich abstrakte Mitglieder aufweist.


Galileo Computing

11.1 Ein einfaches Beispiel  downtop

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.


Galileo Computing

11.2 Arbeiten mit Schnittstellen  downtop

Ü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.


Galileo Computing

11.3 Der as-Operator  downtop

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.


Galileo Computing

11.4 Schnittstellen und Vererbung  downtop

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.


Galileo Computing

11.5 Entwurfsrichtlinien  downtop

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.


Galileo Computing

11.6 Mehrfachimplementierung  downtop

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.


Galileo Computing

11.6.1 Explizite Schnittstellenimplementierung  downtop

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.


Galileo Computing

11.6.2 Ausblenden der Implementierung  downtop

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.


Galileo Computing

11.7 Auf Schnittstellen basierende Schnittstellen  toptop

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.

   

Select * from SQL Server 2000




Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de