Programmieren in C#: Einführung


Kapitel 4: Klassenhierarchien


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.


4.1 Allgemeines

Klassen in Beziehung setzen

Die Objektorientierung versucht, die Softwareentwicklung zu vereinfachen, indem Entwickler tatsächlich existierende Objekte in einer Programmiersprache nachbilden. Dies geschieht wie Sie inzwischen wissen über Klassen, die Eigenschaften und Fähigkeiten von Objekten beschreiben.

Es ist nun auch möglich, Klassen zueinander in Beziehung zu setzen. Die Objektorientierung stellt hierzu ein Instrument zur Verfügung, das Vererbung heißt. Auch dieses Instrument dient dem alleinigen Zweck, die Wirklichkeit besser abzubilden, indem tatsächlich existierende Beziehungen zwischen Objekten in den Quellcode hinüber genommen werden.


4.2 Vererbung

Von allgemeinen zu speziellen Klassen

Die Vererbung macht es möglich, Klassen in eine gemeinsame Klassenhierarchie einzuordnen. Dabei werden Klassen zu Eltern- oder Kindklassen anderer Klassen.

In C# hat jede Klasse genau eine einzige Elternklasse. Sie kann jedoch beliebig viele Kindklassen haben. Auf diese Weise entsteht eine Verästelung, die sich noch unten auffächert und an deren Spitze eine einzige Elternklasse steht. Diese Klasse, die in der Hierarchie ganz oben steht, ist die einzige, die ihrerseits keine Elternklasse hat.

Auszug aus der Klassenhierarchie des .NET-Frameworks

Alle existierenden Klassen im .NET-Framework sind Bestandteil einer einzigen Klassenhierarchie, an deren Spitze die Klasse Object aus dem Namensraum System steht. Alle Klassen - egal, ob von Microsoft im .NET-Framework oder von Ihnen in Programmen definiert - sind direkte oder indirekte Kindklassen von Object. Sie sind alle direkt oder indirekt von Object abgeleitet.

So sind zum Beispiel die in diesem Buch in verschiedenen Beispielprogrammen verwendeten Klassen Console und String direkte Kindklassen von Object. Die Klassen Int32 und Process sind indirekte Kindklassen, da zwischen ihnen und der obersten Elternklasse Object eine oder mehrere andere Klassen stehen.

Die Vererbung hat den Sinn, aus vielen einzelnen Klassen ein Gerüst zu erstellen, das Beziehungen zwischen Objekten in der Wirklichkeit nachahmen soll. Je weiter oben Klassen in der Klassenhierarchie stehen, desto allgemeiner sind sie. Spezialisierte Klassen finden Sie in den unteren Schichten.

Die allgemeinste Klasse im .NET-Framework ist Object. Da alle Klassen Objekte beschreiben, macht es Sinn, wenn alle anderen Klassen Kindklassen von Object sind. Eine allgemeinere Klasse als Object kann es nicht geben.

Sie müssen nichts im Code angeben, wenn Sie eine Klasse erstellen und Ihre Klasse von Object abgeleitet sein soll. Jede Klasse ist standardmäßig von Object abgeleitet, wenn nicht explizit eine andere Elternklasse angegeben wird. So war zum Beispiel auch die im vorherigen Kapitel entwickelte Klasse Favorite eine direkte Kindklasse von Object.

Um Vererbungslinien zu erstellen, die aus mehr als zwei Generationen bestehen, müssen für eine Klasse Elternklassen explizit angegeben werden. Dies ist zum Beispiel in der Vererbungslinie von Process der Fall: Die Klasse Process ist explizit von einer Klasse Component abgeleitet, die wiederum explizit von einer Klasse MarshalByRefObject abgeleitet ist. Da laut Dokumentation für MarshalByRefObject keine Elternklasse explizit angegeben ist, ist sie automatisch von Object abgeleitet.

Das Instrument der Vererbung bewirkt, dass Kindklassen Merkmale ihrer Elternklasse erben. So stehen in Kindklassen alle Merkmale einer Elternklasse zur Verfügung, so als ob diese Merkmale in der Kindklasse selbst definiert worden wären. Da alle Klassen direkt oder indirekt von Object abgeleitet sind, sind die in Object definierten Merkmale in allen existierenden Klassen verfügbar.

Wenn Sie sich die Dokumentation von Object ansehen, finden Sie zum Beispiel eine Methode Equals(), die in dieser Klasse definiert ist. Dieser Methode kann ein Objekt übergeben werden, um zu überprüfen, ob es mit dem Objekt, für das die Methode aufgerufen wird, identisch ist. Weil alle Klassen direkt oder indirekt von Object abgeleitet sind, steht diese Methode allen Klassen zur Verfügung.

using System; 
using System.Diagnostics; 

class Favorite 
{ 
  string name; 
  Uri url; 

  public Favorite(string name, Uri url) 
  { 
    this.name = name; 
    this.url = url; 
  } 

  public void Open() 
  { 
    Process.Start(url.ToString()); 
  } 
} 

class Program 
{ 
  static void Main(string[] args) 
  { 
    var favHighscore = new Favorite("Highscore - Programmieren lernen", new Uri("http://www.highscore.de/")); 
    var favHighscore2 = new Favorite("Highscore - Programmieren lernen", new Uri("http://www.highscore.de/")); 
    Console.Out.WriteLine(favHighscore.Equals(favHighscore2)); 
  } 
} 

Im obigen Beispiel wird die im vorherigen Kapitel entwickelte Klasse Favorite zweimal instantiiert und mit den gleichen Werten initialisiert. Es wird dann für das eine Objekt Equals() aufgerufen und als Parameter das andere Objekt übergeben.

Obwohl Favorite keine Methode Equals() definiert, wird der Code vom C#-Compiler anstandslos übersetzt. Denn Favorite erbt die Methode Equals() von der Elternklasse Object. Es ist zwar nirgendwo explizit angegeben, dass Object die Elternklasse von Favorite ist. Das muss es aber auch nicht, weil Object in diesem Fall automatisch die Elternklasse ist.

Wenn Sie obiges Programm ausführen, wird False ausgegeben. Das verwundert Sie möglicherweise, denn beide Favoriten speichern immerhin die exakt gleichen Werte. Der Grund, warum False ausgegeben wird, ist, dass Equals() bei Referenzvariablen vergleicht, ob diese identisch sind - und nicht, ob die Objekte identisch sind, auf die die Referenzvariablen verweisen. Die beiden Objekte vom Typ Favorite speichern zwar die gleichen Werte. Da es sich aber um zwei unterschiedliche Objekte handelt und favHighscore auf das eine und favHighscore2 auf das andere Objekt verweist, wird False ausgegeben.

