3.6 Konstruktoren
Konstruktoren sind spezielle Methoden, die nur dann aufgerufen werden, wenn mit dem reservierten Wort new eine neue Instanz einer Klasse erzeugt wird. Sie dienen der kontrollierten Initialisierung von Objekten, um beispielsweise Eigenschaften Anfangswerte zuzuweisen, die Verbindung zu einer Datenbank aufzubauen oder eine Datei zu öffnen. Mit Konstruktoren lassen sich unzulässige Objektzustände vermeiden, damit einem Objekt nach Abschluss seiner Instanziierung nicht substanzielle Startwerte fehlen.
Genauso wie Methoden lassen sich Konstruktoren überladen. Allerdings haben Konstruktoren grundsätzlich keinen Rückgabewert, auch nicht void. Bei der Definition eines Konstruktors wird zuerst ein Zugriffsmodifizierer angegeben, direkt dahinter der Klassenbezeichner. Damit sieht der parameterlose Konstruktor der Klasse Circle wie folgt aus:
public Circle() { }
Wird ein Objekt mit
Circle kreis = new Circle();
erzeugt, verbergen sich hinter dem Objekterstellungsprozess zwei Schritte:
- Der erforderliche Speicher für die Daten des Objekts wird reserviert.
- Ein Konstruktor wird aufgerufen, der das Objekt initialisiert.
Ein Blick in die aktuelle Implementierung der Klasse Circle wirft die Frage auf, wo der Konstruktor zu finden ist, der für die Initialisierung verantwortlich ist. Die Antwort ist einfach: Die augenblickliche Klassenimplementierung enthält zwar explizit keinen Konstruktor, er existiert aber dennoch, allerdings implizit. Diesen impliziten Konstruktor, der parameterlos ist, bezeichnet man auch als Standardkonstruktor.
3.6.1 Konstruktoren bereitstellen
Um nach der Instanziierung dem Circle-Objekt individuelle Daten zuzuweisen, muss der Benutzer der Klasse jede Eigenschaft einzeln aufrufen, z. B.:
kreis.Radius = 10;
kreis.XCoordinate = 20;
kreis.YCoordinate = 20;
Wäre es nicht sinnvoller, einem Circle-Objekt schon bei dessen Instanziierung den Radius mitzuteilen, vielleicht sogar auch noch gleichzeitig die Bezugspunktkoordinaten? Genau diese Aufgabe können entsprechend parametrisierte Konstruktoren übernehmen.
Das Beispiel der Circle-Klasse wollen wir daher nun so ergänzen, dass bei der Instanziierung unter drei Erstellungsoptionen ausgewählt werden kann:
- Ein Circle-Objekt kann wie bisher ohne Übergabe von Initialisierungsdaten erzeugt werden.
- Einem Circle-Objekt kann bei der Instanziierung ein Radius übergeben werden.
- Bei der Instanziierung kann sowohl der Radius als auch die Lage des Bezugspunktes festgelegt werden.
Mit diesen neuen Forderungen muss der Code der Circle-Klasse wie folgt ergänzt werden:
public Circle() {}
public Circle(int radius) {
Radius = radius;
}
public Circle(int radius, double x, double y) {
XCoordinate = x;
YCoordinate = y;
Radius = radius;
}
Listing 3.36 Konstruktoren in der Klasse »Circle«
Weiter oben haben Sie im Zusammenhang mit der Bereitstellung der Methode Move erfahren, dass die Zuweisung an ein gekapseltes Feld immer über die Eigenschaftsmethode führen sollte. An dieser Stelle wird dieser Sachverhalt bei der Zuweisung des Radius besonders deutlich. Würden Sie den vom ersten Parameter radius beschriebenen Wert direkt dem Feld _Radius zuweisen, könnte das Circle-Objekt tatsächlich einen negativen Radius aufweisen. Die Überprüfung im set-Accessor der Eigenschaftsmethode garantiert aber, dass der Radius des Kreises niemals negativ sein kann.
3.6.2 Die Konstruktoraufrufe
Im Allgemeinen werden Konstruktoren dazu benutzt, den Feldern eines Objekts bestimmte Startwerte mitzuteilen. Um ein Circle-Objekt zu erzeugen, stehen mit der obigen Konstruktorüberladung drei Möglichkeiten zur Verfügung:
- Es wird ein Kreis ohne die Übergabe eines Arguments erzeugt. Dabei wird der parameterlose
Konstruktor aufgerufen:
Der Kreis hat in diesem Fall den Radius 0, die Bezugspunktkoordinaten werden ebenfalls mit 0 initialisiert.
Circle kreis = new Circle();
- Ein neuer Kreis wird nur mit dem Radius definiert, z. B.:
Es wird der Konstruktor aufgerufen, der ein Argument erwartet. Da den Bezugspunktkoordinaten keine Daten zugewiesen werden, sind deren Werte 0.
Circle kreis = new Circle(10);
- Einem Kreis werden bei der Erzeugung sowohl der Radius als auch die Bezugspunktkoordinaten
übergeben.
Circle kreis = new Circle(10, 15, 20);
Bei der Instanziierung einer Klasse muss der C#-Compiler selbst herausfinden, welcher Konstruktor ausgeführt werden muss. Dazu werden die Typen der übergebenen Argumente mit denen der Konstruktoren verglichen. Liegt eine Doppeldeutigkeit vor oder können die Argumente nicht zugeordnet werden, löst der Compiler einen Fehler aus.
3.6.3 Definition von Konstruktoren
Trotz der Ähnlichkeit zwischen Konstruktoren und Methoden unterliegen Konstruktoren bestimmten, teilweise auch abweichenden Regeln:
- Die Bezeichner der Konstruktoren einer Klasse entsprechen dem Klassenbezeichner.
- Konstruktoren haben grundsätzlich keinen Rückgabewert, auch nicht void.
- Die Parameterliste eines Konstruktors ist beliebig.
- Der Konstruktor einer Klasse wird bei der Instanziierung mit dem Schlüsselwort new aufgerufen.
- Ein Konstruktor kann nicht auf einem bereits bestehenden Objekt aufgerufen werden, beispielsweise um Eigenschaften andere Werte zuzuweisen.
Enthält eine Klasse keinen parametrisierten Konstruktor, wird bei der Erzeugung eines Objekts der implizite, parameterlose Standardkonstruktor aufgerufen. Nun folgt noch eine weitere, sehr wichtige Regel:
Der implizite, parameterlose Standardkonstruktor existiert nur dann, wenn er nicht durch einen parametrisierten Konstruktor überladen wird.
Implementieren Sie nur einen einzigen parametrisierten Konstruktor, enthält die Klasse keinen impliziten Standardkonstruktor mehr. Sie können dann mit
Circle kreis = new Circle();
kein Objekt mehr erzeugen. Wollen Sie das dennoch sicherstellen, muss der parametrisierte Konstruktor ausdrücklich codiert werden. Aus diesem Grund haben wir auch in Circle einen parameterlosen Konstruktor definiert, obwohl er keinen Code enthält. Wir entsprechen damit unserer selbst auferlegten Forderung, ein Circle-Objekt ohne Startwerte erzeugen zu können.
3.6.4 »public«- und »internal«-Konstruktoren
public deklarierte Konstruktoren stehen allen Benutzern der Klasse zur Verfügung. Das macht natürlich im Grunde genommen nur dann Sinn, wenn die Klasse in einer Klassenbibliothek implementiert wird. In dem Fall kann eine andere Anwendung die öffentlichen Konstruktoren dazu benutzen, ein Objekt nach den Vorgaben zu erzeugen, die in der Parameterliste des Konstruktors festgelegt sind. Manchmal ist es jedoch wünschenswert, einen bestimmten Konstruktor nur in der aktuellen Anwendung offenzulegen (also der Anwendung, in der die Klasse definiert ist), um damit eine bestimmte Instanziierung aus anderen Anwendungen heraus zu unterbinden. Mit dem Zugriffsmodifizierer internal können Sie eine solche Einschränkung realisieren. Denken Sie jedoch daran, dass der implizite Standardkonstruktor grundsätzlich immer öffentlich (public) ist.
3.6.5 »private«-Konstruktoren
Sie werden immer wieder Klassen entwickeln, die nicht instanziiert werden dürfen. Um die Instanziierung zu verhindern, muss der parameterlose Konstruktor, der standardmäßig public ist, mit einem private-Zugriffsmodifizierer überschrieben werden, z. B.:
public class Demo {
private Demo() {[...]}
[...]
}
3.6.6 Konstruktorenaufrufe umleiten
Konstruktoren können wie Methoden überladen werden. Jeder Konstruktor enthält dabei typischerweise eine aufgrund der ihm übergebenen Argumente spezifische Implementierung. Manchmal kommt es vor, dass der Konstruktor einer Klasse Programmcode enthält, der von einem zweiten Konstruktor ebenfalls implementiert werden muss. Sehen wir uns dazu die beiden parametrisierten Konstruktoren der Klasse Circle aus Listing 3.36 an:
public Circle(int radius) {
Radius = radius;
}
public Circle(int radius, double x, double y) {
XCoordinate = x;
YCoordinate = y;
Radius = radius;
}
Es fällt auf, dass in beiden Konstruktoren der Radius des Circle-Objekts festgelegt wird. Es liegt nahe, zur Vereinfachung den einfach parametrisierten Konstruktor aus dem dreifach parametrisierten heraus aufzurufen.
Da Konstruktoren eine besondere Spielart der Methoden darstellen und nur über den Operator new aufgerufen werden können, stellt C# eine syntaktische Variante bereit, mit der aus einem Konstruktor heraus ein anderer Konstruktor derselben Klasse aufgerufen wird. Hier kommt erneut das Schlüsselwort this ins Spiel:
public Circle(int radius) {
Radius = radius;
}
public Circle(int radius, double x, double y) : this(radius) {
XCoordinate = x;
YCoordinate = y;
}
Listing 3.37 Konstruktoraufrufumleitung
Die Signatur des dreifach parametrisierten Konstruktors ist um
: this(radius)
ergänzt worden. Dies hat den Aufruf eines anderen Konstruktors, in unserem Fall den Aufruf des einfach parametrisierten, zur Folge. Gleichzeitig wird der vom Aufrufer übergebene Radius, den der dreifach parametrisierte Konstruktor in seiner Parameterliste entgegennimmt, weitergeleitet. Der implizit aufgerufene einfach parametrisierte Konstruktor wird ausgeführt und gibt die Kontrolle danach an den aufrufenden Konstruktor zurück.
Konstruktorverkettung – die bessere Lösung
Betrachten wir unsere drei Konstruktoren in Circle jetzt einmal aus der Distanz und nehmen wir an, wir würden den dreifach parametrisierten aufrufen. Dieser leitet in den zweifach parametrisierten um und dieser wiederum an den parameterlosen. Solche Verkettungen über mehrere Konstruktoren hinweg können natürlich noch extremer ausfallen, wenn noch mehr Konstruktoren in der Klasse eine Rolle spielen. Nicht nur, dass die vielen zwischengeschalteten Aufrufe eine Leistungseinbuße nach sich ziehen, auch die Verteilung der Programmlogik in viele Fragmente trägt nicht dazu bei, den Code überschaubar zu halten.
Aus den genannten Gründen sollten Sie ein anderes Verkettungsprinzip bevorzugen: Leiten Sie immer direkt zu dem Konstruktor mit den meisten Parametern um. Dieser muss dann die gesamte Initialisierungslogik enthalten. Soweit möglich, werden die Übergabeargumente des aufgerufenen Konstruktors weitergeleitet, ansonsten geben Sie Standardwerte an.
Mit dieser Überlegung ändern sich die Konstruktoren in Circle wie folgt:
public Circle() : this(0, 0, 0) {}
public Circle(int radius) : this(radius, 0, 0) {}
public Circle(int radius, double x, double y)
{
Radius = radius;
XCoordinate = x;
YCoordinate = y;
}
Listing 3.38 Überarbeitete Konstruktoren in der Klasse »Circle«
3.6.7 Vereinfachte Objektinitialisierung
Um einem Circle-Objekt mit dem dreifach parametrisierten Konstruktor Daten zuzuweisen, schreiben Sie den folgenden Code:
Circle kreis = new Circle(12, -100, 250);
Es geht aber auch anders. Sie können die Startwerte in derselben Anweisungszeile in geschweiften Klammern angeben, wie das folgende Beispiel zeigt:
Circle kreis = new Circle() { XCoordinate = -7, YCoordinate = 2,Radius = 2 };
Sie können mit dieser Notation sogar auf die runden Klammern verzichten. Diese Art der Objektinitialisierung wird als vereinfachte Objektinitialisierung bezeichnet und orientiert sich nicht an etwaig vorhandenen Konstruktoren. Mit anderen Worten bedeutet dies, dass Sie diese Objektinitialisierung auch dann benutzen können, wenn eine Klasse nur den parameterlosen Standardkonstruktor aufweist.
Sie müssen nicht zwangsläufig alle Eigenschaften angeben. Felder, denen kein spezifischer Wert übergeben wird, werden mit dem typspezifischen Standardwert initialisiert. Die Reihenfolge der durch ein Komma angeführten Eigenschaften spielt keine Rolle, z. B.:
Circle kreis = new Circle {XCoordinate = -100,YCoordinate = -100,Radius = 12};
Die IntelliSense-Hilfe unterstützt Sie bei dieser Initialisierung und zeigt Ihnen die Eigenschaften an, die noch nicht initialisiert sind. Übergeben Sie sowohl im Konstruktoraufruf als auch in den geschweiften Klammern derselben Eigenschaft einen Wert, wird der Wert des Konstruktors verworfen. Das ist wichtig zu wissen, denn ein mit
Circle kreis = new Circle(50) { Radius = 678 };
erzeugtes Objekt hat den Radius 678.
Es hat den Anschein, dass die vereinfachte Objektinitialisierung die Bereitstellung von überladenen Konstruktoren überflüssig macht. Dem ist nicht so, und Sie sollten eine solche Idee auch schnell wieder verwerfen. Konstruktoren folgen der Philosophie der Objektorientierung und werden von jeder .NET-Sprache unterstützt. Das ist bei der vereinfachten Objektinitialisierung nicht der Fall, die im Zusammenhang mit LINQ (Language Integrated Query, siehe Kapitel 11) eingeführt worden ist und nicht die überladenen Konstruktoren ersetzen soll.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.