Startseite  Index  <<  >>  

7 - Vererbung

7.1 Grundprinzip

Ein weiteres wichtiges Prinzip bei der objektorientierten Programmierung ist neben der Kapselung die Vererbung. Dabei werden von einer allgemeineren Klasse, der Basisklasse (auch Superklasse oder übergeordnete Klasse), weitere speziellere Klassen (auch Subklassen, Unterklassen) abgeleitet, die meist zusätzliche Eigenschaften und/oder Methoden bereitstellen. Von der Basisklasse erben sie alle geschützten (protected) und öffentlichen (public) Member. Konstruktoren werden nicht vererbt, können aber von der abgeleiteten Klasse aus aufgerufen werden. Private (private) Eigenschaften und Methoden werden ebenfalls nicht vererbt, der Zugriff darauf ist von der abgeleiteten Klasse aus nicht möglich. Allerdings können die protected-Elemente verwendet werden, da sie abgeleiteten Klassen ihre Verwendung erlauben.

Die Menge aller vorliegenden Klassen bildet eine Klassenhierarchie, welche einer Baumstruktur ähnelt. Bei der Konzeption einer Klassenhierarchie sollten die abzubildenden Informationen auf gemeinsame Eigenschaften und Methoden untersucht werden. Diese lassen sich dann in einer oder mehreren Basisklassen zusammenfassen (Generalisierung). In den abgeleiteten Klassen können dann spezielle Funktionalitäten hinzugefügt werden (Spezialisierung). Die Verschachtelungstiefe kann dabei unendlich sein (zumindest theoretisch).

INFO

Einige Programmiersprachen wie C++ erlauben die so genannte Mehrfachvererbung, bei der eine Klasse von mehreren Klassen erben kann. Dies ist in C# nicht möglich. Hier kann eine Klasse nur von maximal einer anderen Klasse abgeleitet werden - von dieser erben. Dadurch entsteht jedoch eine übersichtlichere Vererbungshierarchie – und solche Vereinfachungen sind ja auch die Ziele moderner Programmiersprachen.

Die Vererbung ist nicht nur bei Klassen möglich, auch Interfaces können von anderen Interfaces abgeleitet werden (dazu mehr im folgenden Kapitel).

Vererbung mit Basisklasse und abgeleiteten Klassen

Zur Verwaltung von Personendaten soll eine Klassenhierarchie entworfen werden. Die Klasse Person vereint dazu die Informationen und Methoden, die alle zu beschreibenden Personen gemeinsam haben. Speziellere Klassen wie Mitarbeiter und Student benötigen weitere Eigenschaften und Methoden, die sie den geerbten Elementen der Klasse Person hinzufügen.

Die Klasse Person besitzt die vier Instanzvariablen name, vorname, geburtsdatum und nummer sowie eine Methode ZeigePerson(), welche die Daten zur Person als String zurückgibt. Von der Klasse Person werden zwei Klassen abgeleitet, die Klassen Mitarbeiter und Student. Beide Klassen besitzen zusätzliche Instanzvariablen. Die Klasse Mitarbeiter besitzt noch eine weitere Methode GehaltAendern().

Der Vorteil dieser Vorgehensweise ist, dass allgemeine Änderungen, wie z.B. die Implementierung der Methode ZeigePerson(), nur noch in der Basisklasse durchgeführt werden müssen. Alternativ kann die Methode auch überladen werden, so dass die Ausgabe auf der Konsole und in eine Datei erfolgen kann. Die Methode steht dann sofort in allen abgeleiteten Klassen zur Verfügung.

Klassendiagramm: Person ist die Basisklasse von Mitarbeiter und Student

Abbildung 7.1: Klassendiagramm: Person ist die Basisklasse von Mitarbeiter und Student

7.1.1 Vererbung von Klassen