Wenn Sie möchten, dass bei einem Vergleich zweier Objekte vom Typ Favorite nicht Referenzvariablen, sondern die Objekte selbst auf Gleichheit überprüft werden, können Sie Equals() für die Klasse Favorite neu definieren. Eine derartige Neudefinition einer Methode in einer Kindklasse bezeichnet man als Überschreiben.


4.3 Methoden überschreiben

Methoden für Kindklassen anpassen

Die Klasse Favorite soll nun die von Object geerbte Methode Equals() überschreiben, damit true zurückgegeben wird, wenn zwei Objekte vom Typ Favorite die gleichen Daten speichern.

using System; 
using System.Diagnostics; 

class Favorite 
{ 
  string name; 
  Uri url; 

  public Favorite(string name, Uri url) 
  { 
    this.name = name; 
    this.url = url; 
  } 

  public void Open() 
  { 
    Process.Start(url.ToString()); 
  } 

  public override bool Equals(Object obj) 
  { 
    Favorite fav = (Favorite)obj; 
    return name == fav.name && url == fav.url; 
  } 
} 

class Program 
{ 
  static void Main(string[] args) 
  { 
    var favHighscore = new Favorite("Highscore - Programmieren lernen", new Uri("http://www.highscore.de/")); 
    var favHighscore2 = new Favorite("Highscore - Programmieren lernen", new Uri("http://www.highscore.de/")); 
    Console.Out.WriteLine(favHighscore.Equals(favHighscore2)); 
  } 
} 

Um eine Methode zu überschreiben, muss der Methodenkopf - also Rückgabewert, Name und Parameterliste - exakt mit dem Methodenkopf übereinstimmen, der von der Elternklasse vorgegeben wird. Für die Methode Equals() bedeutet das, dass sie einen Rückgabewert vom Typ bool besitzen und einen Parameter vom Typ Object erwarten muss. Außerdem muss die Methode öffentlich sein - das Zugriffsattribut public muss gesetzt sein. Genau so ist diese Methode nämlich in der Klasse Object definiert.

Damit sich der Compiler nicht wundert, dass eine Methode definiert wird, die exakt so von der Elternklasse geerbt wird, muss außerdem das Schlüsselwort override vorangestellt werden. Damit ist dem Compiler klar, dass Sie diese Methode tatsächlich überschreiben wollen und nicht versehentlich eine Methode definieren, die genauso geerbt wird.

Um herauszufinden, ob ein Favorit identisch mit einem anderen ist, müssen lediglich die Felder name und url verglichen werden. Wie Sie sehen erfolgt dieser Vergleich nicht mit Equals(), sondern mit einem Operator, der aus zwei Gleichheitszeichen besteht. Es handelt sich hierbei um einen Vergleichsoperator: == gibt true zurück, wenn links und rechts das gleiche steht - andernfalls false.

Damit auf die Felder name und url zugegriffen werden kann, muss der Parameter obj von Object in Favorite umgewandelt werden. Denn nur Objekte vom Typ Favorite besitzen die Felder name und url. Diese Umwandlung erfolgt, indem der entsprechende Datentyp - hier Favorite - in runden Klammern vor die Referenzvariable obj gesetzt wird. Man bezeichnet diese Umwandlung als Casting.

Wie Sie anhand von Equals() und dem Vergleichsoperator == sehen, gibt es mehrere Möglichkeiten, Variablen miteinander zu vergleichen. Grundsätzlich sind Equals() und == gleich: Wenn sie auf Referenzvariablen angewandt werden, vergleichen sie, ob die Referenzvariablen auf das gleiche Objekt zeigen. Beide geben false zurück, wenn sie auf unterschiedliche Objekte zeigen - selbst, wenn diese gleiche Daten speichern.

Bei Wertvariablen werden übrigens immer Objekte auf Gleichheit überprüft. Hier gibt es schließlich keine Unterscheidung zwischen Variable und Objekt, da Wertvariablen Objekte sind.

So wie Equals() kann auch der Vergleichsoperator == überschrieben und für Klassen neudefiniert werden. Und dies hat Microsoft sowohl bei String als auch bei Uri getan. Für diese beiden Klassen bedeutet ein Vergleich mit ==, dass true zurückgegeben wird, wenn die Objekte die gleichen Daten speichern.

Genausogut könnte in diesem Fall jedoch auch Equals() verwendet werden:

return name.Equals(fav.name) && uri.Equals(fav.uri); 

Denn die Klassen String und Uri überschreiben nicht nur den Vergleichsoperator ==, sondern auch Equals(). Deswegen würde auch mit obiger Zeile der Vergleich so wie gewünscht durchgeführt werden.

Wenn Sie die Möglichkeiten, Variablen zu vergleichen, für verwirrend halten, dann ist das leider wahr. Von der Grundregel, dass sowohl == als auch Equals() Referenzvariablen vergleichen und nicht Objekte, auf die sie zeigen, kann jederzeit abgewichen werden. Mit String und Uri haben Sie genau zwei Vertreter dieser Klassen, die von der Grundregel abweichen. Und nachdem Equals() soeben in der Klasse Favorite überschrieben wurde, weicht nun auch diese Klasse von der Grundregel ab.

Diese Abweichungen können durchaus Sinn machen. So möchten wir im obigen Beispiel bei einem Vergleich herausfinden, ob zwei Favoriten die gleichen Daten speichern - völlig egal, ob die zum Vergleich herangezogenen Referenzvariablen auf zwei unterschiedliche Objekte zeigen oder nicht. Das Überschreiben von Equals() macht einen derartigen Vergleich erst möglich.

Problematisch wird es dann, wenn nicht mehr klar zu erkennen ist, was überschriebene Methoden genau tun. Beim Überschreiben sollte daher darauf geachtet werden, dass die Bedeutung der überschriebenen Methode nicht völlig geändert wird, sondern für Entwickler, die die Klasse später einsetzen werden, intuitiv erkennbar bleibt. Das ist nicht immer einfach. Versuchen Sie zum Beispiel für folgendes Programm, die Ergebnisse vorherzusagen.

using System; 

class Program 
{ 
  static void Main(string[] args) 
  { 
    var i = new Int32(); 
    i = 1; 
    var i2 = new Int32(); 
    i2 = 1; 
    Console.Out.WriteLine(i == i2); 
    Console.Out.WriteLine(i.Equals(i2)); 

    var array = new int[] { 1, 2, 3 }; 
    var array2 = new int[] { 1, 2, 3 }; 
    Console.Out.WriteLine(array == array2); 
    Console.Out.WriteLine(array.Equals(array2)); 
  } 
} 

