public
-Member einer Klasse stellen gemeinsam die Schnittstelle einer Klasse dar.
Eine Interface-Deklaration definiert genau eine solche Schnittstelle. Wozu dieser zusätzliche Schritt? Eine Klasse kann mehrere Interfaces implementieren, d.h., Objekte der Klasse können unter
verschiedenen Typen (Interfaces sind auch Typen) auftreten. Auf diese Weise können z.B. Objekte von Klassen unterschiedlichster Funktionalität an Methoden als Parameter übergeben werden,
wenn dieser Parameter von einem Interface-Typ ist. Da die Klasse das Interface implementiert hat, können nun die Methoden des Interface sicher aufgerufen werden.
Sie können sich Interfaces auch als Adapter vorstellen und eine Klasse ist dann ein Multiadapter. Die Klasse kann nun überall dort eingesetzt werden (bzw. ihre Objekte), wo einer der von der Klasse
implementierten Adapter benötigt wird.
Abbildung 8.1: Interfaces sind die Schnittstelle zur konkreten Implementierung
interface
eingeleitet.
public
und internal
verwenden.
I
) vorangestellt wird und der sich ansonsten an die Vorgaben für Bezeichner halten muss.
public
und abstract
, so dass Sie diese Modifizierer weder angeben müssen noch angeben dürfen.
[public
|internal
]interface
IName[: Interface1, Interface2, ...] { // Methoden // Eigenschaften // Indexer // Ereignisse }
ACHTUNG
Oft werden Interfaces als Ersatz für die fehlende Mehrfachvererbung angesehen. Dies ist aber eine irreführende Aussage. Sinn der Mehrfachvererbung ist es ja, etwas von mehreren Klassen zu erben, also auch Funktionalität. Interfaces besitzen aber keine Implementierungen wie z.B. abstrakte Klassen.INFO
Schnittstellen werden Ihnen sehr häufig begegnen. Bekannte Schnittstellen sind z.B.IFormattable
, IComparable
oder IDisposable
.
Die Klassen, welche diese Schnittstellen implementieren, stellen dadurch Methoden zur Verfügung, um eine String
-Repräsentation des Objekts bereitzustellen, um Objekte zu vergleichen oder
um eine gezieltere Freigabe von Ressourcen zu ermöglichen.GetAutor()
und GetVersion()
, welche die jeweilige Information zurückgeben. Neben der
"Vorlagenfunktion" hat dies noch den Vorteil, dass Sie mit einer einzigen Methode, die einen Parameter vom Typ der Schnittstelle entgegennimmt, die Informationen zentral auswerten können -
vorausgesetzt, Sie haben ein Objekt der betreffenden Klassen zur Verfügung.
Die Schnittstelle IVersionsInfo
enthält nur die Methodendeklaration der beiden Methoden ohne eine Implementierung. Diese müssen später die konkreten Klassen vornehmen.
public interface IVersionsInfo { string GetAutor(); string GetVersion(); }Listing 8.1: EinfacheSchnittstellen.cs
abstract
gekennzeichnet werden und es können keine Instanzen der Klasse gebildet werden.
class Name: [Basisklasse,] IInterfaceName[, IInterfaceName...] { // implizite Schnittstellendeklaration publicSchnittstellenMethode() { ... } // explizite Schnittstellendeklaration ISchnittstellenName.SchnittstellenMethode() { ... } }
ACHTUNG
Achten Sie beim Implementieren einer Schnittstelle darauf, dass Sie die Methoden in der implementierenden Klasse mit dem Modifiziererpublic
kennzeichnen. Ansonsten erhalten Sie eine Fehlermeldung des Compilers. In der interface
-Deklaration dürfen Sie public
nicht angeben, da die Methoden automatisch public
sind. INFO
Das Visual Studio unterstützt Sie bei der Implementierung von Schnittstellen über Smarttags. Setzen Sie dazu den Cursor hinter den Schnittstellennamen. Es wird ein kleines Symbol unter dem ersten Buchstaben der Schnittstelle angezeigt. Wenn Sie mit der Maus darüber fahren und dann auf den Pfeil klicken, wird ein Menü geöffnet in dem Sie sich entscheiden können, wie Sie die Schnittstelle implementieren wollen. Die zweite "explizite" Implementierung wird im Anschluss besprochen. Damit Sie bei dieser automatisierten Implementierung nicht vergessen, die Methoden mit "vernünftigen" Anweisungen zu versehen, werden darin Exceptions ausgelöst.Mathe
implementiert die Schnittstelle IVersionsInfo
und muss deshalb die beiden Methoden GetAutor()
und GetVersion()
deklarieren.
Denken Sie daran, dass diese Methoden mit dem Zugriffsmodifizierer public
versehen werden müssen. Die statische Methode DruckeVersionsInfo()
besitzt einen Parameter vom
Typ der Schnittstelle. In der Main()
-Methode wird dieser Methode ein Mathe
-Objekt übergeben. Da die Klasse Mathe
auf jeden Fall die Methoden des Interface
implementiert (sonst würde hier schon der Compiler Widerspruch einlegen), können diese nun sicher in der Methode DruckeVersionsInfo()
aufgerufen werden. Grundsätzlich können beliebige
Objekte mit völlig unterschiedlicher Funktionalität übergeben werden - sicher ist ja, dass sie die Schnittstelle IVersionsInfo
implementieren. Über die Methode GetType()
wird
zusätzlich der "richtige" Typ ausgegeben, der an DruckeVersionsInfo()
übergeben wurde.
Abbildung 8.2: Schnittstellen über Smarttags automatisch implementieren
using System; namespace CSharpBuch.Kap08 { public class EinfacheSchnittstellen { static void Main(string[] args) { Mathe m = new Mathe(); DruckeVersionsInfo(m); Console.ReadLine(); } private static void DruckeVersionsInfo(IVersionsInfo vi) { Console.WriteLine(vi.GetType().ToString()); Console.WriteLine(vi.GetAutor()); Console.WriteLine(vi.GetVersion()); } } public interface IVersionsInfo { string GetAutor(); string GetVersion(); } public class Mathe: IVersionsInfo { private static string version = "0.0.97.123"; private static string autor = "Dirk Frischalowski"; public string GetAutor() { return autor; } public string GetVersion() { return version; } } }Listing 8.2: EinfacheSchnittstellen.cs
INFO
Sie werden es wahrscheinlich nicht immer erreichen, dass eine Schnittstelle genau die Methoden enthält, die von den implementierenden Klassen benötigt werden. Es kann überflüssige Methoden geben, für die es in einigen Klassen nicht sinnvoll ist, sie zu implementieren. Dennoch möchten Sie die Schnittstelle von diesen Klassen implementieren lassen (und nicht mehrere bereitstellen). Eine Lösung, die auch im .NET Framework verwendet wird, löst in den nicht implementierten Methoden eine Exception aus, wie es z.B. auch das Visual Studio bei der automatisierten Implementierung macht. In diesem Fall bemerkt der Entwickler bei deren Aufruf (spätestens der Anwender), dass die Methode nicht zur Verfügung steht, und muss entsprechend darauf reagieren.string IVersionsInfo.GetAutor() { }Weiterhin können Sie auf diese Weise Methoden des Interfaces in der öffentlichen Schnittstelle einer Klasse verbergen, wenn sie nur für interne Aufgaben innerhalb der Klasse benötigt werden. Über die Schnittstelle sind sie aber dennoch verfügbar - dazu gleich ein Beispiel. Ein weiteres Problem kann sich ergeben, wenn mehrere Interfaces Methoden mit gleichen Namen, aber unterschiedlichen Parameterlisten beinhalten. Auch in diesem Fall kann die korrekte Methode beim Aufruf nicht ohne Weiteres ausfindig gemacht werden.
INFO
Sie können durchaus eine implizite Implementierung durch eine einzige Methode vornehmen, die sich aber in mehreren Interfaces befindet. In diesem Fall wird diese Methode für beide Schnittstellen verwendet.IMathe1
und IMathe2
besitzen beide die Methode Add()
, während IMathe1
zusätzlich noch die Methode Sub()
enthält.
Die Klasse Mathe
implementiert nun beide Interfaces und nutzt die explizite Interface-Deklaration, um die Methode Add()
für jedes Interface separat zu implementieren. Vor den
Methoden werden dazu die Interfacenamen angegeben. Sie dürfen in diesem Fall nicht den Modifizierer public
angeben. Die Methode Sub()
wird implizit deklariert, da sie nur in
einem Interface vorkommt.
Alternativ könnten Sie auch die Methode Add()
implizit deklarieren und darin eine der beiden Add()
-Methoden der Interfaces aufrufen (((IMathe2)this).Add()
). Auch hier
muss das Objekt in den Interface-Typ gecastet werden, um die Add()
-Methode der Klasse aufzurufen.
In der Main()
-Methode wird nun ein Mathe
-Objekt erzeugt und die Methode Add()
vom Interface IMathe1
aufgerufen. Dazu ist zwingend ein Cast
notwendig (m as IMathe1)
. Interessant daran ist, dass die Methoden nicht einmal als public
deklariert sind. Da Sub()
implizit deklariert wurde, kann diese Methode
ganz normal über das Objekt aufgerufen werden.
using System; namespace CSharpBuch.Kap08 { public class ExpliziteSchnittstellen { static void Main(string[] args) { Mathe m = new Mathe(); // m.Add(10, 11); => Compilerfehler Console.WriteLine((m as IMathe1).Add(10, 11)); Console.WriteLine(m.Sub(10, 11)); Console.ReadLine(); } } public interface IMathe1 { int Add(int zahl1, int zahl2); int Sub(int zahl1, int zahl2); } public interface IMathe2 { int Add(int zahl1, int zahl2); } public class Mathe : IMathe1, IMathe2 { public int Sub(int zahl1, int zahl2) { return zahl1 - zahl2; } int IMathe1.Add(int zahl1, int zahl2) { return zahl1 + zahl2; } int IMathe2.Add(int zahl1, int zahl2) { return zahl1 + zahl2; } /* public int Add(int zahl1, int zahl2) { return ((IMathe2)this).Add(zahl1, zahl2); } */ } }Listing 8.3: ExpliziteSchnittstellen.cs
Dispose()
zur Verfügung stellt (der Name der Methode ist prinzipiell frei wählbar). Diese
Methode wird manuell aufgerufen und nimmt die notwendigen Schritte, d.h. die Aufräumarbeiten vor, wenn das Objekt nicht mehr benötigt wird. Bei der Arbeit mit Dateien oder Datenbanken
wird statt Dispose()
auch häufig die Methode Close()
verwendet. Dies würde nun grundsätzlich reichen. Das .NET Framework stellt mit der Schnittstelle IDisposable
noch eine etwas umfangreichere Lösung zur Verfügung.
Wenn eine Klasse das Interface IDisposable
implementiert, verpflichtet sie sich, die Methode Dispose()
, welche die einzige Methode des Interface ist, zu implementieren.
Sinn des Ganzen ist es, dass nun beispielsweise eine einfache Prüfmöglichkeit besteht, um herauszufinden, ob ein Objekt eine Methode Dispose()
besitzt und damit Ressourcen vorzeitig freigegeben werden können.
if(objektName is IDisposable) (objektName as IDisposable).Dispose();Wenn Sie direkt mit einem konkreten Objekt arbeiten, wissen Sie natürlich, ob es eine Methode
Dispose()
besitzt, da dies ja bereits IntelliSense im Visual Studio anbietet. Wird ein
Objekt aber an eine Methode übergeben, die einen Parameter vom Typ Object
erwartet, ist dies erst einmal nicht gegeben.
Bei der Implementierung von IDisposable
sind nun mehrere Dinge zu beachten:
Dispose()
manuell aufgerufen wird, müssen Sie sicherstellen, dass der mehrfache (versehentliche) Aufruf erkannt wird und keine negativen Folgen hat.
Dispose()
haben Sie schon alles an Aufräumarbeiten erledigt - kann dies dem Garbage Collector
mitgeteilt werden. Der GC ruft in diesem Fall den Destruktor des Objekts nicht mehr auf.
Dispose()
eine mögliche Dispose()
-Methode der Basisklasse auf.
Dispose()
vergessen wurde bzw. nicht notwendig war, muss der Destruktor die Aufgaben von Dispose()
übernehmen. Dies wird normalerweise
dadurch gelöst, dass der Destruktor seinerseits Dispose()
aufruft, so dass es nur eine Bereinigungsmethode gibt.
INFO
Stellen Sie neben einerDispose()
-Methode zur Sicherheit auch immer einen Destruktor zur Verfügung, da der Aufruf von Dispose()
ja nicht zwingend erforderlich ist. ACHTUNG
In der Dokumentation zum .NET Framework wird auch oft von einerFinalize()
-Methode gesprochen. Diese steht aber nur unter Visual Basic zur Verfügung.
In C# entspricht dies dem Destruktor, der aber zum Teil andere Eigenschaften besitzt. DBZugriff
, die eine Datenbankverbindung nutzen soll, implementiert zum manuellen Schließen der Datenbankverbindung das Interface IDisposable
. Dazu wird die
Methode Dispose()
bereitgestellt. Diese ruft ihrerseits die Methode Dispose()
auf, welche einen booleschen Parameter besitzt. Ziel dieses Umwegs ist es, dass Dispose()
auch vom Destruktor aus aufgerufen werden kann, so dass es nur eine Methode gibt, die für die Freigabe von Ressourcen verantwortlich ist. Dies wäre dann die parametrisierte Dispose(bool)
-Methode.
Weiterhin wird in Dispose()
die Methode SuppressFinalize()
aufgerufen. Dadurch wird der Garbage Collector veranlasst, nicht mehr den Destruktor des im Parameter übergebenen Objekts
aufzurufen, da bereits alles bereinigt ist. Dies führt zu einer geringen Steigerung der Verarbeitungsgeschwindigkeit. In der Methode Dispose(bool)
wird zuerst geprüft, ob Dispose(bool)
bereits aufgerufen wurde. In diesem Fall ist nichts mehr zu tun. Im anderen Fall wird geprüft, ob der Parameter der Methode den Wert true
oder false
hat. Wird die Methode vom
Destruktor aufgerufen, müssen keine Managed Ressourcen freigegeben werden, da dies der Destruktor erledigt. Rufen Sie die Methode aber von Dispose()
aus auf, können auch diese Ressourcen
gleich freigegeben werden. In jedem Fall werden Dateien und Datenbankverbindungen geschlossen, d.h. Operationen durchgeführt, die nicht in das Aufgabengebiet der Garbage Collection fallen. Damit Dispose()
nicht mehrmals ausgeführt wird, wird zum Abschluss die Variable disposeHandled
auf true
gesetzt.
Da für Dateien und Datenbanken die Bereitstellung einer Methode Close()
intuitiver ist, wird auch diese implementiert. Sie ruft ihrerseits wieder die Methode Dispose()
auf.
Die gesamte hier vorgestellte Implementierung von IDisposable
wird im .NET Framework auch unter dem Namen Dispose()
-Pattern geführt.
public class DBZugriff: IDisposable { private bool disposeHandled = false; public DBZugriff() { Console.WriteLine("Objekt erzeugt..."); } public void Close() { Dispose(); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool callDispose) { if(!disposeHandled) { Console.WriteLine("Objekt freigegeben..."); if(callDispose) { // ... Managed-Objekte freigeben } // Dateien oder Datenbankverbindung schließen disposeHandled = true; } } ~DBZugriff() { Dispose(false); Console.WriteLine("Destruktor von Objekt aufgerufen..."); } }Listing 8.4: ManuelleBereinigung.cs
GetLottoZahl(int maxZahl)
und GetAnzahl()
enthält. Die erste Methode liefert eine Zufallszahl im
Bereich von 1 bis maxZahl. Die zweite Methode liefert die Anzahl der zu ziehenden Zahlen, also 5 oder 6. Diese Methoden sollen unter anderem von den Klassen Lotto5Aus35
und Lotto6Aus49
implementiert werden. Verwenden Sie die die Klassen in einer Testanwendung.
Zum Erzeugen von Zufallszahlen verwenden Sie die Klasse Random
aus dem Namespace System
. Die Methode Next(int n)
liefert dazu einen int
-Wert
zwischen 0 und n-1.
Random rd = new Random();
int i = rd.Next(35) + 1;
DateiLogging
, welche Log-Informationen in eine Datei ausgeben soll. Die Dateioperationen werden hier nur durch Ausgaben auf der Konsole
simuliert. Im Konstruktor wird dazu die betreffende Datei geöffnet und bis zur Freigabe nicht wieder geschlossen. Implementieren Sie die Schnittstelle IDisposible
, um die
Datei durch den Aufruf der Methode Dispose()
des Objekts sofort zu schließen.