Advanced  Services Vererbung 1

Hier noch einmal die Klasse Person, die wir den folgenden Überlegungen zugrunde legen.

public class Person
{
   // auto-implemented property
   public string Vorname { get; set; }
   private string nachname;

   public Person()
   {
      this.Vorname = "";
      this.nachname = "";
   }

   public Person(String v, String n)
   {
      this.Vorname = v;
      this.nachname = n;
   }

   public string GetNachname()
   {
      return this.nachname;
   }

   public void SetNachname(string nachname)
   {
      this.nachname = nachname;
   }

   // Ausgabe des Namens auf die Konsole
   public void Println()
   {
      Console.WriteLine(this.Vorname + " " + this.nachname);
   }
}

Wir wollen nun diese Klasse um eine Telefonnummer ergänzen. Hierzu gibt es verschiedene Möglichkeiten. Ein Ansatz wurde in einer Übung verfolgt. Hier haben wir einfach den Datenteil entsprechend erweitert. Dieser Ansatz hat den Nachteil, daß damit die alte Klasse auffhört zu existieren. Es gibt Situationen, wo man das nicht haben will. Und auch Situationen, wo das nicht geht, weil etwa mit der Erweiterung ein anderer Entwickler befaßt ist und man den Quellcode nicht ändern will oder kann. Hier bieten sich die folgenden Ansätze an.

Der "hat ein" Ansatz  ( has a )
public class PersTel
{
   // Membervariablen
   private Person p ;
   private String tel;

   //....
}

Die neue Klasse verwendet im Datenteil ein Objekt der Klasse Person. Das ist ein "hat ein - Ansatz". Die neue Klasse hat im Datenteil ein Objekt der Klasse Person zur Verfügung. Vorteil ist, daß die alte Klasse in ihrer Form bestehen bleibt. Man hat nun zwei unabhängige Klassen Person und PersTel. Zudem ist für diesen Ansatz kein Ausbau der Theorie erforderlich. Ein Nachteil dabei ist allerdings die realtib umständliche Implementierung von Methoden., die im folgenden nur angedeutet ist:

public class PersTel
{
   // Instanzvariablen
   private Person p;
   private String tel;

   // Konstruktoren
   public PersTel()
   {
      this.p = new Person();
      this.tel = "";
   }
   public PersTel(String v, String n, String t)
   {
      this.p = new Person(v, n);
      this.tel = t;
   }

   // Methoden
   public String GetVorName()
   {
      return this.p.Vorname;
   }

   public void SetVorName(string v)
   {
      this.p.Vorname = v;
   }

   // properties
   public string Nachname
   {
      get { return p.GetNachname() ; }
      set { this.p.SetNachname(value) ; }
   }

   public String Telefon
   {
      get { return this.tel; }
      set { this.tel = value; }
   }

   public void Println()
   {
      Console.WriteLine(this.GetVorName() + " " + this.Nachname + " " + this.tel);
   }
}

Auch die Verwendung von Properties für die neue Klasse vereinfacht das Vorgehen nicht grundlegend, da in dieser Situation die Properties nicht vom Compiler generiert werden können. Zwar gibt es die Properties Vorname und Nachname bereits gibt, sie können jedoch nicht in der Klasse PersTel verwendet werden, da Person und PersTel zwei völlig verschiedene Datentypen sind. Sie müssen daher für die Klasse PersTel neu geschrieben werden.


Der "ist ein" Ansatz  ( is a )

Durch Vererbung können die Nachteile des obigen Ansatzes vermieden werden. Die Klasse PersTel wird durch Vererbung ein Spezialfall der Klasse Person. Dadurch wird die erbende Klasse PersTel typverwandt zur Klasse Person und die in Person vorhandenen Methoden können von der erbenden Klasse (wenn diese es zuläßt) verwendet werden. Im Gegensatz zur landläufigen Vorstellung von Vererbung bleibt hier der Erblasser am Leben. Durch das reservierte Zeichen: läßt sich diese Situation verblüffend einfach modellieren.

public class PersTel : Person
{
   // einzige neue Instanzvariable
   private String tel;

   //....
}

Wir wollen hier gleich die gebräuchlichsten Bezeichnungen vorstellen, die für diesen Ansatz verwendet werden.