Die beiden Vergleiche der Variablen i und i2 ergeben true. Das liegt daran, dass es sich bei i und i2 um keine Referenzvariablen handelt, sondern um Wertvariablen. Denn Int32 ist keine Klasse, sondern eine Struktur. Die beiden Variablen hätten auch anders definiert werden können, was üblicherweise in der Praxis auch gemacht wird, weil es klarer ist:

int i = 1; 
int i2 = 1; 

Die beiden Vergleiche der Arrays, die gleich groß sind und die gleichen Zahlen speichern, ergeben false. Die Klasse Array überschreibt weder den Vergleichsoperator == noch die Methode Equals(). In der Tat gibt es keine einfache Möglichkeit, Arrays auf gleiche Werte hin zu vergleichen.

Beachten Sie im obigen Beispiel auch die Möglichkeit, Arrays mit geschweiften Klammern zu initialisieren. In diesem Fall muss zwischen den eckigen Klammern keine Größe für das Array angegeben werden, weil der Compiler die Größe des Arrays automatisch anhand der in den geschweiften Klammern angegebenen Werte ermittelt.

Wenn Sie möchten, können Sie sogar das new int[] weglassen. So kann ein Array auch wie folgt erstellt und gleichzeitig initialisiert werden:

int[] array = { 1, 2, 3 }; 

Im obigen Fall muss der Datentyp der Variablen jedoch explizit auf int[] gesetzt werden. Das Schlüsselwort var darf nicht mehr verwendet werden, weil der Compiler nicht herleiten könnte, welche Daten das Array eigentlich genau speichern soll.

Möchten Sie übrigens tatsächlich Referenzvariablen und nicht die Objekte, auf die sie zeigen, auf Gleichheit überprüfen - völlig unabhängig davon, ob == oder Equals() überschrieben wurden - können Sie die Methode ReferenceEquals() aufrufen. Diese Methode ist ebenfalls in der Klasse Object definiert und steht somit jedem Objekt zur Verfügung. Diese Methode kann außerdem nicht überschrieben werden, weil sie statisch ist.


4.4 Polymorphie

Objekte, die mehrere Datentypen unterstützen

Vererbung bedeutet, dass für eine Kindklasse alle Merkmale einer Elternklasse verwendet werden können, so als wären sie in der Kindklasse definiert worden. Da die Kindklasse all das hat und kann, was die Elternklasse bietet, können Objekte vom Typ der Kindklasse überall da verwendet werden, wo eigentlich Objekte vom Typ der Elternklasse erwartet werden: Ein Objekt vom Typ einer Kindklasse kann ein Objekt vom Typ einer Elternklasse ersetzen. Es hat mehrere Typen.

Objekte, die sich wie Objekte anderer Klassen verhalten können, bezeichnet man als polymorph. Polymorphie ist demnach die Fähigkeit, mehr als einen Datentyp zu unterstützen. Wie das in der Praxis aussieht und welche Vorteile das hat, sehen Sie im Folgenden.

using System; 
using System.Diagnostics; 

class Favorite 
{ 
  string name; 
  Uri url; 

  public Favorite(string name, Uri url) 
  { 
    this.name = name; 
    this.url = url; 
  } 

  public void Open() 
  { 
    Process.Start(url.ToString()); 
  } 
} 

class Favorite2 : Favorite 
{ 
  DateTime dateTime; 

  public Favorite2(string name, Uri url) 
    : base(name, url) 
  { 
    dateTime = DateTime.Now; 
  } 
} 

Die Klasse Favorite erhält im obigen Beispielcode eine Kindklasse Favorite2. Favorite2 soll grundsätzlich all das sein, was Favorite ist - deswegen heißt die Klasse auch ähnlich. Während Favorite aber lediglich einen Namen und eine Adresse speichert, soll Favorite2 zusätzlich den Zeitpunkt speichern, zu dem der Favorit angelegt wird.

Natürlich könnte hier auch direkt die Klasse Favorite erweitert werden. Gehen Sie aber zum Beispiel davon aus, dass die Klasse Favorite von einem anderen Entwickler stammt und Teil einer Klassenbibliothek ist, die Sie nicht ändern können.

Um die Klasse Favorite2 von Favorite abzuleiten, muss hinter dem Klassennamen ein Doppelpunkt gefolgt vom Namen der Elternklasse gesetzt werden. Damit ist Favorite2 keine direkte Kindklasse von Object mehr, sondern erbt von Favorite.

Während Sie bisher davon ausgehen, dass eine Klasse sämtliche Merkmale von ihrer Elternklasse erbt, stimmt dies genaugenommen nicht: Konstruktoren und Destruktoren werden nie vererbt. Damit auch Objekte vom Typ Favorite2 mit einem Namen und einer Adresse initialisiert werden können, muss daher ein Konstruktor explizit definiert werden.

Wenn der Name und die Adresse dem Konstruktor von Favorite2 übergeben werden, müssen die Daten irgendwie in die von der Elternklasse geerbten Felder name und url abgelegt werden. Dies erfolgt üblicherweise über den Aufruf eines Konstruktors der Elternklasse.

Um den Konstruktor einer Elternklasse aufzurufen, müssen Sie hinter dem Methodenkopf einen Doppelpunkt und das Schlüsselwort base setzen. base bezieht sich auf die Elternklasse. Auf diese Weise kann ein Konstruktor der Elternklasse aufgerufen werden. Da Favorite einen geeigneten Konstruktor anbietet, reicht der Konstruktor von Favorite2 die Parameter an diesen weiter. Der Konstruktor der Elternklasse - letztendlich auch nur eine ganz normale Methode - legt dann die Daten in den Feldern name und url ab.

Weil Favorite2 den Zeitpunkt speichern soll, zu dem ein Favorit angelegt wird, wird ein Feld dateTime vom Typ DateTime definiert. Es handelt sich hierbei um eine Struktur, die im Namensraum System definiert ist. Diese Struktur bietet eine statische Eigenschaft Now an, die den aktuellen Zeitpunkt vom Typ DateTime zurückgibt. Auf genau diese Eigenschaft wird im Konstruktor zugegriffen, um das Feld dateTime zu initialisieren.

Anstatt den Namen und die Adresse über den Konstruktor der Elternklasse zu initialisieren, könnten Sie auch auf die Idee kommen, die Felder name und url per Zuweisungsoperator zu initialisieren:

