1. Wprowadzenie
Załóżmy, że mamy klasę Person:
public class Person
{
public string Name;
public int Age;
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
I już czujesz się prawie jak geniusz — możesz tworzyć ludzi z dowolnym imieniem i wiekiem. Ale jest problem: czasem informacji jest za mało! Użytkownik aplikacji podał tylko imię, a wiek z jakiegoś powodu poznamy później. Albo chcesz w niektórych przypadkach ustawić "domyślny" wiek. Jasne, można kombinować, ale C# daje na to eleganckie rozwiązanie — przeciążanie konstruktorów.
Przeciążanie konstruktorów oznacza, że jedna klasa może mieć wiele konstruktorów o tej samej nazwie (czyli nazwie klasy), ale z różnymi listami parametrów.
Czym różnią się te listy? Mogą się różnić przez:
- Liczbę parametrów: Na przykład jeden konstruktor przyjmuje 2 parametry, inny — 3.
- Typy parametrów: Jeden przyjmuje (string, int), inny — (string, decimal).
- Kolejność parametrów: (string, int) i (int, string) — to różne sygnatury.
Najważniejsza zasada: kompilator rozróżnia konstruktory (i w ogóle metody) po ich sygnaturze. Sygnatura to nazwa konstruktora (lub metody) i lista jego parametrów (liczba, typy i kolejność). Modyfikatory dostępu (public, private) i, dla zwykłych metod, typ zwracany, nie są częścią sygnatury. Ale konstruktory nie mają typu zwracanego, a nazwa zawsze jest taka jak klasa.
2. Przeciążanie konstruktorów: składnia i przykład
public class Cat
{
public string Name;
public string Color;
// Konstruktor bez parametrów
public Cat()
{
Name = "Bezimienny kot";
Color = "Szary";
}
// Konstruktor z jednym parametrem
public Cat(string name)
{
Name = name;
Color = "Szary";
}
// Konstruktor z dwoma parametrami
public Cat(string name, string color)
{
Name = name;
Color = color;
}
}
Użycie:
Cat cat1 = new Cat(); // "Bezimienny kot", "Szary"
Cat cat2 = new Cat("Murzik"); // "Murzik", "Szary"
Cat cat3 = new Cat("Ryżyk", "Rudy"); // "Ryżyk", "Rudy"
Jeśli przy tworzeniu obiektu podajesz parametry, C# "zgadnie" odpowiedni konstruktor po ich liczbie i typie.
To mega wygodne! Użytkownik twojej klasy nie musi podawać parametrów, których nie zna: jeśli chce po prostu kota — bierze konstruktor bez parametrów, jeśli zna imię — używa innego, i tak dalej. Chce mieć pełną kontrolę nad kolorem — bierze trzeci wariant.
3. Jak działa wywołanie przeciążonych konstruktorów?
C# wybiera odpowiedni konstruktor automatycznie na etapie kompilacji, na podstawie typu i liczby przekazanych argumentów. Ciężko się pomylić — jeśli coś się nie zgadza, kompilator od razu cię opieprzy.
Spróbujmy dodać konstruktor z parametrem typu int:
public Cat(int age)
{
Name = "Bezimienny kot";
Color = "Szary";
// Dodatkowy kod dla wieku
}
Teraz możemy stworzyć jeszcze "kociaka po wieku":
Cat oldCat = new Cat(5); // Wywoła się konstruktor Cat(int age)
| Wywołanie konstruktora | Jaki się wywoła? |
|---|---|
|
Bez parametrów |
|
Z jednym parametrem typu string |
|
Z dwoma parametrami typu string |
|
Z jednym parametrem typu int |
4. Wewnętrzne wywołania konstruktorów: słowo kluczowe this
Często różne konstruktory robią podobną (albo identyczną) robotę. Żeby nie kopiować kodu, można "wywołać jeden konstruktor z drugiego". Do tego służy słowo kluczowe this.
public class Cat
{
public string Name;
public string Color;
public Cat() : this("Bezimienny kot")
{
// Ten konstruktor wywołuje Cat(string name)
}
public Cat(string name) : this(name, "Szary")
{
// Ten konstruktor wywołuje Cat(string name, string color)
}
public Cat(string name, string color)
{
Name = name;
Color = color;
}
}
W efekcie:
- new Cat() wywołuje Cat(string name), który wywołuje Cat(string name, string color).
- Wszystkie "drogi prowadzą do Rzymu": czyli cała inicjalizacja jest skupiona w jednym "głównym" konstruktorze.
Taki sposób nazywa się constructor chaining (łańcuchowanie konstruktorów).
5. Przykłady z życia
Stwórzmy klasę bohater (Hero) do gry, który ma imię i poziom! Tak byśmy to napisali:
Z jednym konstruktorem:
public class Hero
{
public string Name;
public int Level;
public Hero(string name, int level)
{
Name = name;
Level = level;
}
}
A teraz pozwólmy tworzyć go na różne sposoby!
Wiele konstruktorów:
public class Hero
{
public string Name;
public int Level;
// Jeśli nic nie podano, bohater to "Bezimienny 1 poziomu"
public Hero() : this("Bezimienny", 1)
{
}
// Jeśli znamy tylko imię, poziom domyślnie — 1
public Hero(string name) : this(name, 1)
{
}
// Najważniejszy konstruktor z dwoma parametrami
public Hero(string name, int level)
{
Name = name;
Level = level;
}
}
Teraz na różne sposoby można stworzyć bohatera:
Hero h1 = new Hero(); // "Bezimienny", 1
Hero h2 = new Hero("Artur"); // "Artur", 1
Hero h3 = new Hero("Lora", 10); // "Lora", 10
6. Szczegóły implementacji: ukryte pułapki i niuanse
Po pierwsze: kompilator nie tworzy "pustego" konstruktora domyślnie, jeśli sam zadeklarujesz choć jeden inny konstruktor. Więc jeśli napisałeś własny konstruktor z parametrami, ale zapomniałeś dodać bezparametrowy, to taki kod:
Hero h = new Hero(); // Błąd, jeśli nie ma konstruktora bez parametrów!
wywali błąd kompilacji.
Po drugie: jeśli w łańcuchu wywołań konstruktorów wywołasz nie ten, możesz popsuć logikę inicjalizacji. Ważne, żeby trzymać się spójności: najlepiej, żeby cała prawdziwa robota była w najpełniejszym konstruktorze, a reszta tylko przekazywała tam dane przez this(...).
Po trzecie: jeśli parametry różnią się tylko typami (np. Cat(string s) i Cat(object o)), można przypadkiem dostać zamieszanie przy wywołaniu konstruktora z argumentem typu null. Kompilator nie zawsze ogarnie, który konstruktor chcesz wywołać.
7. Przeciążanie i inicjalizacja pól
Często w klasie są pola, które trzeba koniecznie ustawić. Przeciążanie konstruktorów pomaga zrobić to wygodnie, ale to ty decydujesz, jakie wartości domyślne mają sens dla twojej klasy.
Przykład z walidacją:
public class Book
{
public string Title;
public int Year;
// Książka domyślnie — "Bez tytułu", rok 2000
public Book() : this("Bez tytułu", 2000)
{
}
public Book(string title) : this(title, 2000)
{
}
public Book(string title, int year)
{
Title = title;
Year = year;
}
}
8. Przeciążanie z różnymi zestawami parametrów
Możesz spotkać klasy, gdzie liczba konstruktorów idzie w dziesiątki! Nie zawsze to znak dobrej architektury (czasem lepiej użyć metod konfiguracyjnych albo wzorca "builder"), ale dla obiektów użytkowych, modeli, DTO — całkiem spoko opcja.
Przykład: klasa "Zwierzak", gdzie możesz jawnie podać wszystkie cechy, albo tylko te najważniejsze.
public class Pet
{
public string Name;
public int Age;
public string Type;
public bool IsVaccinated;
// Konstruktor domyślny
public Pet() : this("NoName", 0, "Cat", false) { }
// Minimum informacji
public Pet(string name, string type) : this(name, 0, type, false) { }
// Pełny konstruktor
public Pet(string name, int age, string type, bool isVaccinated)
{
Name = name;
Age = age;
Type = type;
IsVaccinated = isVaccinated;
}
}
Różnica między przeciążaniem metody a konstruktora
| Zwykła metoda | Konstruktor (w tym przeciążony) |
|---|---|
| Można wywołać w dowolnym momencie | Wywołuje się tylko przy tworzeniu obiektu (new) |
| Może zwracać wartość dowolnego typu | Nigdy nie zwraca wartości (nie podaje się typu) |
| Nazwa metody dowolna, zwykle czasownik | Nazwa zawsze taka jak klasa |
| Można przeciążać po parametrach | Też przeciąża się po parametrach |
GO TO FULL VERSION