beerbte Klasse
(Person)
Elternklasse, Vaterklasse, Oberklasse, Basisklasse, parentclass, superclass, baseclass
erbende Klasse
(PersTel)
Kindklasse, Sohnklasse, Unterklasse, Ableitung, childclass, subclass

Die graphische Darstellung der Vererbung sieht folgendermaßen aus. Das Rechteck für die Oberklasse wird oberhalb oder links von der Ableitung plaziert. Der Pfeil geht von der Ableitung zur Oberklasse. Der durchgezogene Pfeil symbolisiert die Vererbung.



Bereits jetzt kann man mit dem Defaultkonstruktor Objekte vom Typ PersTel erstellen und diese Objekte dürfen alle Methoden der Klasse Person verwenden, sofern diese public sind, wie das folgende Beispiel zeigt.

public class PersTelDemo1
{
   public static void Main(String args[])
   {
      PersTel pt = new PersTel();
      pt.SetVorname("Kurt");
      pt.SetNachname("Tucholsky");
      pt.Println();
   }

}

Daß dies funktioniert, zeigt uns, daß ein Objekt der Klasse PersTel ein sogar initialisiertes Objekt der Klasse Person enthalten muß, sonst könnten die Methoden SetVorname(), SetNachname() und Println() ja nicht fehlerfrei arbeiten. Im Gegensatz zum "hat ein" - Ansatz ist dieses Person-Objekt jedoch nicht sichtbar. Da der Datenteil von Person private ist, kann auch die erbende Klasse PersTel nur mit Hilfe der public-Methoden aus Person auf diesen geerbten Datenteil zugreifen. Dieser Schutz ist gewollt, der Entwickler der Klasse Person soll bestimmen können, was durch Vererbung weitergegeben wird und was nicht. Wir bauen nun unsere Klasse PersTel analog zum "hat ein" - Ansatz aus.

public class PersTel : Person
{
   // auto-implemented property für eine neue Instanzvariable
   public String Telefon { get; set; }

   // Konstruktoren
   public PersTel()
   {
      this.Telefon = "";
   }

   public PersTel(String v, String n)
   {
      this.Vorname = v;
      this.SetNachname(n);
      this.Telefon = "";
   }

   public PersTel(String v, String n, String t) : base(v,n)
   {
      this.Telefon = t;
   }

   // Methoden
   public void Println()
   {
      Console.WriteLine(this.Vorname + " " + this.GetNachname() + " " + this.Telefon);
   }
}

Jede Klasse erbt von Objekt

Wir legen folgende Klasse an und erzeugen von ihr ein Objekt.

class ZiemlichLeer
{
}

Tatsächlich können wir von dieser Klasse ein Objekt anlegen. Wenn wir die Instanz verwenden wollen, zeigt uns die Intellisense von VisualStudio vier Methoden an.

Ein Blick in die Dokumentation zeigt, daß diese vier Methoden aus der Klasse Object stammen. Somit erbt jede Klasse in C# automatisch von der Klasse Object und die Vererbingshierarchie sieht genauer wie folgt aus.


Die Methoden der Klasse Object

Die Intellisense zeigt genau die Methoden an, die public sind und nicht static. Der Begriff static wird weiter unten geklärt.

Konstruktor
Object() Initializes a new instance of the Object class.
Methoden
Returntyp Name der Methode
virtual bool
 
Equals(Object ob)
Determines whether the specified Object is equal to the current Object.
static bool
 
Equals(Object ob1, Object ob2)
Overloaded. Determines whether two Object instances are equal.
protected virtual void
 
Finalize()
Allows an Object to attempt to free resources and perform other cleanup operations before the Object is reclaimed by garbage collection.
virtual int
 
GetHashCode()
Serves as a hash function for a particular type.
Type
 
GetType()
Gets the Type of the current instance.
protected Object
 
MemberwiseClone()
Creates a shallow copy of the current Object.
static bool
 
ReferenceEquals(Object ob1, Object ob2)
Determines whether the specified Object instances are the same instance.
virtual string
 
ToString
Returns a String that represents the current Object.





Wie ein Objekt instanziiert wird (Konstruktoren und Destruktor bei Vererbung)