class Favorite2 : Favorite 
{ 
  DateTime dateTime; 

  public Favorite2(string name, Uri url) 
    : base(name, url) 
  { 
    this.name = name; 
    this.url = url; 
    dateTime = DateTime.Now; 
  } 
} 

Während obiger Code grundsätzlich in Ordnung ist, würde sich der Compiler trotzdem weigern, ihn zu kompilieren. Das Problem ist, dass name und url private Merkmale sind und private Merkmale ausschließlich in der Klasse verwendet werden können, in der sie definiert sind. Obwohl die Klasse Favorite2 die beiden Felder erbt, kann sie tatsächlich nicht direkt auf sie zugreifen.

Um der Kindklasse einen direkten Zugriff auf name und url einzuräumen, könnten die Felder öffentlich gemacht werden. Dies würde aber das Prinzip der Datenkapselung verletzen, weil dann wirklich jeder - also auch völlig andere Klassen - auf diese Felder zugreifen könnten.

Um Merkmale Kindklassen zugänglich zu machen, sie aber weiterhin gegenüber anderen Klassen zu schützen, bietet C# das Schlüsselwort protected an. Indem vor die Felder name und url das Schlüsselwort protected gesetzt wird, können sie in Kindklassen verwendet werden.

using System; 
using System.Diagnostics; 

class Favorite 
{ 
  protected string name; 
  protected Uri url; 

  public Favorite() 
  { 
  } 

  public Favorite(string name, Uri url) 
  { 
    this.name = name; 
    this.url = url; 
  } 

  public void Open() 
  { 
    Process.Start(url.ToString()); 
  } 
} 

class Favorite2 : Favorite 
{ 
  DateTime dateTime; 

  public Favorite2(string name, Uri url) 
  { 
    this.name = name; 
    this.url = url; 
    dateTime = DateTime.Now; 
  } 
} 

Nachdem im Konstruktor von Favorite2 direkt auf die geerbten Felder name und url zugegriffen wird, um sie zu initialisieren, ist kein expliziter Aufruf eines Konstruktors der Elternklasse mehr notwendig. Wenn kein Konstruktor explizit aufgerufen wird, wird jedoch der Standardkonstruktor implizit aufgerufen. Das wiederum funktioniert natürlich nur, wenn der Standardkonstruktor existiert. Deswegen muss im obigen Beispiel der Standardkonstruktor zur Klasse Favorite hinzugefügt werden.

Wenn Sie eine Klasse erstellen, müssen Sie darüber nachdenken, ob Sie oder andere Entwickler eventuell später einmal eine neue Klasse von dieser Klasse ableiten möchten. Für diesen Fall müssen Sie sich überlegen, welche geschützten Merkmale in einer Kindklasse zugänglich sein sollen. Diese Merkmale dürfen Sie dann nicht mit private schützen, sondern müssen sie mit protected definieren.

Während public-Merkmale die Schnittstelle einer Klasse nach außen darstellen, stellen protected-Merkmale die Schnittstelle einer Klasse zu Kindklassen dar. private-Merkmale hingegen können immer nur in der Klasse verwendet werden, in der sie definiert sind.

Im folgenden Beispiel wird ein Objekt vom Typ Favorite2 erstellt und an eine Methode übergeben, die eigentlich ein Objekt vom Typ Favorite erwartet. Der Grund, warum das Programm problemlos kompiliert und ausgeführt werden kann, liegt darin, dass alle Objekte vom Typ Favorite2 all das sein können, was Objekte vom Typ Favorite sind. Schließlich hat Favorite2 alle Merkmale von Favorite geerbt.

using System; 
using System.Diagnostics; 

class Favorite 
{ 
  string name; 
  Uri url; 

  public Favorite(string name, Uri url) 
  { 
    this.name = name; 
    this.url = url; 
  } 

  public void Open() 
  { 
    Process.Start(url.ToString()); 
  } 
} 

class Favorite2 : Favorite 
{ 
  DateTime dateTime; 

  public Favorite2(string name, Uri url) 
    : base(name, url) 
  { 
    dateTime = DateTime.Now; 
  } 
} 

class Program 
{ 
  static void OpenFavorite(Favorite fav) 
  { 
    fav.Open(); 
  } 

  static void Main(string[] args) 
  { 
    var favHighscore = new Favorite2("Highscore - Programmieren lernen", new Uri("http://www.highscore.de/")); 
    OpenFavorite(favHighscore); 
  } 
} 

Im obigen Programm wird ein Objekt vom Typ Favorite2 erstellt und an eine Methode OpenFavorite() übergeben, die einen Parameter vom Typ Favorite erwartet. Weil das Objekt vom Typ Favorite2 auf einer Klasse basiert, die von Favorite abgeleitet ist, kann es sich wie ein Objekt vom Typ Favorite verhalten und daher problemlos an die Methode OpenFavorite() übergeben werden. Das ist es, was man unter Polymorphie versteht.

Der Vorteil von Polymorphie ist, dass alter Code auf neuen Code zugreifen kann. Stellen Sie sich vor, ein anderer Programmierer hätte die Klasse Favorite und die Methode OpenFavorite() entwickelt. Sie können die Klasse Favorite nicht verwenden, weil Sie den Zeitpunkt speichern müssen, zu dem ein Favorit erstellt wird - etwas, was die Klasse Favorite nicht unterstützt. Sie können aber eine Kindklasse Favorite2 erstellen und Objekte vom Typ dieser Kindklasse an die Methode OpenFavorite() übergeben, obwohl der Programmierer, der OpenFavorite() entwickelte hatte, gar nicht wusste, dass Sie eines Tages eine Klasse Favorite2 entwickeln. Dank Polymorphie kann die früher entwickelte Methode OpenFavorite() tatsächlich Objekte verwenden, die auf einer später entwickelten Klasse basieren.


4.5 Virtuelle Methoden

Methodenaufrufe zur Laufzeit Datentypen zuordnen

Die im vorherigen Abschnitt entwickelte Klasse Favorite2 soll nun derart erweitert werden, dass jeder Aufruf des Favoriten gezählt wird. Jedesmal, wenn die Methode Open() aufgerufen wird, soll eine Variable um 1 erhöht werden. Dies soll Ihnen helfen zu erkennen, welche Favoriten besonders beliebt sind und häufiger verwendet werden als andere.

Die Methode Open(), die von Favorite geerbt wird, muss irgendwie erweitert werden, um eine Zählvariable pro Aufruf zu erhöhen. Um die Methode zu ändern, muss sie, wie Sie bereits in diesem Kapitel erfahren haben, überschrieben werden. Dazu muss die Methode mit exakt dem Methodenkopf, der in der Elternklasse verwendet wird, in der Kindklasse definiert werden.