Syntax

  • Die Deklaration der Klasse erfolgt wie bisher.
  • Danach folgt durch einen Doppelpunkt getrennt der Name der Basisklasse.

    class Klassenname: Basisklasse

  • Eine Klasse erbt alle öffentlichen (public) und geschützten (protected) Teile der Basisklasse.
  • Nicht vererbt werden Konstruktoren, Destruktoren, statische Klassenelemente und die privaten Member.
  • Die Tiefe einer Vererbungshierarchie ist zunächst nicht begrenzt. Wird eine Klasse C von einer Klasse B und diese von einer Klasse A abgeleitet, erbt C alles von den Klassen A und B.

    INFO

    Im Gegensatz zu anderen Programmiersprachen können Sie vor der Basisklasse keinen Zugriffsmodifizierer verwenden. Möchten Sie in der abgeleiteten Klasse Methoden der Basisklasse verdecken bzw. protected-Methoden zugänglich machen, müssen Sie die Methoden mit private verdecken oder neue Methoden bereitstellen.

    Implementierung der Klassen Person, Mitarbeiter und Student

    Der Quellcode zur obigen Abbildung könnte, wie im Folgenden dargestellt, aufgebaut sein. Es werden dazu drei Klassen deklariert. Die Klasse Person besitzt vier Datenelemente, die in vier Instanzvariablen gespeichert werden. Diese werden als private deklariert. Zugriffsmethoden auf diese Variablen wurden auch aus Gründen der Übersichtlichkeit nicht vorgesehen. Die Klasse Person hat in diesem Beispiel zwei Konstruktoren: einen parameterlosen, in dem die Variablen mit Initialwerten belegt werden (durch Aufruf des Konstruktorinitialisierers), und einen, dem Werte für alle vier Instanzvariablen zu übergeben sind. Des Weiteren verfügt sie über die Methode ZeigePerson(), welche alle Eigenschaftswerte in Form eines Strings zurückliefert (vergleichbar mit der Methode ToString()). Damit Sie später einen Überblick über die Aufrufreihenfolge der Konstruktoren erhalten, wird in jedem Konstruktor noch eine Methode untergebracht, die den verwendeten Konstruktor ausgibt.

    public class Person
    {
      private string name;
      private string vorname;
      private string gebdat;
      private int nummer;
      public Person(): this("", "", "", 0)
      {
        Console.WriteLine("1. Konstruktor von Person");
      }
      public Person(string name, string vorname, string gebdat, int nummer)
      {
        this.name    = name;
        this.vorname = vorname;
        this.gebdat  = gebdat;
        this.nummer  = nummer;
        Console.WriteLine("2. Konstruktor von Person");
      }
      public string ZeigePerson()
      {
        if(name == "")
          return("Keine Angaben vorhanden");
        else
          return(nummer + ": " + name + ", " + vorname + "; geboren am: " + gebdat);
      }
    } 
    Listing 7.1: Mitarbeiter.cs

    Von der Klasse Person sind die Klassen Mitarbeiter und Student abgeleitet. Die Klasse Mitarbeiter besitzt die zwei zusätzlichen Datenelemente titel und gehalt. Alle anderen Datenelemente erbt sie von der Klasse Person. Die Objekte dieser Klasse sollen ebenfalls über einen von zwei Konstruktoren erzeugt werden. Da Konstruktoren nicht vererbt werden, sind sie hier erneut anzugeben. Außerdem müssen die neu hinzugekommenen Instanzvariablen initialisiert werden. Die Klasse wurde außerdem um die Methode GehaltAendern() erweitert, welche das aktuelle Gehalt um den übergebenen Wert erhöht.

    public class Mitarbeiter: Person
    {
      private string titel;
      private double gehalt;
      public Mitarbeiter(): base()
      {
        titel = "";
        gehalt = 0;
        Console.WriteLine("1. Konstruktor von Mitarbeiter");
      }
      public Mitarbeiter(string titel, string name, string vorname, string gebdat, int nummer, 
                                double gehalt) : base(name, vorname, gebdat, nummer)
      {
        this.titel = titel;
        this.gehalt = gehalt;
        Console.WriteLine("2. Konstruktor von Mitarbeiter");
      }
      public double GehaltAendern(double gehaltsErhoehung)
      {
        gehalt += gehaltsErhoehung;
        return (gehalt);
      }
    }
    Listing 7.2: Mitarbeiter.cs

    7.1.2 Konstruktorverkettung

    Eine Besonderheit betrifft die Verwendung des Schlüsselworts base, welches später noch genauer erläutert wird. Damit rufen Sie den Konstruktor der Basisklasse auf. Im Falle des parameterlosen Konstruktors der Klasse Mitarbeiter bedeutet dies, dass zuerst der parameterlose Konstruktor der Klasse Person aufgerufen wird, der seinerseits den Konstruktor mit den vier Standardwerten aufruft. Dieser wird dann auch als Erstes ausgeführt. Es folgt der parameterlose Konstruktor der Klasse Person und zuletzt der Konstruktor der Klasse Mitarbeiter. Auf diese Weise wird eine korrekte Initialisierung sichergestellt.

    Abarbeitungsreihenfolge der Konstruktoren

    Abbildung 7.2: Abarbeitungsreihenfolge der Konstruktoren

    Wenn Sie base() nicht angeben, wird automatisch der parameterlose Standardkonstruktor der Basisklasse aufgerufen. Sie können base() demnach zum Aufruf des parameterlosen Konstruktors der Klasse Mitarbeiter auch weglassen. Hier ist aber einiges zu beachten!

    Geben Sie base() nicht an, wird also der parameterlose Standardkonstruktor aufgerufen. Dieser existiert aber nur,

  • wenn die Basisklasse gar keinen Konstruktor besitzt (dann wird automatisch ein solcher Konstruktur erzeugt - der dann halt keine Anweisungen enthält) oder
  • wenn Sie selbst einen solchen Konstruktor definieren.

    Besitzt die Basisklasse nur einen parametrisierten Konstruktor, wird nicht automatisch ein parameterloser Standardkonstruktor bereitgestellt. Sie erhalten dann eine Fehlermeldung vom Compiler.

    Beispiel: Fortsetzung

    Die Klasse Student besitzt eine zusätzliche Variable studienrichtung und sie verfügt nur über einen Konstruktor. Damit ist es nicht möglich, ein Student-Objekt zu erzeugen, das nur Standardwerte verwendet. Auch dieser Konstruktor ruft zuerst den Konstruktor der Basisklasse auf, um dessen Instanzvariablen korrekt zu initialisieren.
    public class Student: Person
    {
      private string studienrichtung;
      public Student(string name, string vorname, string gebdat, int nummer, string fach) : base(name, vorname, gebdat, nummer)
      {
        studienrichtung = fach;
        Console.WriteLine("Konstruktor von Student");
      }
    }
    Listing 7.3: Mitarbeiter.cs

    In der Klasse MitarbeiterMain werden in der Methode Main() Objekte der Klassen Mitarbeiter und Student erzeugt. Über die Methode ZeigePerson() der Basisklasse Person werden die Inhalte der Instanzvariablen der Objekte angezeigt, allerdings nur die Werte, die sich in der Basisklasse Person befinden (dafür muss später noch Abhilfe geschaffen werden). Für den Mitarbeiter wird zusätzlich die Methode GehaltAendern(300.00) aufgerufen. Sie können hier erkennen, dass es keinen Unterschied zwischen dem Aufruf einer Methode der Basisklasse und dem einer Methode der verwendeten Klasse gibt.

    public class MitarbeiterMain
    {
      public static void Main(string[] args)
      {
        Mitarbeiter ma = new Mitarbeiter();
        Console.WriteLine(ma.ZeigePerson());
        ma = new Mitarbeiter("Dr.", "Müller", "Heiko", "10.10.76", 1, 5000.00);
        Console.WriteLine(ma.ZeigePerson());
        Console.WriteLine("Neues Gehalt: " + ma.GehaltAendern(300.00));
        Student stud = new Student("Bauer", "Jana", "20.01.85", 456, "Informatik");
        Console.WriteLine(stud.ZeigePerson());
        Console.ReadLine();
      }
    }
    Listing 7.4: Mitarbeiter.cs

    Auch wenn Konstruktoren nicht vererbt werden können, sind sie an der Erzeugung eines Objekts einer abgeleiteten Klasse immer beteiligt. Der Konstruktor der Basisklasse wird immer vor dem Konstruktor der abgeleiteten Klasse abgearbeitet. Hier ist wiederum zu beachten, dass ohne Angabe von base() immer nach einem parameterlosen Standardkonstruktor gesucht wird. Dies können Sie selbst ganz leicht nachprüfen, indem Sie die Verwendung von base() im gezeigten Beispiel auskommentieren und die Ausgabeanweisungen verfolgen.

    7.2 Zugriff auf die Basisklasse und das eigene Objekt

    7.2.1 this

    Es wurde bisher schon häufiger vom Schlüsselwort this Gebrauch gemacht, welches eine Referenz auf das aktuelle Objekt darstellt. Mit anderen Worten, this verweist auf das Objekt, für das die Methode bzw. hier der Konstruktor aufgerufen wurde. this kann in jeder nicht-statischen Methode verwendet werden. In statischen Methoden ist this nicht verfügbar (und die Verwendung wird auch vom Compiler bemängelt), da statische Methoden nicht auf einem bestimmten Objekt arbeiten und auch kein Objekt benötigen.

    Die this-Referenz wird beispielsweise verwendet:

  • wenn in einer Methode die Variablennamen durch Parameternamen verdeckt werden. Dies ist z.B. der Fall, wenn Variablen verwendet werden, die denselben Bezeichner wie die Parameter tragen.
  • Weitere Einsatzfälle sind die Übergabe des eigenen Objekts als Parameter an eine Methode, die einen Parameter vom Typ des Objekts (oder einen Typ der Basisklassen) verlangt.
  • Bei der Deklaration eines Indexers (vgl. Kapitel zu Eigenschaften) wird das Schlüsselwort this zwingend benötigt.

    7.2.2 base

    Mit dem Schlüsselwort base kann von der abgeleiteten Klasse aus die Basisklasse angesprochen werden. Es wird eingesetzt, um einen bestimmten Konstruktor der Basisklasse aufzurufen. Beim Überschreiben von Methoden kann darüber die Methode der Basisklasse aufgerufen werden (siehe Beispiel zu "Methoden verdecken")

    Syntax

  • Geben Sie hinter der Parameterliste des Konstruktors einen Doppelpunkt und das Wort base sowie die Parameterliste des aufzurufenden Konstruktors an. Damit wird der durch die Parameterliste definierte Konstruktor der Basisklasse aufgerufen.
  • Rufen Sie innerhalb der überschreibenden Methode die Methode der Basisklasse über base.Methodenname() auf.
  • Die mehrmalige Verwendung von base wie in base.base.Methode() ist nicht zulässig.
  • Das Schlüsselwort base darf nicht in statischen Methoden verwendet werden.

    7.3 Zuweisungskompatibilität

    Die objektorientierte Programmierung erlaubt es, ein Objekt einer abgeleiteten Klasse in den Typ einer Basisklasse zu konvertieren, was auch als "Casten" bezeichnet wird. Der Grund liegt in der Zuweisungskompatibilität dieser Typen. Da ein abgeleitetes Objekt höchstens mehr Member (also mehr Funktionalität bzw. zusätzliche Datenelemente) besitzt als seine Vorgänger in der Klassenhierarchie, können problemlos seine Methoden, Instanzvariablen etc. verwendet werden.

    Beispielsweise kann eine Objektvariable vom Typ Person erzeugt werden, um ein Student-Objekt zu referenzieren. Das "Casten" kann über mehrere Ebenen der Vererbungshierarchie erfolgen. Wichtig ist dabei, dass der Typ, in den das Objekt konvertiert werden soll, in der Vererbungshierarchie oberhalb des Objekttyps steht. Oder anders gesagt: Die Klasse des Objekts muss ein Nachkomme der Klasse sein, in die gecastet werden soll. Das Casten erfolgt implizit, wenn ein Objekt einer abgeleiteten Klasse einer Variablen vom Typ einer Basisklasse zugewiesen wird. Durch Voranstellen des in Klammern eingeschlossenen Typs kann auch explizit gecastet werden.

    Mitarbeiter ma = new Mitarbeiter();
    Person p = (Person)ma;
    //oder
    Person p = ma;

    Beachten Sie aber, dass eine Zuweisung in umgekehrter Richtung nicht zulässig ist, wenn der gecastete Typ nicht ein Basistyp des anderen ist. Die folgende Zuweisung ist nicht möglich und führt zu einer InvalidCastException, da der Typ Person kein von Mitarbeiter abgeleiteter Typ ist.
    Person p = new Person();
    Mitarbeiter m = (Mitarbeiter)p;

    Diese expliziten Typkonvertierungen werden beispielsweise bei der Parameterübergabe oder bei der Speicherung von Objekten in Arrays genutzt, was eine effektive Programmierung ermöglicht. Auch nach dem Casten ist es möglich, den originalen Typ eines Objekts zu bestimmen. Diesen liefert der Aufruf von GetType().ToString() eines Objekts zurück.

    7.3.1 Typkompatibilität prüfen

    Weiterhin lässt sich überprüfen, ob ein Objekt zu einem bestimmten Typ kompatibel ist. Dazu verwenden Sie den is-Operator, z.B.
    // ist p zuweisungskompatibel zur Klasse Person ?
    if(p is Person)

    Syntax

  • Mithilfe des is-Operators kann getestet werden, ob ein Objekt in einen bestimmten Typ konvertiert werden kann.
  • Der Ausdruck Objekt is Objekttyp gibt einen logischen Wert (true oder false) zurück, der in Bedingungen, z.B. in if-Anweisungen oder Schleifen, verwendet werden kann.

    7.3.2 Sichere Typkonvertierung

    Ein Objekt, das in ein Basisklassen-Objekt konvertiert wurde, kann nur noch die Member dieser Basisklasse ansprechen. Wurde beispielsweise ein Mitarbeiter-Objekt in ein Person-Objekt gecastet, ist der Aufruf der Methode GehaltAendern() nicht mehr möglich. Aber auch hierfür gibt es einen Ausweg. Zunächst muss sicher sein, dass eine Objektvariable ein Objekt einer bestimmten Klasse referenziert. Dies prüfen Sie mit dem is-Operator. Später lässt sich die Methode der Klasse aufrufen, indem das Objekt in den entsprechenden Typ gecastet wird. Dies erfolgt entweder mit dem as-Operator oder durch das Voranstellen des in Klammern gesetzten Objekttyps.

    Syntax

  • Mithilfe des as-Operators kann ein Objekt in einen bestimmten Typ konvertiert bzw. als dieser Typ angesprochen werden. Ist eine Konvertierung nicht möglich, liefert dieser Ausdruck null zurück. Im Unterschied dazu kommt es beim Casten (bei dem der Objekttyp in Klammern vorangestellt wird) zu einer Exception.
    Objekt as Objekttyp

  • Wird dieser Ausdruck in Klammern geschrieben, kann anschließend sofort ein Methodenaufruf erfolgen. Der Klammerausdruck sollte nur nicht null liefern.
    (Objekt as Objekttyp).Methode(); 

    Verwenden der Zuweisungskompatibilität von Objekttypen

    Zum Test besitzt die Klasse Person lediglich eine Methode Ausgabe(), die einen Namen ausgibt. Die Klasse Mitarbeiter, die von Person abgeleitet ist, enthält ebenfalls nur eine Methode AusgabeGehalt(), die einen konstanten Wert als Gehalt liefert. In der Main()-Methode werden nun von beiden Klassen Objekte erzeugt und an die statische Methode AusgabeDaten() übergeben. Da der Parameter vom Typ Person ist, kann in jedem Fall die Methode Ausgabe() aufgerufen werden (was schon einmal ein Vorteil des Einsatzes der Vererbung ist). Um noch speziellere Informationen auszugeben, wird über den is-Operator geprüft, ob es sich bei dem Parameter um den Typ Mitarbeiter handelt. In diesem Fall wird der Parameter über den as-Operator in den Typ Mitarbeiter gecastet und dann dessen Methode AusgabeGehalt() aufgerufen. Zum Abschluss wird noch der vollständige Typname des Parameters ausgegeben.

    using System;
    namespace CSharpBuch.Kap07
    {
      public class Zuweisungskompatibel
      {
        static void Main(string[] args)
        {
          Person p = new Person();
          Mitarbeiter ma = new Mitarbeiter();
          // der Vorteil der Vererbung und Zuweisungskompatibilität -
          // eine Methode kann für verschiedene Typen aufgerufen werden
          AusgabeDaten(p);
          AusgabeDaten(ma);
        }
        static void AusgabeDaten(Person p)
        {
          p.Ausgabe();
          if(p is Mitarbeiter)
            (p as Mitarbeiter).AusgabeGehalt();
          Console.WriteLine(p.GetType().ToString());
        }
      }
      public class Person
      {
        public void Ausgabe()
        {
          Console.WriteLine("Musterperson");
        }
      }
      public class Mitarbeiter: Person
      {
        public void AusgabeGehalt()
        {
          Console.WriteLine("1000 Euro");
        }
      }
    }
    Listing 7.5: Zuweisungskompatibel.cs

    Ausgabe:
    Musterperson
    CSharpBuch.Kap07.Person
    Musterperson
    1000 Euro
    CSharpBuch.Kap07.Mitarbeiter

  • 7.4 Methoden verdecken

    Für ein Objekt können sowohl Methoden der eigenen Klasse aufgerufen werden als auch Methoden der Basisklasse, sofern es sich um eine abgeleitete Klasse handelt. Häufig kommt es vor, dass eine Methode in einer abgeleiten Klasse anders funktionieren soll als in der Basisklasse. In diesem Fall kann die Methode in der abgeleiteten Klasse verdeckt (auch ersetzt) werden, indem Sie eine gleichnamige Methode mit der gleichen Parameterliste erstellen. Die Methode der Basisklasse ist dann für Objekte dieser abgeleiteten Klasse verdeckt.

    Beispiel: Methoden von Klassen in abgeleiteten Klassen anders implementieren

    Bisher haben Sie base nur in Zusammenhang mit dem Aufrufen eines Konstruktors der Basisklasse verwendet. Innerhalb einer Methode können Sie damit die gleichnamige Methode in der Basisklasse aufrufen. In der Methode Main() werden wieder zwei Objekte vom Typ Person und Mitarbeiter erzeugt. In der Klasse Mitarbeiter wurde die Methode Ausgabe() der Basisklasse verdeckt. Jetzt kann für beide die Methode Ausgabe() aufgerufen werden, die für jedes Objekt ein anderes Ergebnis liefert.

    using System;
    namespace CSharpBuch.Kap07
    {
      public class MethodenVerdecken
      {
        static void Main(string[] args)
        {
          Person p = new Person();
          Mitarbeiter ma = new Mitarbeiter();
          p.Ausgabe();
          ma.Ausgabe();      
        }
      }
      public class Person
      {
        public void Ausgabe()
        {
          Console.WriteLine("Musterperson");
        }
      }
      public class Mitarbeiter: Person
      {
        public void Ausgabe()
        {
          base.Ausgabe();
          Console.WriteLine("1000 Euro");
        }
      }
    }
    Listing 7.6: MethodenVerdecken.cs

    INFO

    Der Compiler weist Sie beim Übersetzen dieser Anwendung mit einer Warnung darauf hin, dass eine geerbte Methode verdeckt wurde. Es ist ja möglich, dass Sie nur die Schnittstelle der Basisklasse kennen und nicht ihre geschützten Member und dass Sie das Verdecken nicht beabsichtigt haben. Variablen können in einer abgeleiteten Klasse auch verdeckt werden.

    Dies geschieht allerdings meist unabsichtlich, zumal Variablen der Basisklasse in abgeleiteten Klassen nicht sichtbar sein sollten.

    Verwenden Sie in diesem Fall in der verdeckenden Methode das Schlüsselwort new. Es kennzeichnet in diesem Fall die Methode als neue Implementierung und es wird vor dem Rückgabetyp der Methode angegeben. Jetzt gibt der Compiler keine Warnung mehr aus.

    public new void Ausgabe()
    {
      ...
    }

    Was passiert beim Aufruf einer verdeckten Methode für ein Objekt, das in ein Objekt vom Typ der Basisklasse konvertiert wurde? Das soll das nächste Beispiel zeigen.

    Beispiel: Verdeckte Methoden über Basisklassen aufrufen

    Es werden wieder zwei Objekte erzeugt und deren Methode Ausgabe() aufgerufen. Zusätzlich wird dem Person-Objekt ein Mitarbeiter-Objekt zugewiesen und dessen Methode Ausgabe() aufgerufen. Das Ergebnis ist, dass die Methode Ausgabe() des Person-Objekts aufgerufen wird und nicht, wie man hätte erwarten können, die Methode Ausgabe() der Klasse Mitarbeiter. Der Grund liegt darin, dass hier bereits der Compiler festlegt, welche Methode aufgerufen wird. Und da die Methode über ein Person-Objekt aufgerufen wird, ist dies auch die Methode Ausgabe() der Person-Klasse.

    public static void Main(string[] args)
    {
      Person p = new Person();
      Mitarbeiter ma = new Mitarbeiter();
      p.Ausgabe();
      ma.Ausgabe();
      p = ma;
      p.Ausgabe();
    }
    Listing 7.7: MethodenVerdecken.cs (Auszug)

    Diese Funktionalität ist in den meisten Fällen nicht erwünscht und lässt sich beheben, indem Sie den Methoden ein polymorphes Verhalten verleihen, d.h. die Methode verhält sich abhängig vom konkreten Objekttyp anders. So würde die Methode Ausgabe() im Beispiel Zuweisungskompatibel.cs immer die Methode Ausgabe() der Person-Klasse ausführen.

    INFO

    Wenn Sie Methoden verdecken, kann es zu Problemen kommen, wenn dies über mehrere Ebenen der Vererbungshierarchie hinweg erfolgt. Der Compiler beginnt seine Suche nach einer passenden Methode bei der Basisklasse und steigt die Hierarchie dann herab. Findet er eine passende Methode, wird diese verwendet, auch wenn dazu "Casts" notwendig sind. Als Lösung können Sie beim Aufruf der Methode den Typnamen mit angeben, so dass nicht die gesamte Klassenhierarchie durchsucht wird.

    7.5 Polymorphie

    Als Polymorphie (Vielgestaltigkeit) bezeichnet man das unterschiedliche Verhalten von gleichnamigen Methoden für Objekte abgeleiteter Klassen. In der Basisklasse wird dazu eine Methode definiert, die dann in den abgeleiteten Klassen überschrieben wird (beachten Sie: nicht verdeckt oder überladen, sondern überschrieben). Auch wenn ein Objekt der abgeleiteten Klasse in den Typ der Basisklassen konvertiert wurde, wird beim Aufruf einer solchen polymorphen Methode immer die richtige Methode verwendet. Die "richtige" Methode ist die Methode des Typs des ursprünglichen Objekts. Wurde z.B. ein Mitarbeiter-Objekt einer Variablen vom Typ Person zugewiesen, wird zur Laufzeit beim Aufruf einer polymorphen Methode zuerst in der Klasse Mitarbeiter nach der passenden Methode gesucht. Es wird hier von dynamischer Bindung gesprochen. Der Methodenaufruf wird also nicht zur Kompilierzeit, sondern erst zur Laufzeit der Anwendung aufgelöst (was natürlich etwas länger dauert).

    7.5.1 Methoden überschreiben

    Um dieses polymorphe Verhalten zu erreichen, werden die Methoden der Basisklasse, die zum Überschreiben vorgesehen sind, mit dem Schlüsselwort virtual versehen. Beim Überschreiben der Methoden in der abgeleiteten Klasse (auch Überlagern genannt) ist dieser Methode dann das Schlüsselwort override voranzustellen. Damit wird festgelegt, dass beim Aufruf der Methode zur Laufzeit zuerst in der Klasse des betreffenden Objekts nach der entsprechenden Methode gesucht werden soll, unabhängig davon, in welchen Typ das Objekt gecastet wurde. Das gilt auch bei der Vererbung über mehrere Klassen. Die Methode ist auf der obersten Stufe der Klassenhierarchie mit virtual zu definieren. In allen abgeleiteten Klassen kann sie mit override überschrieben werden. Der Zugriffsmodifizierer wird für polymorphe Klassen durch die virtuelle Methode vorgegeben und kann beim Überschreiben nicht mehr geändert werden. Aus einer protected-Methode kann also in einer abgeleiteten Klasse keine public-Methode mehr gemacht werden.

    Beispiel: Aufrufe polymorpher Methoden werden zur Laufzeit aufgelöst

    Das Beispiel verwendet in der Klasse PolymorpheMethoden wieder die Methode AusgabeDaten(), der als Parameter ein >Person-Objekt übergeben wird. Darin kann nun ohne weitere Prüfung des konkreten Typs die Methode Ausgabe() aufgerufen werden, da die Methoden der Basis- und abgeleiteten Klassen mit virtual und override deklariert sind.

    using System;
    namespace CSharpBuch.Kap07
    {
      public class PolymorpheMethoden
      {
        public static void Main(string[] args)
        {
          Person p = new Person();
          Mitarbeiter ma = new Mitarbeiter();
          AusgabeDaten(p);
          AusgabeDaten(ma);
        }
        static void AusgabeDaten(Person p)
        {
          p.Ausgabe();
        }
      }
      public class Person
      {
        public virtual void Ausgabe()
        {
          Console.WriteLine("Musterperson");
        }
      }
      public class Mitarbeiter: Person
      {
        public override void Ausgabe()
        {
          base.Ausgabe();
          Console.WriteLine("1000 Euro");
        }
      }
    }
    Listing 7.8: PolymorpheMethoden.cs

    INFO

    Sie sollten aber nun nicht wahllos alle Methoden der Basisklasse als virtuell deklarieren, sondern nur die, die auch später ein polymorphes Verhalten aufweisen sollen. Die Definition polymorpher Methoden geht auf Kosten der Ausführungsgeschwindigkeit, da die Methoden erst dynamisch zur Laufzeit gebunden werden.

    7.5.2 Methoden versiegeln

    Haben Sie eine Vererbungshierarchie mit polymorphen Methoden aufgebaut, kann es sinnvoll sein, ab einer bestimmten Klasse wieder das polymorphe Verhalten einer Methode auszuschalten.

    Die Angabe von override macht eine Methode gleichzeitig zu einer virtuellen Methode, die in der nächsten abgeleiteten Klasse überschrieben werden kann und ein polymorphes Verhalten besitzt. Mit dem Schlüsselwort sealed wird eine Methode versiegelt. Das heißt, sie ist die letzte polymorphe Methode innerhalb der Vererbungshierarchie, z.B.

    public sealed override string Ausgabe()
    { 
      ... 
    }

    Eine versiegelte Methode kann zwar verdeckt, aber nicht mehr mit override überschrieben werden.

    Beispiel: Versiegelte Methoden können nicht mehr überschrieben werden

    Das Beispiel zeigt einen anderen Anwendungsfall, in dem die Zuweisungskompatibilität von Typen einen Nutzen bringt. In der Main()-Methode wird dazu ein Array (Arrays werden später noch genauer erläutert) deklariert, das drei Person-Objekte aufnehmen kann. Jedem Element wird ein Person-Objekt zugewiesen, welches aber von unterschiedlichen, von Person abgeleiteten Klassen erzeugt wurde. Mittels der foreach-Anweisung wird dann durch das Array iteriert und die Methode AusgabeDaten() für jedes Element aufgerufen.

    Zur besseren Übersicht geben die Methoden noch weitere Informationen aus, z.B. den Typ des Objekts, für das gerade die Methode Ausgabe() aufgerufen wurde.

    Wie Sie als Ergebnis des Aufrufs der Anwendung sehen können, wird die Methode Ausgabe() des Dozent-Objekts nicht aufgerufen, sondern "nur" die Methode der Klasse Mitarbeiter. Der Grund liegt darin, dass beim Durchsuchen der Klassenhierarchie, ausgehend von Person als letzte überschriebene Methode, die Methode Ausgabe() der Klasse Mitarbeiter gefunden wurde.

    using System;
    namespace CSharpBuch.Kap07
    {
      public class MethodenVersiegeln
      {
        public static void Main(string[] args)
        {
          Person[] personen = new Person[3];
          personen[0] = new Person();
          personen[1] = new Mitarbeiter();
          personen[2] = new Dozent();
          foreach(Person p in personen)
            AusgabeDaten(p);
        }
        static void AusgabeDaten(Person p)
        {
          Console.WriteLine("*** " + p.GetType().ToString() + "***");
          p.Ausgabe();
          Console.WriteLine();
        }
      }
      public class Person
      {
        public virtual void Ausgabe()
        {
          Console.WriteLine("Name: Musterperson");
        }
      }
      public class Mitarbeiter: Person
      {
        public sealed override void Ausgabe()
        {
          base.Ausgabe();
          Console.WriteLine("Gehalt: 1000 Euro");
        }
      }
      public class Dozent: Mitarbeiter
      {
        new public void Ausgabe()
        {
          base.Ausgabe();
          Console.WriteLine("IQ: 113");
        }
      }
    } 
    Listing 7.9: MethodenVersiegeln.cs

    7.6 Die Klasse Object

    Wenn Sie beim Deklarieren einer Klasse keine Basisklasse angeben, wird die Klasse automatisch von der Klasse Object abgeleitet. Damit ist die Klasse Object Basisklasse aller Klassen des .NET Frameworks und auch der von Ihnen erzeugten Klassen (direkt oder indirekt). Dies impliziert auch, dass alle Klassen die öffentlichen Methoden und Eigenschaften (die so genannten Member) dieser Klasse erben.

    MethodeBeschreibung
    Equals()Prüft, ob die eigene Objektinstanz mit der übergebenen übereinstimmt.
    GetHashCode()Liefert einen Hashcode für dieses Objekt, der es identifizieren soll (ein Hashcode ist niemals eindeutig). Hashcodes werden z.B. zum Sortieren von Elementen und der Implementierung einer schnellen Suche nach einem Objekt verwendet.
    GetType()Liefert ein Objekt vom Typ Type, über das weitere Informationen über den Datentyp des betreffenden Objekts ermittelt werden können. Über die Eigenschaft Name kann z.B. der Typname, mittels der Eigenschaft Namespace der zugehörige Namespace ermittelt werden.
    ReferenceEquals()Über diese statische Methode prüfen Sie, ob die übergebenen Objektverweise auf dieselbe Instanz zeigen.
    ToString()Liefert eine String-Repräsentation des Objekts, standardmäßig den vollständigen Typnamen.
    Tabelle 7.1: Öffentliche Methoden der Klasse Object

    Diese Methoden können in eigenen Klassen überschrieben werden, um eventuell deren Standardverhalten zu ändern. So kann Equals() z.B. auch dann true liefern, wenn die Werte der Datenelemente beider Objekte übereinstimmen. Insbesondere die String-Repräsentation kann angepasst werden, um z.B. die Werte der Datenelemente einfließen zu lassen.

    Beispiel: Methoden der Klasse Object überschreiben

    Die Klasse Test wird zuerst als leere Klasse ohne Member deklariert und die Anwendung ausgeführt. Sie erhalten die Ausgabe:

    Gleiche Referenz: tl, t2
    Gleich: t1, t2
    ToString(): CSharpBuch.Kap07.Test
    HashCode(): 58225482

    Die Referenzen von t1 und t2 sind tatsächlich gleich, da sie das gleiche Objekt referenzieren. Der Methode ReferenceEquals() werden dazu einfach die Objektreferenzen von zwei Objekten übergeben. Dagegen sind t1 und t3 nicht gleich, da sie zwar denselben Typ haben, aber eben nicht das gleiche Objekt referenzieren. Auf die gleiche Weise arbeitet die Methode Equals(), die aber auf einem konkreten Objekt aufgerufen wird. Die Methode ToString() gibt standardmäßig den vollständigen Typnamen aus und der Hashcode wird berechnet (siehe z.B. Hashfunktion - Wiki).

    Jetzt implementieren Sie die Klasse Test, indem Sie zwei Instanzvariablen hinzufügen und die Methoden ToString(), Equals() und HashCode() überschreiben. In der Methode ToString() werden nun die Datenwerte der Instanzvariablen, durch Doppelpunkt getrennt, ausgegeben. Equals() liefert schon dann true zurück, wenn es sich um den gleichen Objekttyp handelt und der Hashcode beider Objekte gleich ist. Es muss sich also nicht mehr um dasselbe Objekt handeln. Und zur Berechnung des Hashcodes wird einfach der Rückgabewert der Methode GetHashCode() der Basisklasse (also Object) durch 2 dividiert.

    using System;
    namespace CSharpBuch.Kap07
    {
      public class ObjectBasisklasse
      {
        public static void Main(string[] args)
        {
          Test t1 = new Test();
          Test t2 = t1;
          Test t3 = new Test();
          if(Object.ReferenceEquals(t1, t2))
            Console.WriteLine("Gleiche Referenz: tl, t2");
          if(Object.ReferenceEquals(t1, t3))
            Console.WriteLine("Gleiche Referenz: t1, t3");
          if(t1.Equals(t2))
            Console.WriteLine("Gleich: t1, t2");
          if(t1.Equals(t3))
            Console.WriteLine("Gleich: t1, t3");
          Console.WriteLine("ToString(): " + t1.ToString());
          Console.WriteLine("HashCode(): " + t1.GetHashCode());
        }
      }
      class Test
      {
        private string x = "test";
        private int y = 100;
        public override string ToString()
        {
          return String.Format("{0}:{1}", x, y);
        }
        public override int GetHashCode()
        {
          return base.GetHashCode() / 2;
        }
        public override bool Equals(object obj)
        {
          return (this.GetType() == obj.GetType()) &
                 (this.GetHashCode() == obj.GetHashCode());
        }
      }
    }
    Listing 7.10: ObjectBasisklasse.cs

    Ausgabe mit überschriebenen Methoden:
    Gleiche Referenz: tl, t2
    Gleich: t1, t2
    ToString(): test:100
    HashCode(): 29112741

    INFO

    Wenn Sie Objekte korrekt vergleichen wollen, müssen die Methoden GetHashCode() und Equals() passend überschrieben werden, damit beide bei allen Objekten den gleichen oder unterschiedliche Werte liefern.

    7.7 Abstrakte Klassen und Methoden

    Wenn Sie in einer Basisklasse eine Methode als virtuell deklarieren, kann diese in einer abgeleiteten Klasse überschrieben werden, muss aber nicht. In einigen Fällen möchte man jedoch erzwingen, dass eine Methode in einer abgeleiteten Klasse überschrieben wird. Eine andere Möglichkeit wäre, dass man eine Klasse nur als Vorlage für andere Klassen erstellen will, ohne dass von dieser Klasse Instanzen gebildet werden können. Kurz gesagt, Sie möchten eine Vorlage erstellen, die dann je nach Anwendungsfall erweitert wird bzw. erweitert werden muss.

    Syntax

  • Eine Klasse wird zur abstrakten Klasse, wenn Sie der Klassendeklaration das Schlüsselwort abstract voranstellen. Die Reihenfolge der Modifizierer und von abstract ist egal.
  • Auch alle abstrakten Methoden sind durch dieses Schlüsselwort zu kennzeichnen. In diesem Fall hat die Methode keinen Rumpf und es wird direkt nach den runden Klammern ein Semikolon angefügt.
  • Eine abstrakte Methode wird ebenfalls mit abstract definiert. In diesem Fall muss auch die umgebende Klasse mit abstract deklariert werden.
  • Andererseits muss eine abstrakte Klasse nicht unbedingt abstrakte Member enthalten. In diesem Fall soll die Klasse einfach als nicht unmittelbar nutzbare Vorlage dienen.
  • Bei der Ableitung einer abstrakten Klasse müssen zwingend alle mit abstract definierten Methoden überschrieben werden, um eine so genannte konkrete Klasse zu erzeugen. Dabei ist das Schlüsselwort override wie bei polymorphen Methoden anzugeben.
  • Wenn Sie nicht alle abstrakten Methoden einer abstrakten Klasse überschreiben, müssen Sie diese Klasse ebenfalls als abstract deklarieren und können auch von ihr keine Instanzen erzeugen.
  • Vormals virtuelle Methoden können in einer abgeleiteten abstrakten Klasse als abstract gekennzeichnet werden.
  • Von einer abstrakten Klasse lassen sich keine Objekte bilden. Es ist jedoch möglich, eine Variable vom Typ der abstrakten Klasse zu erzeugen, welcher "Nachkommen" von konkreten Klassen zugewiesen werden können, die von der abstrakten Klasse abgeleitet sind.

    Damit eignen sich abstrakte Klassen hervorragend, um bereits allgemeingültige Anweisungen aufzunehmen und neben einem Methodengerüst auch schon etwas "Füllung" zu liefern. Die von einer abstrakten Klasse abgeleiteten Klassen können später ihre spezielle Funktionalität hinzufügen. Sofern die abgeleiteten Klassen ihrer Schnittstelle keine wichtigen neuen Methoden hinzufügen, können Sie über ein Objekt vom Typ der abstrakten Klasse alle Methoden der abgeleiteten Klasse aufrufen. Durch einen Austausch des konkreten Objekts kann durch das Ändern einer einzigen Zeile eine völlig andere Funktionalität erzeugt werden. Dazu nun ein konkretes Beispiel.

    Beispiel: Abstrakte Klassen erlauben einfache Erweiterungen

    Ihre Aufgabe ist es, eine komplexe Anwendung zu erstellen, die von mehreren Benutzern gleichzeitig verwendet wird. Um einen besseren Überblick zu erhalten, welcher Nutzer welchen Teil der Anwendung nutzt, möchten Sie ein so genanntes Logging-Feature implementieren (also ein Protokollieren von ausgewählten Informationen). Die aufzuzeichnenden Informationen beinhalten den Benutzernamen, die aufgerufene Funktionalität und die Zeitdauer der Bearbeitung. Später können Sie aufgrund dieser Informationen die am häufigsten ausgeführten Funktionen optimieren und gegebenenfalls nie verwendete Funktionen eliminieren.

    Sie haben sich überlegt, dass Sie momentan zwei Ausgabeziele für die Logging-Informationen benötigen - die Konsole und eine Log-Datei. Später sollen die Informationen auch noch in eine Datenbank geschrieben werden. Als Grundfunktionalität benötigen Sie

  • einen "Speicher" für Ihre Log-Informationen,
  • eine Methode, um einen neuen Eintrag zu erstellen,
  • eine Methode zum Löschen aller Informationen sowie
  • eine Methode zum Speichern bzw. Ausgeben der Daten.

    Zum Speichern der Daten verwenden Sie einen string (im Kapitel Zeichenkettenverarbeitung werden Sie lernen, dass sich hier besser ein StringBuilder eignet). In der Methode Add() übergeben Sie einen String und fügen seinen Inhalt an den vorhandenen String an. Die Methode Clear() weist dem String eine leere Zeichenkette zu und löscht so die aktuellen Log-Informationen. Die Methode Save() deklarieren Sie als abstract, da Sie möchten, dass davon abgeleitete Klassen sie überschreiben, z.B. um eine Konsolenausgabe erzeugen. Damit später Zugriff auf die Log-Informationen besteht, wird noch eine Hilfsmethode LogInhalt() bereitgestellt, über die jederzeit auf den aktuellen Log-Inhalt zugegriffen werden kann. Damit können Sie bis auf die Methode Save() bereits alle anderen Methoden in der abstrakten Klasse implementieren. Weil Sie das Logging auch in anderen Anwendungen einsetzen wollen, erstellen Sie eine neue Datei Logging.cs und fügen dort die Log-Klassen ein.

    Sie beginnen mit der abstrakten Klasse Logging. Davon leiten Sie die Klasse KonsoleLogging ab. Diese überschreibt nun die abstrakte Methode Save() und gibt den Log-Inhalt auf der Konsole aus. Das war doch einfach. In einer zweiten Klasse DateiLogging benötigen Sie noch einen speziellen Konstruktor, um den Dateinamen zu übergeben, der später beim Speichern verwendet werden soll. Über einen StreamWriter (deshalb das Einbinden des Namespaces System.IO) können Sie einen Text sehr einfach in eine Datei schreiben (Write() - schreiben, Flush() - Ausgabepuffer leeren, Close() - Datei schließen). Die Datei wird in diesem Fall neu angelegt oder überschrieben. Das war's. Durch die Verwendung der abstrakten Klasse konnten die konkreten Klassen (zumindest in diesem Beispiel) sehr einfach implementiert werden. Außerdem ist das Umschwenken bzw. Wechseln zwischen einer Konsolen- bzw. Dateiausgabe völlig transparent.

    using System;
    using System.IO;
    namespace CSharpBuch.Kap07
    {
      public abstract class Logging
      {
        string log;
        public void Add(string logEintrag)
        {
          log = log + logEintrag;
        } 
        public void Clear()
        {
          log = "";
        }
        public string LogInhalt()
        {
          return log;
        }
        public abstract void Save();
      }
      public class KonsoleLogging : Logging
      {
        public override void Save()
        {
          Console.WriteLine(LogInhalt());
        }
      }
      public class DateiLogging: Logging
      {
        private string dateiname;
        public DateiLogging(string dateiname)
        {
          this.dateiname = dateiname;
        }
        public override void Save()
        {
          StreamWriter sw = new StreamWriter(dateiname);
          sw.Write(LogInhalt());
          sw.Flush();
          sw.Close();
        }
      }
    }
    Listing 7.11: Logging.cs

    Jetzt sollten Sie das Ergebnis Ihrer Bemühungen noch testen. Dazu erstellen Sie eine Variable vom Typ Logging (also vom Typ der abstrakten Klasse) und weisen ihr ein KonsoleLogging-Objekt zu. Jetzt können Sie über die Methoden der abstrakten Klasse Einträge hinzufügen, die Log-Daten speichern und das Log auch wieder leeren.

    Um von einem Konsole-Logger auf ein Datei-Logging umzustellen, ist nur das Ändern einer einzigen Anweisung notwendig. Sie weisen der Logging-Variablen einfach ein Objekt vom Typ DateiLogging zu. Der Rest der Anwendung (d.h. der Algorithmus - bzw. Ihre Anwendungslogik) bleibt unberührt. Zum Testen sollten Sie den Aufruf von Clear() aber entfernen, da Sie sonst die beiden Log-Einträge in der Datei wieder löschen. Alternativ ändern Sie die Methode Save() der Klasse DateiLogging, so dass die Einträge am Dateiende angefügt werden.

    using System;
    namespace CSharpBuch.Kap07
    {
      public class AbstrakteKlassen
      {
        static void Main(string[] args)
        {
          Logging log = new KonsoleLogging();
          // Logging log = new DateiLogging(@"C:\Temp\Logdatei.txt");
          log.Add("Eintrag 1\n");
          log.Add("Eintrag 2\n");
          log.Save();
          log.Clear(); // falls Logging in Datei - auskommentieren
          log.Save();  // da die Datei sonst wieder geleert wird
          Console.ReadLine();
        }
      }
    }
    Listing 7.12: AbstrakteKlassen.cs

    7.8 Versiegelte Klassen

    Genau wie Methoden können Sie auch Klassen versiegeln. In diesem Fall wird vor dem Schlüsselwort class noch das Schlüsselwort sealed angegeben. Damit legen Sie fest, dass von dieser Klasse keine weiteren Klassen abgeleitet werden dürfen.

    Der Vorteil versiegelter Klassen liegt einerseits in einer Geschwindigkeitssteigerung bei der Ausführung, da der Compiler nicht mehr nach überschriebenen Methoden suchen muss. Andererseits verhindern Sie damit, dass die Klasse erweitert wird. So ist es dann z.B. nicht mehr möglich, dass jemand eine abgeleitete Klasse an eine Methode übergibt, die einen versiegelten Typ als Parameter erwartet.

    Beispiel: Durch Versiegeln die Vererbungslinie beenden

    Eine Klasse zur Matrizenrechnung enthält nur statische Methoden. Da diese Methoden bereits optimiert sind und keine zusätzliche Funktionalität benötigt wird, verhindern Sie zusätzlich die Vererbungsmöglichkeit.

    public sealed class Matrizen
    {
      ...
    }

    7.9 Verschachtelte Klassen

    Innerhalb einer Klasse können Sie neben Instanzvariablen oder Methoden auch neue, verschachtelte (nested) Klassen deklarieren. Diese werden genauso wie die bisher bekannten Klassen deklariert. Sinn einer solchen Klassendeklaration ist es, Funktionalität, die vielleicht häufig in der äußeren Klasse benötigt wird, noch einmal zu kapseln oder um Objekte bereitzustellen, die auf die Elemente der äußeren Klasse unabhängig voneinander zugreifen können.

    Syntax

  • Verschaltete Klassen werden wie "normale" Klassen deklariert.
  • Die Verschachtelungstiefe ist beliebig.
  • Sie können alle Zugriffsmodifizierer verwenden, also auch private, protected und protected internal. Der Modifizierer private ist voreingestellt.
  • Innere Klassen haben Zugriff auf die statischen Member der äußeren Klassen. Nicht-statische Member der äußeren Klasse können nicht genutzt werden, da Objekte innerer Klassen auch ohne äußere existieren können.
       // erzeugt ein Objekt der inneren Klasse
       // (falls diese public ist)
       Aussen.Innen ai = new Aussen.Innen();

  • Innere Klassen können über eine Referenz auf das äußere Objekt auch auf dessen private- und protected-Elemente zugreifen.
  • Sie können Instanzen von inneren Klassen ohne ein Objekt der äußeren Klasse erstellen, vorausgesetzt der Zugriff auf die innere Klasse besteht. Um von einer inneren Klasse auf das äußere Objekt zuzugreifen, übergeben Sie im Konstruktor der inneren Klasse einen Verweis auf das äußere Objekt.

    Beispiel: Innere Klassen zum Zugriff auf äußere nutzen

    Ein typisches Beispiel für innere Klassen ist die Implementierung eines Iterators für Objekte, die in der äußeren Klasse verwaltet werden. So kann eine Klasse z.B. Personendaten oder Dateinamen verwalten. Über einen Iterator kann dann jeweils das nächste Objekt der Liste zurückgegeben werden, so dass die Objekte z.B. über eine while-Anweisung durchlaufen werden können.

    Zuerst erstellen Sie für das Beispiel eine abstrakte Klasse VorIterator, welche nur eine Methode GetNext() enthält. Darüber soll auf den jeweils nächsten Datensatz positioniert werden. Die Klasse StringListe verwaltet eine Liste von Zeichenketten, die hier als Array fester Länge implementiert ist (der Einfachheit halber).

    Weiterhin enthält die Klasse nur die Methode GetVorIterator(), die ein Objekt der inneren Klasse zurückgibt. Da die innere Klasse die von "überall" sichtbare abstrakte Klasse VorIterator erweitert, besteht auch keine Notwendigkeit, den Aufbau der inneren Klasse zu veröffentlichen.

    In der inneren Klasse InternVor, wird die Methode GetNext() der abstrakten Klasse VorIterator implementiert. Dazu merkt sie sich über einen Index die aktuelle Position und vergleicht ihn mit der Länge des Arrays in der äußeren Klasse. Ist kein Datensatz mehr vorhanden, wird null zurückgegeben. Die innere Klasse kann also auch auf die privaten Teile der äußeren Klasse zugreifen. Damit sie das kann, wird ihr im Konstruktor eine Referenz auf das Objekt der äußeren Klasse übergeben (return new InternVor(this)). Dies ist damit auch ein Beispiel zur Verwendung von this.

    Was haben Sie nun erreicht, was nicht schon vorher möglich war? Sie können jetzt beliebig viele Iteratoren zum Durchlaufen der Elemente der äußeren Klasse erzeugen, die sich alle an unterschiedlichen Positionen befinden können. Der Aufwand, dies alles in der äußeren Klasse zu implementieren, wäre um ein Vielfaches höher, denn Sie müssen sich die Positionen für beliebig viele Iteratoren merken und auch wissen, welches Objekt beim Zugriff mit welcher Position verknüpft ist.

    In der Main()-Methode wird zum Testen ein Objekt der Klasse StringListe erstellt. Dann wird ein VorIterator-Objekt über die Methode GetVorIterator() beschafft. Über das Objekt vom Typ der abstrakten Klasse kann nun die Methode GetNext() aufgerufen werden. In der while-Anweisung wird dazu eine Zuweisung an den String s mit dem Vergleich auf null verknüpft.

    using System;
    namespace CSharpBuch.Kap07
    {
      public class VerschachtelteKlassen
      {
        static void Main(string[] args)
        {
          string s;
          StringListe sl = new StringListe();
          VorIterator iter = sl.GetVorIterator();
          while((s = iter.GetNext()) != null)
            Console.WriteLine(s);
        }
      }
      public abstract class VorIterator
      {
        public abstract string GetNext();
      }
      public class StringListe
      {
        private string[] liste = {"Eintrag 1","Eintrag 2","Eintrag 3"};
        private class InternVor: VorIterator
        {
          private int index = -1;
          StringListe sl;
          public InternVor(StringListe sl)
          {
            this.sl = sl;
          }
          public override string GetNext()
          {
            index ++;
            if(index < sl.liste.Length)
              return sl.liste[index];
            else
            {
              index = sl.liste.Length;
              return null;
            }
          }
        }
        public VorIterator GetVorIterator()
        {
          return new InternVor(this);
        }
      }
    }
    Listing 7.13: VerschachtelteKlassen.cs 

    7.10 Übungsaufgaben

    Aufgabe 1

    Erstellen Sie eine Klasse Rechner, welche die Methoden Addition(), Subtraktion(), Multiplikation() und Division() besitzt. Den Methoden sind jeweils zwei Werte zu übergeben. Das Ergebnis der Berechnung ist der Rückgabewert der Methode. Von der Klasse Rechner sind folgende zwei Klasse abzuleiten.

    Die Klasse ProfiRechner ermöglicht wissenschaftliche Berechnungen. Hier sollen zwei weitere Methoden hinzugefügt werden, welche die Fakultät n! (Fakultaet(n)) und die Potenz xy (Potenz(x, y)) berechnen. Beachten Sie bei der Berechnung auch Sonderfälle und geben Sie im Fall ungültiger Parameter 0 zurück.

    Die Klasse GeoRechner dient zur Berechnung von geometrischen Figuren. Definieren Sie dazu die Methoden FlaecheRechteck(a, b) zur Berechnung der Fläche und UmfangRechteck(a, b) zur Berechnung des Umfangs eines Rechtecks mit den Seiten a und b, sowie FlaecheKreis(r) zur Berechnung des Flächeninhalts eines Kreises mit dem Radius r. Realisieren Sie die Berechnungen in dieser Klasse durch Aufrufe der Methoden der Basisklasse.

    In der Main()-Methode sind alle Methoden zu testen. Erzeugen Sie dazu von den abgeleiteten Klassen je ein Objekt. Rufen Sie alle möglichen Methoden für die beiden Objekte auf und geben Sie die Ergebnisse mit erläuternden Kommentaren auf der Konsole aus.

    Aufgabe 2

    Eine abstrakte Klasse Zeit soll eine Variable vom Typ DateTime (Struktur aus dem Namespace System, welche das aktuelle Datum und die aktuelle Zeit repräsentiert) und zwei Konstruktoren besitzen. Ein parameterloser Konstruktor dient dazu, die aktuelle Zeit des Computers zu ermitteln (nutzen Sie dazu die Eigenschaft DateTime.Now) und die Variable damit zu initialisieren. Dem zweiten Konstruktor kann eine beliebige Zeit übergeben werden. Die Klasse definiert außerdem die abstrakte Methode ZeitAnzeige().

    Leiten Sie von dieser Klasse zwei Klassen (ZeitKurz und ZeitLang) ab, welche die abstrakte Methode überschreiben. Die Methode der Klasse ZeitKurz soll die Zeit ohne Sekunden zurückgeben und die der Klasse ZeitLang mit Angabe der Sekunden. Verwenden Sie dazu die Methoden ToShortTimeString() bzw. ToLongTimeString() des DateTime-Objekts. Testen Sie beide Klassen.

    Aufgabe 3

    In der folgenden Abbildung ist eine Vererbungshierarchie dargestellt, die zu implementieren ist. Die Methode Vereinsdaten() liefert die Werte aller Datenelemente als String zurück. Die Methode Einnahmen() liefert die Einnahmen des Vereins (Beitrag * Anzahl Mitglieder) als double-Wert zurück. Und die Methode AnzahlAendern(int x) ändert die Anzahl der Vereinsmitglieder (anzahlMG) auf den übergebenen Wert und liefert die neue Anzahl zurück.

    Vererbungshierarchie für Aufgabe 3

    Abbildung 7.3: Vererbungshierarchie für Aufgabe 3

    Die Methoden VereinsDaten() und Einnahmen() weichen in den abgeleiteten Klassen von den Methoden der Basisklassen ab. In der Methode VereinsDaten() sind zusätzlich die neuen Datenelemente der Klassen zurückzugeben. Von den Einnahmen des Sportvereins ist die Objektmiete abzuziehen. Die Methoden sind so zu überschreiben, dass sie ein polymorphes Verhalten aufweisen.

    Der Quellcode soll so kurz wie möglich sein. Nutzen Sie wenn möglich die Methoden der Basisklasse.

    Erstellen Sie nun eine Klasse mit einer Main()-Methode und einer Methode Ausgaben(). Erzeugen Sie in der Main()-Methode je ein Objekt der Klassen SportVerein und TierVerein. Rufen Sie die Methode Ausgaben() zweimal auf und übergeben Sie ihr jeweils eines der beiden Verein-Objekte. In der Methode Ausgaben() soll jede der drei Methoden für das übergebene Objekt einmal aufgerufen und die Rückgabewerte auf der Konsole ausgegeben werden.