Es sei gleich darauf hingewiesen, daß im Gegensatz zu public-Methoden Konstruktoren, auch wenn sie public sind, nicht vererbt werden. Da Konstruktoren speziell für die Initialisierung von Objekten ihres Typs da sind, müssen sie stets neu geschrieben werden. Man kann jedoch auf die Konstruktoren der Elternklasse Bezug nehmen. Ebensowenig wird der Destruktor vererbt. Für den Destruktor gilt noch eine kleine Besonderheit.


Wie ein Objekt entsteht

Um nachzuvollziehen wie ein Objekt entsteht, geben wir uns drei sehr einfache Klassen vor, die in Vererbung stehen. Konstruktoren und Destruktoren enthalten jeweils nur eine Konsolmeldung mit der sie sich dokumentieren.

class GrandPa
{
   public GrandPa()
   {
      Console.WriteLine("Constructing GrandPa");
   }
   ~GrandPa()
   {
      Console.WriteLine("Destroying GrandPa");
   }
}

class Parent : GrandPa
{
   public Parent()
   {
      Console.WriteLine("Constructing Parent");
   }
   ~Parent()
   {
      Console.WriteLine("Destroying Parent");
   }
}


class Child : Parent
{
   public Child()
   {
      Console.WriteLine("Constructing Child");
   }
   ~Child()
   {
      Console.WriteLine("Destroying Child");
   }
}

Eine ebenfalls sehr einfache Mainklasse legt nur ein einziges Objekt vom Typ Child an.

static void Main(string[] args)
{
   Child c = new Child();
   Console.WriteLine();
}

Wir erhalten die folgende Ausgabe:

Das Objekt der Klasse Child enthält also in seinem Inneren ein komplettes GrandPa-Objekt und ein Parent-Objekt sowie die Daten des Childobjekts. Die zerstörung des Objekts erfolgt dann in umgekehrter Reihenfolge, sozusagen von außen nach innen. Den Aufbau eines Objekts kann man also schematisch folgendermaßen darstellen.


Überschreiben von Methoden  (method overriding ), das Schlüsselwort "new" für Methoden

Zu den Set-Get-Methoden gibt es nichts neues zu sagen. Interessanter ist da schon die Methode Println(). Obwohl wir bereits eine Methode dieses Namens erben, haben wir sie hier neu geschrieben, was der Compiler ohne weiteres zuläßt. Diesen Vorgang nennt man Überschreiben von Methoden (method overriding). Da die geerbte Methode keine Telefonnummer ausgeben kann, haben wir uns zu diesem Schritt entschlossen. Bei einem Aufruf der Form this.GetNachname() greift Nachname() auf den von Person geerbten Datenteil im Objekt this zu. Wie immer kann man ein this vor einem Punkt auch weglassen.


overriding versus overloading

Am Anfang verwechselt man das Überladen von Methoden leicht mit dem Überschreiben von Methoden.


method hiding deutlich machen mit "new" (optional)

Wer dem Compiler genau auf die Finger schaut, wird folgendes Warning bemerkt haben:

warning CS0108: 'Child.Method()' hides inherited member 'Parent.Method()'. Use the new keyword if hiding was intended.

Durch das Überschreiben von Println() in Child wird also die von Parent geerbte Methode Println() verdeckt. Mit dem (optionalen) Schlüsselwort "new" kann (und sollte ) man das im Code deutlich machen.

// Methoden
new public void Println()
{
   Console.WriteLine(Vorname + " " + this.GetNachname() + " " + Telefon);
}

Der base-Mechanismus für Methoden

Unsere Überschreibung der Println()-Methode erscheint umständlich und redundant. Mit Hilfe des Schlüsselworts "base" kann man sich nämlich in der abgeleiteten Klasse elegant auf Methoden der Elternklasse beziehen:

// Methoden
new public void Println()
{
   base.Println();
   Console.WriteLine(Telefon);
}

Der base-Mechanismus für Konstruktoren