Sie hatten bereits gesehen, dass beim Überschreiben das Schlüsselwort override angegeben werden muss. Das trifft jedoch nur auf Methoden zu, die in der Elternklasse mit dem Schlüsselwort virtual definiert wurden. Welche Sinn und Zweck derartige virtuelle Methoden haben, wird Ihnen im Folgenden gezeigt.

using System; 
using System.Diagnostics; 

class Favorite 
{ 
  protected string name; 
  protected Uri url; 

  public Favorite(string name, Uri url) 
  { 
    this.name = name; 
    this.url = url; 
  } 

  public virtual void Open() 
  { 
    Process.Start(url.ToString()); 
  } 
} 

class Favorite2 : Favorite 
{ 
  DateTime dateTime; 
  int i; 

  public Favorite2(string name, Uri url) 
    : base(name, url) 
  { 
    dateTime = DateTime.Now; 
    i = 0; 
  } 

  public override void Open() 
  { 
    ++i; 
    Console.Out.WriteLine(i); 
    Process.Start(url.ToString()); 
  } 
} 

class Program 
{ 
  static void Open(Favorite fav) 
  { 
    fav.Open(); 
  } 

  static void Main(string[] args) 
  { 
    var favHighscore = new Favorite2("Highscore - Programmieren lernen", new Uri("http://www.highscore.de/")); 
    Open(favHighscore); 
  } 
} 

Die Klasse Favorite2 überschreibt nun die Methode Open() der Elternklasse Favorite. Da jeder Aufruf des Favoriten mitgezählt werden soll, wird in dieser Methode ein Feld i vom Typ int mit dem Inkrement-Operator ++ jeweils um 1 erhöht. Da Variablen vom Typ int automatisch mit 0 initialisiert werden, muss i nicht explizit auf einen Startwert gesetzt werden. Zur Kontrolle wird außerdem der Wert von i bei jedem Aufruf von Open() auf die Standardausgabe ausgegeben.

Damit Favorite2 die Methode Open() überschreiben kann, muss die Methode in der Elternklasse Favorite mit virtual definiert worden sein. Es handelt sich somit bei Open() um eine virtuelle Methode.

In der Klasse Program wird wie zuvor ein Objekt vom Typ Favorite2 erstellt und an die Methode OpenFavorite() übergeben. In dieser Methode wird für das entsprechende Objekt Open() aufgerufen. Sie erwarten sicherlich, dass die in der Klasse Favorite2 definierte Methode Open() aufgerufen wird - schließlich ist diese Methode extra überschrieben worden. In der Tat wird genau diese Methode aufgerufen und das Feld i dabei von 0 auf 1 erhöht. Wenn Sie das Programm ausführen, wird 1 ausgegeben.

Dass tatsächlich die in Favorite2 definierte Methode Open() aufgerufen wird, liegt daran, dass diese Methode virtuell ist. Denn der Parameter von OpenFavorite() hat eigentlich den Datentyp Favorite. Weil Open() jedoch virtuell ist, wird nicht stupide die Methode der Klasse aufgerufen, die als Datentyp des Parameters angegeben ist, sondern die .NET-Plattform entscheidet zur Laufzeit, welche Methode genau ausgeführt werden muss. Dies hängt vom Datentyp des Objekts ab, das als Parameter übergeben wird - völlig unabängig von dem Datentyp, der vor dem Parameter steht. So wird im obigen Beispiel ein Objekt vom Typ Favorite2 übergeben, obwohl der Parameter von OpenFavorite() den Datentyp Favorite hat.

Es ist auch möglich, die Methode Open() zu überschreiben, wenn diese nicht virtuell ist. Das macht einen großen Unterschied, wie Ihnen folgendes Beispielprogramm zeigt.

using System; 
using System.Diagnostics; 

class Favorite 
{ 
  protected string name; 
  protected Uri url; 

  public Favorite(string name, Uri url) 
  { 
    this.name = name; 
    this.url = url; 
  } 

  public void Open() 
  { 
    Process.Start(url.ToString()); 
  } 
} 

class Favorite2 : Favorite 
{ 
  DateTime dateTime; 
  int i; 

  public Favorite2(string name, Uri url) 
    : base(name, url) 
  { 
    dateTime = DateTime.Now; 
    i = 0; 
  } 

  public new void Open() 
  { 
    ++i; 
    Console.Out.WriteLine(i); 
    Process.Start(url.ToString()); 
  } 
} 

class Program 
{ 
  static void Open(Favorite fav) 
  { 
    fav.Open(); 
  } 

  static void Main(string[] args) 
  { 
    var favHighscore = new Favorite2("Highscore - Programmieren lernen", new Uri("http://www.highscore.de/")); 
    Open(favHighscore); 
  } 
} 

Open() ist nun in der Klasse Favorite als gewöhnliche nicht-virtuelle Methode definiert. Um diese Methode in der Kindklasse Favorite2 neu zu definieren, muss nun das Schlüsselwort new anstelle von override verwendet werden. Während override nur für virtuelle Methoden verwendet werden darf, um diese zu überschreiben, wird new verwendet, um eine Methode in einer Kindklasse neu zu definieren.

Weil die Methode Open() nicht virtuell ist, wird nicht mehr zur Laufzeit überprüft, welches Open() in OpenFavorite() eigentlich aufgerufen werden soll. Da der Parameter von OpenFavorite() den Datentyp Favorite hat, wird immer die Methode Open() aufgerufen, die in Favorite definiert ist. In diesem Fall würde also nie die in Favorite2 überschriebene Methode aufgerufen werden. Wenn Sie das Programm ausführen, wird nichts auf die Standardausgabe ausgegeben.

Wenn Sie Klassen erstellen und davon ausgehen, dass Sie oder andere Entwickler später Kindklassen ableiten werden, müssen Sie entscheiden, welche Methoden virtuell sein sollen und welche nicht. Für die Methoden, die Sie als virtuell kennzeichnen und die eventuell in Kindklassen überschrieben werden, ist sichergestellt, dass zur Laufzeit jeweils genau die richtige Methode ausgeführt wird - unabhängig davon, welchen Datentyp eine Referenzvariable hat. Bei virtuellen Methoden kommt es allein auf den Datentyp des entsprechenden Objekts an.

