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.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.
Abbildung 7.1: Klassendiagramm: Person ist die Basisklasse von Mitarbeiter und Student
class Klassenname: Basisklasse
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.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
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.
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!
base()
nicht an, wird also der parameterlose Standardkonstruktor aufgerufen. Dieser existiert aber nur,
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
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.
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:
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")
base.Methodenname()
auf.
base
wie in base.base.Methode()
ist nicht zulässig.
base
darf nicht in statischen Methoden verwendet werden.
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.
is
-Operator, z.B.
// ist p zuweisungskompatibel zur Klasse Person ? if(p is Person)
is
-Operators kann getestet werden, ob ein Objekt in einen bestimmten Typ konvertiert werden kann.
true
oder false
) zurück, der in Bedingungen, z.B. in if
-Anweisungen oder Schleifen, verwendet werden kann.
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.
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
null
liefern.
(Objekt as Objekttyp).Methode();
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
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üsselwortnew
. 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() { ... }
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)
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.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).
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.
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.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.
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
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.
Methode | Beschreibung |
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. |
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.
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(): 58225482Die 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
Gleiche Referenz: tl, t2 Gleich: t1, t2 ToString(): test:100 HashCode(): 29112741
INFO
Wenn Sie Objekte korrekt vergleichen wollen, müssen die MethodenGetHashCode()
und Equals()
passend überschrieben werden,
damit beide bei allen Objekten den gleichen oder unterschiedliche Werte liefern.abstract
voranstellen. Die Reihenfolge der Modifizierer und von abstract
ist egal.
abstract
definiert. In diesem Fall muss auch die umgebende Klasse mit abstract
deklariert werden.
abstract
definierten Methoden überschrieben werden, um eine so genannte konkrete Klasse zu erzeugen. Dabei
ist das Schlüsselwort override
wie bei polymorphen Methoden anzugeben.
abstract
deklarieren und können auch von ihr keine Instanzen erzeugen.
abstract
gekennzeichnet werden.
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
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
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.
public sealed class Matrizen { ... }
private
, protected
und protected internal
. Der Modifizierer private
ist voreingestellt.
// erzeugt ein Objekt der inneren Klasse // (falls diese public ist) Aussen.Innen ai = new Aussen.Innen();
private
- und protected
-Elemente zugreifen.
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
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.
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.
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.
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.
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.