Wir haben bereits eingangs festgestellt, daß unser mit dem Defaultkonstruktor initialisiertes PersTel-Objekt einen initialisierten Person-Teil enthält. Das kann man sich nur so erklären, daß bei der Initialisierung des PersTel-Objektes auch der Person-Teil initialisiert ist. Der Compiler ergänzt nämlich automatisch als erstes Statement eines Konstruktors einen heimlichen Aufruf des Defaultkonstruktors der Elternklasse.

public class PersTel : Person
{
   private String tel;

   // Konstruktoren
   public PersTel()
   {
      // versteckter Aufruf des Defaultkonstruktors von Person
      tel = "" ;
   }

}

Man kann diesen Aufruf aber auch selbst schreiben. Damit hat man die Möglichkeit, auch andere Konstruktoren der Elternklasse zu aktivieren. Die Konstruktoren der Elternklasse werden dabei aus der Kindklasse heraus immer mit dem Namen base aufgerufen. Zudem muß ein base-Aufruf immer das erste Statement in einem Konstruktor sein. Wir zeigen dies im folgenden Beispiel.

public class PersTel : Person
{
   // auto-implemented property für eine neue Instanzvariable
   public String Telefon { get; set; }

   // Konstruktoren
   public PersTel() : base() ;  // expliziter Aufruf des Defaultkonstruktors von Person
   {
      Telefon = "" ;
   }
   public PersTel(String n) : base(n) ;  // expliziter Aufruf des Konstruktors Person(String n)
   {
      Telefon = "" ;
   }
   public PersTel(String v, String n) : base(v, n) ;  // Aufruf des Konstruktors Person(String v, String n)
   {
      Telefon = "" ;
   }

   // die letzteren als Übung

   public PersTel(String v, String n, String t) : base(v, n) ;  // Aufruf des Konstruktors Person(String v, String n)
   {
      Telefon = t ;
   }
   public PersTel(Person p) : base(p) ;  // Aufruf des Konstruktors Person(Person p)
   {
      Telefon = "" ;
   }
   public PersTel(Person p, String t) : base(p) ;  // expliziter Aufruf des Konstruktors Person(Person p)
   {
      Telefon = t ;
   }
   public PersTel(PersTel pt) : base(pt) ;  // expliziter Aufruf des Konstruktors Person(Person p)
   {
      Telefon = pt.Telefon ;
   }

}

Mit this zu einem anderen Konstruktor (derselben Klasse) verzweigen

Wir betrachten die Klasse PersTel in der folgenden Variante.

class PersTel : Person
{
   public String Telefon { set; get; }

   public PersTel() : base()
   {
      this.Telefon = "";
   }

   // Verzweigung zum nächsten Konstruktor
   public PersTel(string vn, string nn) : this(vn, nn, "")
   {
   }

   public PersTel(String vorname, String nachname, String telefon) : base(vorname, nachname)
   {
      this.Telefon = telefon;
   }

   //..
}

Hier sehen wir einen ähnlichen Mechanismus wie mit base. Mit Hilfe von this ruft der zweite Konstruktor den dritten auf. Damit kann man Redundanzen bei Konstruktoren vermeiden. Man kann aber entweder nur base() oder nur this() verwenden. Trotzdem gibt es zu jedem Konstruktor einen base()-Aufruf, auch wenn er nicht explizit angegeben wird.


Hinweis für Javaprogrammierer

Es gibt in C# keinen anonymen Konstruktor (Initialisierer).


Typverwandtschaft durch Vererbung

Ein weiterer Vorteil der Vererbung ist die dadurch entstehende Typverwandtschaft. Sie kann verglichen werden mit der Typverwandtschaft bei primitiven Datentypen. So kann man ja stets zum breiteren primitiven Datentyp hin zuweisen, also ist //1 ok, aber //2 falsch

int a = 17;
double x = 13;

x = a ;     // zulässig         //1
// a = x ;  // unzulässig       //2

Person p = new Person() ;
PersTel pt = new PersTel() ;

pt = p ;     // unzulässig      //3
p  = pt ;    // zulässig        //4