So schön dieser Automatismus ist: Er kostet Performance. Während bei nicht-virtuellen Methoden bereits zur Kompilierung klar ist, welche Methode ausgeführt werden muss, muss bei virtuellen Methoden zur Laufzeit überprüft werden, welchen Datentyp ein Objekt genau hat. Abhängig davon muss dann die richtige Methode aufgerufen werden. Weil bei jedem Aufruf einer virtuellen Methode der Datentyp des Objekts überprüft werden muss, ist der Aufruf virtueller Methoden langsamer als der nicht-virtueller Methoden. Deswegen sollten Sie als Entwickler nicht jede Methode virtuell machen, sondern nur die, von denen Sie ausgehen, dass Kindklassen sie eventuell überschreiben möchten.

Statische Methoden sind übrigens nie virtuell. Sie können auch nicht mit virtual definiert werden. Wenn sie in Kindklassen überschrieben werden, muss das mit new geschehen.


4.6 Interfaces

Einfordern bestimmter Verhaltensweisen von Objekten

Die Vererbung ist ein Instrument, mit dem Sie in C# immer in Berührung kommen. Schließlich sind alle Klassen in die große Klassenhierarchie des .NET-Frameworks eingeordnet. Selbst dann, wenn Sie für eine Klasse keine Elternklasse angeben, ist Ihre Klasse automatisch von Object abgeleitet.

Die Vererbung macht Objekte polymorph: Objekte haben nicht mehr nur einen einzigen Datentyp, sondern können überall da verwendet werden, wo Objekte vom Typ ihrer Elternklasse erwartet werden. Das ist von Vorteil, wenn Sie wie im Beispiel im vorherigen Abschnitt eine Methode aufrufen wollen, deren Parameter nicht den Datentyp besitzt, den Ihr Objekt hat. Basiert Ihr Objekt jedoch auf einer Klasse, in deren Vererbungslinie der Datentyp des Parameters auftaucht, können Sie das Objekt problemlos an die Methode übergeben.

Jede Methode im .NET-Framework erwartet als Parameter Objekte ganz bestimmter Datentypen. Sie könnten nun jeweils durch die Vererbung Ihre Klassen von genau den Klassen ableiten, die als Datentyp für Parameter von den verschiedenen Methoden verwendet werden, die Sie gerne aufrufen würden. C# macht Ihnen da jedoch einen Strich durch die Rechnung: In C# kann jede Klasse nur eine einzige Elternklasse haben.

C# unterstützt im Gegensatz zu anderen Programmiersprachen wie C++ lediglich die Einfachvererbung. Das Gegenteil von Einfachvererbung heißt Mehrfachvererbung. Diese ermöglicht, eine Klasse von mehreren Klassen abzuleiten. Es handelt sich dabei um ein Instrument, das so tatsächlich in anderen Programmiersprachen wie C++ existiert. Weil es jedoch zu einigen Komplikationen führen kann, hat Microsoft sich entschlossen, die Mehrfachvererbung in C# nicht zu unterstützen, um die Programmiersprache nicht unnötig zu verkomplizieren.

Stattdessen kennt C# ein anderes Instrument, das gewissermaßen die Mehrfachvererbung ersetzt: Interfaces sehen ähnlich aus wie Klassen, besitzen jedoch weder Felder noch Methodendefinitionen. Interfaces bestehen ausschließlich aus Methodenköpfen - also Methoden ohne geschweifte Klammern. Da Interfaces keine Methoden definieren und ihnen somit etwas fehlt, können sie nicht instantiiert werden. Es ist nicht möglich, mit new ein Objekt vom Typ eines Interfaces zu erstellen.

Interfaces machen nur Sinn im Zusammenhang mit Vererbung. Sie müssen von einer Klasse implementiert werden, bevor sie sinnvoll eingesetzt werden können. Dabei muss die Klasse all die Methoden vollständig definieren, die im Interface vorgegeben sind. Wie das im Detail aussieht, erfahren Sie im Folgenden.

using System; 
using System.Diagnostics; 

class Favorite 
{ 
  protected string name; 
  protected Uri url; 

  public Favorite(string name, Uri url) 
  { 
    this.name = name; 
    this.url = url; 
  } 

  public string Name 
  { 
    get { return name; } 
  } 

  public virtual void Open() 
  { 
    Process.Start(url.ToString()); 
  } 
} 

class Favorite2 : Favorite, IComparable 
{ 
  DateTime dateTime; 
  int i; 

  public Favorite2(string name, Uri url, int i) 
    : base(name, url) 
  { 
    this.i = i; 
    dateTime = DateTime.Now; 
  } 

  public override void Open() 
  { 
    ++i; 
    Process.Start(url.ToString()); 
  } 

  public int CompareTo(object obj) 
  { 
    Favorite2 fav = (Favorite2)obj; 
    if (fav.i > i) 
      return 1; 
    else if (fav.i < i) 
      return -1; 
    else 
      return 0; 
  } 
} 

class Program 
{ 
  static void Main(string[] args) 
  { 
    Favorite2[] favs = 
    { 
      new Favorite2("Highscore - Programmieren lernen", new Uri("http://www.highscore.de/"), 10), 
      new Favorite2("Microsoft Deutschland GmbH", new Uri("http://www.microsoft.de/"), 5), 
      new Favorite2("Microsoft .NET Framework ", new Uri("http://www.microsoft.de/NET/"), 20) 
    }; 

    Array.Sort(favs); 
    Console.Out.WriteLine(favs[0].Name); 
  } 
} 

Die Klasse Array bietet eine statische Methode Sort() an, der ein beliebiges Array übergeben werden kann, um Elemente zu sortieren. Damit Sort() aber weiß, wie die Elemente im Detail sortiert werden sollen, benötigt diese Methode Unterstützung: Sort() erwartet, dass alle Elemente in einem zu sortierenden Array vom Typ IComparable sind.

Im obigen Beispielprogramm soll ein Array bestehend aus drei Elementen vom Typ Favorite2 sortiert werden. Das Array soll dabei derart sortiert werden, dass der Favorit, der am häufigsten aufgerufen wurde, nach der Sortierung auf der ersten Position steht. Der Einfachheit halber wurde der Konstruktor der Klasse Favorite2 erweitert, um das entsprechende Feld i unterschiedlich zu initialisieren. Zur Kontrolle wird außerdem der Name des ersten Favoriten im Array nach der Sortierung ausgegeben.

Das Problem bei Favorite2 ist, dass diese Klasse bereits eine Elternklasse hat - nämlich Favorite. Sie kann daher nicht nochmal von einer Elternklasse abgeleitet werden. Schließlich unterstützt C# keine Mehrfachvererbung. Glücklicherweise ist jedoch IComparable keine Klasse, sondern ein Interface. Sie erkennen das unter anderem am Anfangsbuchstaben I: Interfacenamen wird üblicherweise ein I vorangestellt.

