Menu

Konstruktor w C#: domyślny, z parametrami, łańcuchy this() i base()

Jak działają konstruktory w C#: niejawny konstruktor domyślny i kiedy znika, konstruktory z parametrami i przeciążone, łączenie w łańcuch przez this(...) i base(...), kolejność wykonania, konstruktory statyczne i prywatne oraz konstruktory podstawowe z C# 12.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Konstruktor to kod, który uruchamia się, gdy tworzysz obiekt przez new. Ma taką samą nazwę jak klasa, nie ma typu zwracanego, a jego zadaniem jest wprowadzić nowy obiekt w poprawny stan, zanim ktokolwiek go użyje.

Wynik:

Mug: 8.50
Rejected: price

Ponieważ konstruktor odrzuca ujemną cenę, nie może istnieć żaden Product z ujemną ceną. Ta gwarancja to główny powód, żeby pisać konstruktory zamiast ustawiać pola z zewnątrz.

Konstruktor domyślny i kiedy znika

Jeśli klasa nie deklaruje żadnego konstruktora, kompilator dostarcza publiczny konstruktor bez parametrów, który nie robi nic poza uruchomieniem inicjalizatorów pól. Dlatego new BankAccount() działa dla klasy, która ma tylko pola.

Gdy tylko zadeklarujesz jakikolwiek konstruktor, ten niejawny przestaje być generowany:

class Product
{
    public string Name;
    public Product(string name) { Name = name; }
}

var p = new Product();   // error CS7036: There is no argument given that corresponds to the required parameter 'name'

To zaskakuje, gdy konstruktora bez parametrów potrzebuje serializator, ORM albo ograniczenie generyczne new(). Jeśli chcesz mieć oba, zadeklaruj konstruktor bez parametrów samodzielnie:

public Product() { }                       // or
public Product() : this("Unnamed") { }     // delegate to the other one (next section)

Konstruktor może być public, internal, protected lub private. Jego poziom dostępu decyduje, kto może tworzyć przez niego obiekty.

Przeciążanie i łańcuchy przez this(...)

Klasa może mieć kilka konstruktorów z różnymi listami parametrów. Żeby nie kopiować tych samych przypisań do każdego z nich, połącz je w łańcuch: : this(...) wywołuje inny konstruktor tej samej klasy, zanim wykona się bieżące ciało.

Wynik:

new Pizza():
  full constructor: medium, classic, 0
  size-only constructor body
  parameterless constructor body
Result: medium classic with 0 toppings

Łańcuch najpierw wykonuje najpełniejszy konstruktor, a potem wraca aż do tego, który wywołano. Właściwą pracę (walidację, przypisania) trzymaj w jednym konstruktorze, a pozostałe niech tylko dostarczają wartości domyślne.

Alternatywą są parametry opcjonalne: public Pizza(string size = "medium", string crust = "classic", int toppings = 0) daje jeden konstruktor, który obsługuje wszystkie trzy wywołania. Łańcuch nadal lepiej pasuje, gdy krótsze konstruktory muszą coś obliczyć albo gdy zmiana wartości domyślnej nie może wymagać ponownej kompilacji kodu wywołującego (wartości domyślne parametrów opcjonalnych są kopiowane do kodu wywołującego w czasie kompilacji).

Wywołanie konstruktora klasy bazowej przez base(...)

Klasa pochodna nie dziedziczy konstruktorów. Każdy konstruktor klasy pochodnej musi najpierw uruchomić konstruktor klasy bazowej. Jeśli nic nie napiszesz, kompilator wstawia wywołanie bezparametrowego konstruktora bazowego; jeśli klasa bazowa go nie ma, musisz wybrać konstruktor przez : base(...).

Kolejność, w jakiej wszystko się wykonuje, warto zobaczyć choć raz:

Wynik:

Truck field initializer
Vehicle field initializer
Vehicle constructor body
Truck constructor body
KL-204, 3 axles