Ein wenig raffinierter ist die Sache jedoch bei Objekten. Es überrascht zunächst, daß die Zuweisung //3 falsch ist, wo doch PersTel in gewissem Sinne als der umfassendere Datentyp angesehen wird. Wäre jedoch diese Zuweisung erlaubt, dann würde die Referenz pt auf ein Objekt der Klasse Person zeigen, was beim Aufruf etwa der Methode GetTelnum() fatale Folgen hat, existiert doch im Person-Objekt überhaupt keine Telefonnummer. Zeigt dagegen wie in //4 ein Zeiger vpm Typ Person auf ein PersTel- Objekt ist die Datenkonsistenz gewährleistet. Alle Methoden, die Person zur Verfügung hat können auf das Objekt vom Typ PersTel angewandt werden. Es ist allerdings nicht möglich über den Zeiger p etwa auf die Telefonnummer zuzugreifen, da es in Person keine Methoden dazu gibt. Man kann also über die Referenz p fehlerfrei zugreifen, kann aber nicht alle Fähigkeiten des Objekts ausschöpfen. In diesem Zusammenhang nennt man p gerne einen Basiszeiger. Die Typverwandtschaft über Vererbung ist die einzige Verwandtschaft, die es bei Klassen gibt. Klassen, die nicht in einer direkten Vererbungslinie stehen, sind nicht verwandt. Zum besseren Verständnis betrachten wir dazu einen Teil der überschaubaren Hierarchie zu den Klassen String und StringBuilder.



Die beiden wichtigsten Punkte zur Typverwandtschaft von Klassen lauten:

So sind String und StringBuilder offensichtlich thematisch verwandt aber eben nicht typverwandt ! Dagegen sind alle Klassen der unteren Ebene typverwandt mit Object. Diese Verwandtschaft äußert sich in einer Zuweisungskompabilität von unten nach oben. Referenzen einer unteren Ebene können Referenzen einer höheren Ebene zugewiesen werden aber nicht umgekehrt, d.h.


Späte Bindung  (late binding) mit virtual und override