Interfacedefinitionen sehen Klassen ähnlich. Sie werden aber nicht mit dem Schlüsselwort class erstellt, sondern mit interface. Die genaue Definition von IComparable sieht wie folgt aus:

interface IComparable 
{ 
  int CompareTo(Object obj); 
} 

IComparable enthält eine einzige Methode CompareTo(), die einen Parameter erwartet und ein Ergebnis vom Typ int zurückgibt. Wie Sie sehen ist diese Methode jedoch nicht vollständig definiert - es ist nur der Methodenkopf angegeben. Klassen, die dieses Interface implementieren - man spricht hier nicht von Erben, sondern Implementieren - werden gezwungen, die Methode CompareTo() vollständig zu definieren. Indem sie die Methode CompareTo() vollständig definieren, unterstützen sie den Datentyp IComparable und können überall dort verwendet werden, wo IComparable erwartet wird - zum Beispiel in der Methode Sort() der Klasse Array.

Die Methode CompareTo() muss so implementiert werden, dass eine positive oder negative Zahl zurückgegeben wird, wenn die zu vergleichenden Objekte größer oder kleiner sind - was immer das für die jeweilige Klasse genau bedeutet. Sind die zu vergleichenden Objekte gleich, muss 0 zurückgegeben werden. Im obigen Beispiel wird das Feld i verglichen. Je nachdem, welches i größer ist, wird 1 oder -1 zurückgegeben.

Wenn das Array favs, das in Main() erstellt wird, an Sort() übergeben wird, erwartet diese Methode, dass alle Elemente im Array vom Typ IComparable sind. Ist das der Fall, steht für alle Elemente im Array eine Methode CompareTo() zur Verfügung, die von Sort() zur Sortierung des Arrays verwendet werden kann. Sort() ruft dabei CompareTo() für alle Elemente im Array auf und übergibt andere Elemente im Array als Parameter, um anhand der Rückgabewerte die richtige Sortierung vorzunehmen.

Methoden in Interfaces sind immer öffentlich und virtuell. Sie können und dürfen weder public noch virtual voranstellen. Wenn eine Klasse Methoden definiert, die in einem Interface vorgegeben werden, muss außerdem nicht override angeben werden. Schließlich ist es Sinn und Zweck von Interfaces, Klassen zur Definition der entsprechenden Methoden zu zwingen.

Schnittstellen werden immer dann verwendet, wenn von Objekten ein bestimmtes Verhalten erwartet wird. Im obigen Beispiel erwartet Sort(), dass die Elemente im Array sortiert werden können. Das können sie, wenn die Elemente vom Typ IComparable sind, weil sie dann die Methode CompareTo() anbieten und miteinander verglichen werden können. Das Interface IComparable zwingt ein Objekt, eine bestimmte Fähigkeit zu unterstützen - mehr nicht.

Das Implementieren von Interfaces hat geringere Auswirkungen auf eine Klasse als das Erben. Denn beim Erben werden nicht nur vollständige Methodendefinitionen mit den entsprechenden Zugriffsattributen übernommen, sondern auch sämtliche Felder, die in der Elternklasse definiert sind. Die Vererbung ist durchaus ein sinnvolles Instrument. Da eine Klasse jedoch durch die Vererbung viel stärker verändert wird als durch Interfaces, werden Interfaces dann eingesetzt, wenn tatsächlich nur ein bestimmtes Verhalten von einer Klasse erwartet wird und keine Details wie Methodendefinitionen oder Felder vererbt werden sollen.

Sie können das auch von einer anderen Warte aus betrachten: Wenn Sie Methoden definieren, deren Parameter vom Typ eines Interfaces sind, ist es einfacher, beliebige Objekte als Parameter zu übergeben. Denn völlig egal, auf welchen Klassen die entsprechenden Objekte basieren, die als Parameter übergeben werden sollen - die entsprechenden Interfaces können immer auf alle Fälle implementiert werden.


4.7 Abstrakte Klassen

Mischgebilde zwischen herkömlichen Klassen und Interfaces

Abstrakte Klassen sind herkömliche Klassen mit allen Merkmalen, die in herkömlichen Klassen verwendet werden dürfen, die jedoch zusätzlich Methoden ohne Rumpf besitzen können - also Methodenköpfe, wie sie in Interfaces vorgegeben werden. Sie werden mit dem Schlüsselwort class definiert, dem das Schlüsselwort abstract vorangestellt wird. So wie Interfaces können abstrakte Klassen nicht instantiiert werden.

Wie alle anderen Konstrukte, die die Objektorientierung anbietet und die Sie im Laufe dieses Buchs kennengelernt haben, sind auch abstrakte Klassen einfach nur ein Instrument, um Modelle zu erstellen, die die Wirklichkeit besser abbilden sollen. Während herkömliche Klassen verwendet werden, um herkömliche Objekte zu beschreiben, werden Interfaces verwendet, um ausschließlich das Verhalten von Objekten zu beschreiben. Abstrakte Klassen hingegen sind ein Mischgebilde. Sie verwenden sie beispielsweise, wenn Sie ein Objekt beschreiben möchten und ein Teil der Beschreibung, die das Verhalten des Objekts betrifft, nicht implementieren können oder wollen.

using System; 
using System.Diagnostics; 

abstract class Favorite 
{ 
  protected string name; 
  protected Uri url; 

  public Favorite(string name, Uri url) 
  { 
    this.name = name; 
    this.url = url; 
  } 

  public abstract void Open(); 
} 

class Favorite2 : Favorite 
{ 
  DateTime dateTime; 
  int i; 

  public Favorite2(string name, Uri url) 
    : base(name, url) 
  { 
    dateTime = DateTime.Now; 
  } 

  public override void Open() 
  { 
    ++i; 
    Process.Start(url.ToString()); 
  } 
} 

class Program 
{ 
  static void Main(string[] args) 
  { 
    var favHighscore = new Favorite2("Highscore - Programmieren lernen", new Uri("http://www.highscore.de/")); 
    favHighscore.Open(); 
  } 
} 