Najpierw wykonują się inicjalizatory pól, klasy pochodnej przed klasą bazową, a potem ciała konstruktorów, od bazowego w dół. Zanim więc wykona się ciało konstruktora Truck, Plate jest już ustawione. Jedyna pułapka w tej kolejności: jeśli konstruktor bazowy wywołuje metodę virtual, którą Truck nadpisuje, nadpisana wersja wykona się przed ciałem konstruktora Truck i zobaczy Axles wciąż równe 0. Unikaj wywoływania metod wirtualnych z konstruktorów.

Konstruktory statyczne

Konstruktor statyczny inicjalizuje typ, a nie obiekt. Nie ma parametrów ani modyfikatora dostępu, a środowisko uruchomieniowe wywołuje go dokładnie raz, tuż przed pierwszym użyciem typu.

Wynik:

Program started
Loading tax table (runs once)
123.00
119.00

Środowisko uruchomieniowe zapewnia, że konstruktor statyczny jest bezpieczny wątkowo: nawet jeśli kilka wątków jednocześnie sięgnie po typ, wykona się on raz. Jeśli rzuci wyjątek, każde późniejsze użycie typu rzuca TypeInitializationException aż do końca procesu, więc trzymaj go prostym i wolnym od operacji wejścia i wyjścia, które mogą się nie udać.

Konstruktor a inicjalizator obiektu

Inicjalizator obiektu, new Pizza("large") { Toppings = 3 }, nie jest drugim konstruktorem. Kompilator zamienia go na "uruchom konstruktor, a potem przypisz te składowe". Używaj każdego z nich do tego, w czym jest dobry:

  • Parametry konstruktora dla wartości, bez których obiekt nie może być poprawny. Każdy kod wywołujący musi je przekazać, a konstruktor może je sprawdzić.
  • Inicjalizator dla ustawień opcjonalnych z rozsądnymi wartościami domyślnymi.
var order = new Order(customerId: 42) { Note = "Leave at the door", GiftWrap = true };

Od C# 11 właściwość oznaczona required musi zostać ustawiona w inicjalizatorze, co daje składni inicjalizatora część gwarancji konstruktora. Omawia to strona o właściwościach.

Konstruktory prywatne

Konstruktor private oznacza, że instancje może tworzyć tylko sama klasa. Korzystają z tego dwa wzorce. Statyczna metoda fabrykująca, w której klasa kontroluje tworzenie i może zwrócić obiekt z pamięci podręcznej albo null, oraz singleton:

Wynik:

21
100

Błąd kompilatora dla new Temperature(5m) poza klasą to CS0122, ten sam, który modyfikatory dostępu dają dla każdej prywatnej składowej. Nazwane metody fabrykujące rozwiązują realne ograniczenie: dwa konstruktory nie mogą oba przyjmować jednego decimal, ale FromCelsius i FromFahrenheit już mogą. Klasę, która ma tylko składowe statyczne (klasę narzędziową), zamiast ukrywać jej konstruktor, lepiej zadeklarować jako static.

Konstruktory z ciałem wyrażeniowym

Konstruktor, którego ciało to jedna instrukcja, może używać =>:

public Product(string name) => Name = name;

Dzięki krotkom można nawet przypisać kilka pól w jednej linii: public Point(int x, int y) => (X, Y) = (x, y);.

