1. Einführung
Nehmen wir an, wir haben eine Klasse Person:
public class Person
{
public string Name;
public int Age;
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
Und du fühlst dich schon fast wie ein Genie – du kannst Personen mit beliebigem Namen und Alter erstellen. Aber hier kommt das Problem: Manchmal reichen die Infos nicht! Der User deiner App hat nur den Namen eingegeben, das Alter ist aus irgendeinem Grund erst später bekannt. Oder du willst in manchen Fällen ein "Standard"-Alter vergeben. Klar, man könnte sich Workarounds ausdenken, aber C# hat dafür eine elegante Lösung – Konstruktor-Überladung.
Konstruktor-Überladung bedeutet, dass eine Klasse mehrere Konstruktoren mit demselben Namen (also dem Klassennamen) haben kann, aber mit unterschiedlichen Parameterlisten.
Worin unterscheiden sich diese Listen? Sie können sich unterscheiden durch:
- Anzahl der Parameter: Zum Beispiel nimmt ein Konstruktor 2 Parameter, ein anderer – 3.
- Typen der Parameter: Einer nimmt (string, int), ein anderer – (string, decimal).
- Reihenfolge der Parameter: (string, int) und (int, string) – das sind unterschiedliche Signaturen.
Das wichtigste Regel: Der Compiler unterscheidet Konstruktoren (und auch Methoden) anhand ihrer Signatur. Die Signatur besteht aus dem Namen des Konstruktors (oder der Methode) und der Parameterliste (Anzahl, Typen und Reihenfolge). Zugriffsmodifizierer (public, private) und – bei normalen Methoden – der Rückgabetyp gehören nicht zur Signatur. Konstruktoren haben keinen Rückgabetyp und der Name ist immer gleich dem Klassennamen.
2. Konstruktor-Überladung: Syntax und Beispiel
public class Cat
{
public string Name;
public string Color;
// Konstruktor ohne Parameter
public Cat()
{
Name = "Namenlose Katze";
Color = "Grau";
}
// Konstruktor mit einem Parameter
public Cat(string name)
{
Name = name;
Color = "Grau";
}
// Konstruktor mit zwei Parametern
public Cat(string name, string color)
{
Name = name;
Color = color;
}
}
Verwendung:
Cat whiskers = new Cat(); // "Namenlose Katze", "Grau"
Cat mittens = new Cat("Murzik"); // "Murzik", "Grau"
Cat ginger = new Cat("Ryzhik", "Rot"); // "Ryzhik", "Rot"
Wenn du beim Erstellen eines Objekts Parameter angibst, "errät" C# den passenden Konstruktor anhand der Anzahl und Typen der Parameter.
Das ist super praktisch! Der User deiner Klasse muss keine Parameter angeben, die er nicht kennt: Wenn er einfach nur eine Katze will – nimmt er den Konstruktor ohne Parameter, wenn der Name bekannt ist – nimmt er den anderen, und so weiter. Will er volle Kontrolle über die Farbe – nimmt er die dritte Variante.
3. Wie funktioniert der Aufruf von überladenen Konstruktoren?
C# wählt den passenden Konstruktor automatisch beim Kompilieren aus, je nach Typ und Anzahl der übergebenen Argumente. Da kann man sich kaum vertun – wenn doch was schiefgeht, meckert der Compiler sofort.
Probieren wir mal einen Konstruktor mit einem int-Parameter hinzuzufügen:
public Cat(int age)
{
Name = "Namenlose Katze";
Color = "Grau";
// Zusätzlicher Code für das Alter
}
Jetzt können wir auch eine "Katze nach Alter" erstellen:
Cat oldCat = new Cat(5); // Ruft den Konstruktor Cat(int age) auf
| Konstruktor-Aufruf | Welcher wird aufgerufen? |
|---|---|
|
Ohne Parameter |
|
Mit einem string-Parameter |
|
Mit zwei string-Parametern |
|
Mit einem int-Parameter |
4. Interne Konstruktoraufrufe: Das Schlüsselwort this
Oft machen verschiedene Konstruktoren ähnliche (oder sogar gleiche) Dinge. Damit du Code nicht doppelt schreibst, kannst du "einen Konstruktor aus einem anderen aufrufen". Dafür gibt's das Schlüsselwort this.
public class Cat
{
public string Name;
public string Color;
public Cat() : this("Namenlose Katze")
{
// Dieser Konstruktor ruft Cat(string name) auf
}
public Cat(string name) : this(name, "Grau")
{
// Dieser Konstruktor ruft Cat(string name, string color) auf
}
public Cat(string name, string color)
{
Name = name;
Color = color;
}
}
Am Ende:
- new Cat() ruft Cat(string name) auf, der wiederum Cat(string name, string color) aufruft.
- Alle "Wege führen nach Rom": Die ganze Initialisierung steckt im einen "Haupt"-Konstruktor.
Das nennt man constructor chaining (Konstruktor-Kettenaufruf).
5. Beispiele aus dem echten Leben
Lass uns eine Klasse Held (Hero) für ein Spiel bauen, mit Name und Level! So könnte das aussehen:
Mit einem Konstruktor:
public class Hero
{
public string Name;
public int Level;
public Hero(string name, int level)
{
Name = name;
Level = level;
}
}
Und jetzt erlauben wir verschiedene Arten, ihn zu erstellen!
Viele Konstruktoren:
public class Hero
{
public string Name;
public int Level;
// Wenn nichts angegeben wird, ist der Held "Namenlos" auf Level 1
public Hero() : this("Namenlos", 1)
{
}
// Wenn nur der Name bekannt ist, Level standardmäßig 1
public Hero(string name) : this(name, 1)
{
}
// Der Hauptkonstruktor mit zwei Parametern
public Hero(string name, int level)
{
Name = name;
Level = level;
}
}
Jetzt kannst du den Helden auf verschiedene Arten erstellen:
Hero h1 = new Hero(); // "Namenlos", 1
Hero h2 = new Hero("Artur"); // "Artur", 1
Hero h3 = new Hero("Lora", 10); // "Lora", 10
6. Implementierungsdetails: Versteckte Fallen und Feinheiten
Erstens: Der Compiler erstellt keinen "leeren" Standardkonstruktor, wenn du selbst mindestens einen anderen Konstruktor deklarierst. Wenn du also einen eigenen Konstruktor mit Parametern schreibst, aber keinen ohne Parameter, dann gibt folgender Aufruf:
Hero h = new Hero(); // Fehler, wenn es keinen parameterlosen Konstruktor gibt!
einen Kompilierungsfehler.
Zweitens: Wenn du in der Konstruktor-Kette den falschen aufrufst, kann die Initialisierungslogik kaputtgehen. Bleib konsistent: Am besten macht der "vollständige" Konstruktor die eigentliche Arbeit, die anderen leiten nur die Daten per this(...) weiter.
Drittens: Wenn sich die Parameter nur durch den Typ unterscheiden (z.B. Cat(string s) und Cat(object o)), kann es bei einem Aufruf mit null zu Verwirrung kommen. Der Compiler weiß dann nicht immer, welchen Konstruktor du meinst.
7. Überladung und Initialisierung von Feldern
Oft gibt es in einer Klasse Felder, die unbedingt gesetzt werden müssen. Konstruktor-Überladung macht das bequem, aber du entscheidest selbst, welche Default-Werte für deine Klasse sinnvoll sind.
Beispiel mit Prüfung:
public class Book
{
public string Title;
public int Year;
// Standard-Buch – "Ohne Titel", Jahr 2000
public Book() : this("Ohne Titel", 2000)
{
}
public Book(string title) : this(title, 2000)
{
}
public Book(string title, int year)
{
Title = title;
Year = year;
}
}
8. Überladung mit verschiedenen Parametersätzen
Manchmal gibt es Klassen mit Dutzenden von Konstruktoren! Das ist nicht immer ein Zeichen für gutes Design (manchmal sind "Setter"-Methoden oder das Builder-Pattern besser), aber für User-Objekte, Models, DTOs – völlig okay.
Beispiel: Die Klasse "Haustier", bei der du alle Eigenschaften explizit angeben kannst, oder nur die wichtigsten.
public class Pet
{
public string Name;
public int Age;
public string Type;
public bool IsVaccinated;
// Standard-Konstruktor
public Pet() : this("NoName", 0, "Cat", false) { }
// Minimum an Infos
public Pet(string name, string type) : this(name, 0, type, false) { }
// Vollständiger Konstruktor
public Pet(string name, int age, string type, bool isVaccinated)
{
Name = name;
Age = age;
Type = type;
IsVaccinated = isVaccinated;
}
}
Unterschied zwischen Methodenüberladung und Konstruktorüberladung
| Normale Methode | Konstruktor (auch überladen) |
|---|---|
| Kann jederzeit aufgerufen werden | Wird nur beim Erstellen eines Objekts aufgerufen (new) |
| Kann Werte beliebigen Typs zurückgeben | Gibt nie einen Wert zurück (kein Typ angegeben) |
| Methodenname beliebig, meist ein Verb | Name ist immer gleich dem Klassennamen |
| Kann nach Parametern überladen werden | Kann ebenfalls nach Parametern überladen werden |
GO TO FULL VERSION