Late bindung nennt man die Eigenschaft, daß die VM erst zur Laufzeit entscheidet welche Methode aufgerufen wird, wenn durch Vererbung mehrere Methoden zur Verfügung stehn. Es gibt Diskussionen darüber, ob man den durch das Schlüsselwort "virtual" einschaltbaren Mechanismus tatsächlich "late binding" nennen soll oder nicht (siehe etwa https://en.wikipedia.org/wiki/Late_binding). Für Sprachen, die eine Runtime Environment benützen wie C# oder Java spielt diese mehr theoretische Diskussion in der Praxis meiner Meinung nach keine Rolle. Bevor wir "late bindung" näher erläutern, halten wir eine wichtige Eigenschaft von C# fest:


Die Voreinstellung in C# : late binding ist abgeschaltet

Wir betrachten die folgende Situation

class Program
{
   static void Main(string[] args)
   {
      Child ch = new Child();
      Parent pa = ch;

      ch.Method();
      pa.Method();
   }
}

mit den Klassen Parent und Child:

class Parent
{
   public void Method()
   {
      Console.WriteLine("Method of Parent");
   }
}

class Child : Parent
{
   public void Method()
   {
      Console.WriteLine("Method of Child");
   }
}

Das Programm macht die folgende zu erwartende Ausgabe:

Bei "pa" handelt es sich ja tatsächlich um ein Objekt vom Typ Child. Diese Information geht aber bei der (zulässigen) Zuweisung "Parent pa = ch;" leider verloren.


Mit virtual und override wird late binding eingeschaltet

Es wäre oft sehr nützlich, wenn die Basisreferenz den Typ der rechten Seite kennen würde und dann die "richtige" Methode aufrufen würde. Mit Hilfe einer VMT (virtual method table) ist dies tatsächlich möglich. Mit dem Schlüsselwort "virtual" signalisiert man dem Compiler, daß er eine VMT erstellen soll.

Wir ändern nun unsere Klassen und setzen die Schlüsselwörter "virtual" und "override" ein:

class Parent
{
   public virtual void Method()
   {
      Console.WriteLine("Method of Parent");
   }
}

class Child : Parent
{
   public override void Method()
   {
      Console.WriteLine("Method of Child");
   }
}

Ohne Änderung von Main erhalten wir nun die folgende Ausgabe:

Es ist wichtig, darauf hinzuweisen, daß beide Schlüsselwörter verwendet werden müssen um dieses Verhalten zu erreichen. "virtual" alleine ist wirkungslos und führt lediglich zu einem Warning des Compilers. "override" alleine führt zu einer Fehlermeldung.

Diese erstaunliche Tatsache nennt man späte Bindung. Erst zur Laufzeit wird entschieden, welche Println() Methode verwendet wird. Der Basiszeiger kennt den Typ der rechten Seite. Mit Hilfe der zu dieser Methode gehörenden VMT kann die Basisreferenz die richtige Methode finden. In der VMT sind alle virtuellen Methoden dieses Namens in der Reihenfolge der Vererbungshierarchie gespeichert. Vom Typ der rechten Seite ausgehend wird in der VMT aufwärts nach der Methode gesucht. Die so gefundene Methode wird dann aufgerufen. Nochmal in Kurzform zum Merken:

Beim Compilieren wird zunächst nur untersucht, ob eine Methode des entsprechenden Namens in der Basisklasse existiert. Ist dies nicht der Fall, bricht der Compiler mit einer Fehlermeldung ab. Damit wird sichergestellt, daß zumindest in der Basisklasse die entsprechende Methode existiert. Erst zur Laufzeit greift dann der oben beschriebene Suchmechanismus für. Da sichergestellt wurde, daß in der Basisklasse die gesuchte Methode existiert, arbeitet der Suchalgorithmus nie erfolglos.


Weitervererben von virtual

Was ist, wenn eine Klasse eine mit override gekennzeichneten Methode erbt. Pflanzt sich dann die Eigenschaft automatisch fort oder ist das Schlüsselwort override erneut notwendig? Diese Frage klären wir jetzt. Dazu ergänzen wir unsere Hierarchie um ein GrandChild:

class Parent
{
   public virtual void Method()
   {
      Console.WriteLine("Method of Parent");
   }
}

class Child : Parent
{
   public override void Method()
   {
      Console.WriteLine("Method of Child");
   }
}

class GrandChild : Child
{
   public void Method()
   {
      Console.WriteLine("Method of GrantChild");
   }
}

Da wir beim Compilieren wieder die Fehlermeldung warning CS0114: 'GrandChild.Method()' hides inherited member 'Child.Method()' ist die Situation klar:

Das Schlüsselwort "override" ist notwendig damit auch die Methode aus GrandChild in die VMT aufgenommen wird


Der Destruktor und die Finalize()-Methode

Wie man der Tabelle der Methoden von Object entnehmen kann existiert eine Methode Finalize() die nicht öffentlich zugänglich iat, denn das Schlüsselwort protected bedeutet normalerweise, daß die Methode nur in Kindklassen sichtbar ist. Ein vom Entwickler geschriebener Destruktor wird vom Compiler automatisch in einen Aufruf der Finalize()-Methode umgewandelt. Aus den folgenden Zeilen

~MeineKlasse()
{
   // Freigeben belegter Resourcen
}

macht der Compiler

protected override void Finalize()
{
   try
   {
       // Freigeben belegter Resourcen

   finally
   {
       base.Finalize();
   }
}

Die Methode Finalize() spielt daher eine Sonderrolle. Sie ist zwar als protected sichtbar, wird aber trotzdem von der Intellisense nicht angezeigt. Der Versuch diese Methode selbst zu überschreiben endet mit einer Fehlermeldung des Compilers.






Vererbungshierarchien und Mehrfachvererbung

Mit der Klasse PersTel muß die Vererbungslinie keineswegs zu Ende sein. Es ist ohne weiteres möglich, von der Klasse PersTel eine weitere oder sogar mehrere Ableitungen zu bilden. Eine Klasse kann beliebig viele Kindklassen haben. Umgekehrt gibt es aber in C# keine Mehrfachvererbung bei Klassen, eine Klasse kann also nur eine einzige Elternklasse haben. Das ist die gleiche Struktur wie bei Verzeichnissen. Ein Verzeichnis kann mehrere Unterverzeichnisse haben, aber ein Verzeichnis hat genau ein Oberverzeichnis.


Vererbung verbieten mit sealed

Der Vererbungsmechanismus ist grundsätzlich ein offener Mechanismus. Jemand findet eine Klasse, die ein anderer geschrieben hat, für seine Zwecke brauchbar. Allerdings möchte er sie noch genau auf seine Bedürfnisse zuschneiden. Mit Vererbung ist dies möglich. Der Entwickler ergänzt dann Daten und Methoden, wie er es für notwendig hält. Nicht immer will man seine Klassen jedoch einfach an Dritte weitergeben. Möchte der Entwickler einer Klasse verhindern, daß seine Klasse Kinder bekommt, so kann er dies sehr einfach verhüten. Mit dem Schlüsselwort sealed kann er eine Vererbung generell verbieten. Im Falle unserer Klasse Person würde das so aussehen:

public sealed class Person
{
   // ...
}

Der Versuch, diese Klasse zu beerben, also

public class PersTel : Person
{
   // ...
}

endet dann mit der Fehlermeldung "cannot inherit from sealed Person". Beispiele aus der API sind etwa die Klassen String und StringBuffer oder auch die Klasse System .


Überschreiben verbieten mit sealed

Man muß nicht gleich das Vererben pauschal unterbinden, man kann mit sealed auch subtiler arbeiten und nur einzelne Methoden verbieten. Dazu setzt man das Schlüsselwort in der Signatur der Methode ebenfalls nach dem Schlüsselwort public.

public class Person   // Klasse ist nicht sealed
{
   // ...

   public sealed void notOverridable()   // Methode ist sealed
   {
      //...
   }

}

Nun kann man zwar eine Unterklasse von Person bilden, aber obige Methode nicht überschreiben.

public class PersTel : Person     // OK
{
   // ...

   public void notOverridable()   // compile time error
   {
      //...
   }

}

Der Versuch, die Klasse PersTel zu compilieren, endet mit der Fehlermeldung
notOverridable() in PersTel cannot override notOverridable() in Person; overridden method is sealed.

Das explizite Untersagen des Überschreibens hat jedoch zur Folge, daß der Mechanismus der späten Bindung ausgehebelt wird, da man ja in der Unterklasse die Methode nicht mehr überschreiben kann.


Object als Rootklasse

Ähnlich wie es bei Verzeichnissen ein oberstes Verzeichnis gibt, so gibt es in C# auch eine Rootklasse, von der sich alle Klassen ableiten (müssen). Auch unsere Klasse Person ist eine Ableitung der Klasse Object. Der Beweis dafür ist recht einfach zu erbringen:

public class DochNichtRoot
{
   public static void Main(String args[])
   {
      Person p = new Person();
      Console.WriteLine( p.ToString() );
   }

}

Das obige kleine Programm läßt sich fehlerfrei compilieren, obwohl es in unserer Klasse keine Methode mit dem Namen toString() gibt. Wir erben nämlich diese Methode von der Klasse Object. Schreibt man keine explizite : Klausel, so ergänzt der Compiler automatisch ": Object". Also ist hier die Klasse DochNichtRoot keine Wurzelklasse, sondern eine Unterklasse von Object.

Das Konzept, alle Klassen von einer generellen Rootklasse abzuleiten, hat große Vorteile, so sind etwa für jede Javaklasse die folgenden Zuweisungen möglich, da Object ja immer eine Basisklasse ist.

Object ob1, ob2 ;
Person p = new Person();
ob1 = p ;
int arr[] = new int[5] ;
ob2 = arr ;

Wie man sieht, sind auch Arrays nur spezielle Objekte.


Zusammenfassung

Das Thema Vererbung ist hiermit keineswegs erschöpfend behandelt. Eine ganze Reihe von Feinheiten fehlen noch, so gibt es etwa noch neben public und private die access modifier protected und (default). Mit dem letzteren meint man den Schutzzustand, der entsteht, wenn man keinen (der drei anderen) access modifier verwendet. Außerdem ist der genaue Vorgang der Initialisierung eines Objektes, das geerbte Teile enthält, noch nicht geklärt. Des weiteren wurden namenlose Blöcke bzw. anonyme Konstruktoren noch nicht erläutert. Ganz zu schweigen von Interfaces und geschachtelten Klassen (inner class oder in neuerer Terminologie nested class).


Übungen

Ausbau der Klasse PersTel

Anlegen einer Klasse PersAddr

this and base