Konstruktory podstawowe (C# 12)

C# 12 pozwala klasie lub strukturze deklarować parametry konstruktora w samej deklaracji typu. Parametry są dostępne w każdej składowej:

public class Customer(string name, int id)
{
    public string Name { get; } = name;          // copy into a property
    public string Label => $"#{id} {Name}";      // or read a parameter directly

    public Customer(string name) : this(name, 0) { }   // other constructors must chain to it
}

Dwie różnice względem rekordów zaskakują. Parametry konstruktora podstawowego klasy nie stają się publicznymi właściwościami; udostępniasz je sam. A parametr użyty wewnątrz składowej jest przechwytywany do ukrytego pola, które pozostaje modyfikowalne, więc name = "x"; wewnątrz metody się kompiluje. Konstruktory podstawowe najlepiej sprawdzają się przy wstrzykiwaniu zależności, gdzie parametry to usługi, które klasa tylko wywołuje:

public class OrderService(IOrderRepository repo, ILogger<OrderService> log)
{
    public Order Get(int id) => repo.Find(id);
}

Częste błędy

  • Utrata konstruktora bez parametrów. Dodanie konstruktora z parametrami usuwa niejawny konstruktor (CS7036 przy każdym new X()).
  • Brak : base(...), gdy klasa bazowa nie ma konstruktora bez parametrów. Kompilator nie może wstawić niejawnego wywołania i zgłasza, że brakuje argumentu dla wymaganego parametru konstruktora bazowego.
  • Wywoływanie metod wirtualnych w konstruktorze. Nadpisana wersja wykonuje się przed ciałem konstruktora pochodnego.
  • Ciężka praca w konstruktorze. Wywołania sieciowe, odczyty plików czy cokolwiek wolnego sprawiają, że obiekty są kosztowne, a wyjątki trudne do obsłużenia. Do takiej pracy użyj statycznej metody fabrykującej albo asynchronicznej metody inicjalizującej.
  • Napisanie typu zwracanego. public void Product() to zwykła metoda o nazwie Product, a kompilator ją odrzuca, bo składowa nie może mieć takiej samej nazwy jak typ, który ją zawiera (CS0542).

Najczęściej zadawane pytania

Czym jest konstruktor w C#?

Konstruktor to specjalna metoda, która uruchamia się przy tworzeniu obiektu przez new. Ma taką samą nazwę jak klasa i nie ma typu zwracanego: public Product(string name) { Name = name; }. Jego zadaniem jest zostawić nowy obiekt w poprawnym stanie, zwykle przez przypisanie pól z parametrów i odrzucenie błędnych danych.

Czy C# automatycznie tworzy konstruktor domyślny?

Tylko wtedy, gdy klasa nie deklaruje żadnego konstruktora. Wtedy kompilator dodaje publiczny konstruktor bez parametrów, który zostawia każde pole z wartością z inicjalizatora lub domyślną. Gdy tylko napiszesz jakikolwiek konstruktor, na przykład przyjmujący nazwę, niejawny znika i new Product() przestaje się kompilować (błąd CS7036). Jeśli nadal go potrzebujesz, zadeklaruj public Product() { } samodzielnie.

Jak wywołać jeden konstruktor z innego w C#?

Użyj : this(...) za listą parametrów konstruktora: public Product(string name) : this(name, 0m) { }. Najpierw wykonuje się konstruktor docelowy, potem ciało tego, który wywołano. Konstruktor klasy bazowej wywołuje się tak samo przez : base(...).

Czym jest konstruktor statyczny w C#?

To konstruktor oznaczony static, bez parametrów i bez modyfikatora dostępu, który uruchamia się raz na typ, zanim powstanie pierwsza instancja albo zostanie użyta pierwsza składowa statyczna. Służy do inicjalizacji pól statycznych, które wymagają więcej niż jednowierszowego inicjalizatora. Nie możesz go wywołać sam, a jeśli rzuci wyjątek, typ staje się bezużyteczny do końca działania programu (TypeInitializationException).

Konstruktor czy inicjalizator obiektu?

Parametrów konstruktora używaj dla wartości, bez których obiekt nie może być poprawny, bo kompilator zmusza każdy kod wywołujący do ich przekazania, a konstruktor może je sprawdzić. Inicjalizatora obiektu (new Product("Mug") { Color = "blue" }) używaj dla ustawień opcjonalnych. Inicjalizator działa po zakończeniu konstruktora.

Czym są konstruktory podstawowe (primary constructors) w C# 12?

To parametry zapisane w samej deklaracji klasy, class Customer(string name, int id) { ... }, dostępne w każdej składowej klasy. W przeciwieństwie do parametrów pozycyjnych rekordu nie stają się publicznymi właściwościami: udostępnij je jawnie przez public string Name => name; albo public string Name { get; } = name;.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