Im obigen Beispielprogramm wird davon ausgegangen, dass ein Favorit aus Name und Adresse besteht und geöffnet werden kann. Während in der entsprechenden Klasse Favorite wie zuvor zwei Felder name und url definiert werden, soll die Methode Open() diesmal nicht implementiert werden. Es ist also klar, dass ein Favorit geöffnet werden können soll. Es ist aber nicht klar, wie dies im Detail geschieht. Ein Grund für dieses Design könnte zum Beispiel sein, dass es auf verschiedene Weise möglich ist, einen Favoriten zu öffnen, und bewusst keine Standardvorgehensweise definiert werden soll. So soll eine Klasse, die von Favorite abgeleitet wird, selbst entscheiden, ob zum Beispiel der Standardbrowser verwendet werden soll oder der Browser eines bestimmten Herstellers.

Damit sich der Compiler nicht wundert, dass der Methode Open() der Rumpf fehlt, muss ihr das Schlüsselwort abstract vorangestellt werden. Dadurch wird die Methode außerdem automatisch virtuell. Das Schlüsselwort virtual muss und darf nicht verwendet werden.

Besitzt eine Klasse mindestens eine abstrakte Methode, muss die Klasse selbst ebenfalls abstrakt sein. Das heißt, das Schlüsselwort abstract muss auch vor class gesetzt werden.

Indem die Klasse Favorite2 von Favorite abgeleitet wird, wird sie gezwungen, die Methode Open() zu implementieren. In dieser Hinsicht unterscheidet sich eine abstrakte Klasse nicht von einem Interface. Der einzige Unterschied ist, dass bei abstrakten Klassen im Gegensatz zu Interfaces override verwendet werden muss.


4.8 Zusammenfassung

Vererbung als weiteres wichtiges Werkzeug der Objektorientierung

In diesem Kapitel haben Sie die Vererbung kennengelernt, die es ermöglicht, Klassen zueinander in Beziehung zu setzen. Sie haben polymorphe Objekte kennengelernt, die mehrere Datentypen unterstützen, was eine Wiederverwendung von Code ermöglicht, bei dem alter Code auf neuen Code zugreift. Da C# lediglich die Einfachvererbung unterstützt, spielen Interfaces eine wichtige Rolle, die zwar keine Felder und vollständigen Methodendefinition besitzen können, jedoch ein bestimmtes Verhalten von Objekten definieren können. Abstrakte Klassen, die Sie abschließend kennengelernt haben, sind ein Mischgebilde und zwischen herkömlichen Klassen und Interfaces angesiedelt.


4.9 Aufgaben

Übung macht den Meister

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Überschreiben Sie in der im Abschnitt 4.2, „Vererbung“ entwickelten Klasse Favorite die Methode ToString(), die von der Klasse Object definiert wird. Erstellen Sie ein Objekt vom Typ Favorite und rufen Sie ToString() auf. Geben Sie den Rückgabewert zur Kontrolle auf die Standardausgabe aus.

    Schlagen Sie in der Dokumentation von Object nach, wie die Methode ToString() definiert ist. Da es die Aufgabe dieser Methode ist, ein Objekt als einen String zu beschreiben, würde es sich für die Klasse Favorite anbieten, wenn diese Methode den Namen und die Adresse als String zurückgibt.

  2. Implementieren Sie für die Klasse Favorite aus Ihrer Lösung zur Aufgabe 1 das Interface IClonable, das im Namensraum System definiert ist. Klonen Sie das Objekt, das Sie in Ihrer Lösung zur Aufgabe 1 erstellt haben, und rufen Sie für den Klon ToString() auf. Geben Sie wieder zur Kontrolle den Rückgabewert auf die Standardausgabe aus.

    Wenn Sie sich die Dokumentation von IClonable ansehen, stellen Sie fest, dass Sie nur eine einzige Methode Clone() definieren müssen. Diese muss eine Kopie des Objekts zurückgeben, für das sie aufgerufen wird.

  3. Erstellen Sie ein Interface IFavorite, das aus zwei Eigenschaften Name und URL und einer Methode Open() ohne Parameter und Rückgabewert besteht. Die Klasse Favorite aus Ihrer Lösung zur Aufgabe 2 soll dieses Interface implementieren. Das Programm soll nach dieser Änderung so funktionieren wie zuvor.

    Während Sie für die Methode im Interface IFavorite lediglich den Methodenkopf angeben dürfen, müssen Sie für die Eigenschaften die beiden Accessoren get und set in geschweiften Klammern angeben. Hinter get und set darf jedoch kein weiteres Paar geschweifter Klammern gesetzt werden. Auf diese Weise definieren Sie Eigenschaften, die gelesen und geschrieben werden können müssen, ohne jedoch eine Implementation vorzugeben.

  4. Machen Sie die Klasse Favorite aus Ihrer Lösung zur Aufgabe 3 abstrakt, indem Sie die Definition der Accessoren der Eigenschaften Name und URL entfernen. Leiten Sie eine Klasse Favorite2 von Favorite ab, für die Sie als einzigen Konstruktor den Standardkonstruktor definieren. Verwenden Sie Favorite2 dann, um ein Objekt zu erstellen, und setzen Sie den Namen und die Adresse über einen Zugriff auf die beiden Eigenschaften. Rufen Sie dann ToString() auf und geben Sie den String auf die Standardausgabe aus.

    Sie müssen in der Klasse Favorite die Accessoren der Eigenschaften als abstrakt kennzeichnen. Der C#-Compiler meckert sonst, weil Favorite laut dem Interface IFavorite die Accessoren eigentlich definieren muss. Indem Sie ihnen das Schlüsselwort abstract voranstellen, erkennt der Compiler, dass Sie die Accessoren absichtlich nicht definieren wollen, sondern dies Kindklassen überlassen möchten. Die Felder name und url müssen außerdem mit protected in Favorite definiert werden, damit die Kindklasse Favorite2 auf sie zugreifen kann.

    Beachten Sie, dass Sie für eine fehlerfreie Kompilierung das Interface ICloneable von Favorite entfernen müssen. Da Favorite eine abstrakte Klasse ist, können keine Objekte erstellt werden. Ein Klonen macht daher auch keinen Sinn. Das Interface ICloneable muss daher nun von Favorite2 implementiert werden.

    Beachten Sie außerdem, dass Sie der Klasse Favorite einen Standardkonstruktor hinzufügen müssen. Denn wenn die Klasse Favorite2 über den Standardkonstruktor initialisiert wird, wird automatisch der Standardkonstruktor der Elternklasse aufgerufen. Sie können zwar explizit über base einen Konstruktor der Elternklasse aufrufen. Da der einzige Konstruktor von Favorite jedoch zwei Parameter zum Initialisieren der Felder erwartet, macht ein Aufruf dieses Konstruktors vom Standardkonstruktor in Favorite2 kaum Sinn.