Konstruktory — obiekt gotowy do działania
Czy obiekt biletu bez nazwy wydarzenia powinien w ogóle powstać? Konstruktor pozwala ustalić wymagane dane już na wejściu.
Czego się dziś nauczysz
- Napiszesz konstruktor przyjmujący dane i ustawiający pola obiektu.
- Odróżnisz konstruktor domyślny od własnego i powiesz, kiedy ten pierwszy znika.
- Użyjesz
thisdo rozróżnienia pola i parametru o tej samej nazwie. - Wskażesz kolejność: inicjalizator pola, ciało konstruktora, inicjalizator obiektu.
- Wywołasz jeden konstruktor z drugiego przez
: this(...). - Napiszesz konstruktor kopiujący i zdecydujesz o kopii płytkiej albo głębokiej.
Przygotowanie: lekcje 37–39 — umiesz zdefiniować klasę, pole i modyfikator dostępu.
Zaplanuj 1–2 godziny lekcyjne, a zadania rozszerzające wykonaj w osobnym laboratorium. Używamy .NET 8 lub nowszego. Nie łącz kilku kompletnych przykładów w jednym Program.cs.
Wyjaśnienie krok po kroku
Przed: tworzenie i późniejsze uzupełnianie
Gdy obiekt powstaje pusty, a klient dopiero potem wypełnia pola, może zapomnieć o jednej wartości. Różne miejsca programu powtarzają te same przypisania. Konstruktor zbiera przygotowanie obiektu w jednym miejscu. Nie należy jednak wkładać do niego całego menu ani pytania użytkownika o dane.
Nazwa, parametry i brak wyniku
Konstruktor ma dokładnie nazwę klasy i nie zapisujemy przed nim typu wyniku, nawet void. Wyrażenie
new Bilet("Roboty") tworzy obiekt i uruchamia odpowiedni konstruktor. Całe wyrażenie
new daje referencję do obiektu; sam konstruktor nie zwraca jej instrukcją return.
Tak wygląda to w kodzie — Definicja typu — poza Main. To fragment ilustracyjny, nie cały Program.cs.
class Paczka
{
public int Masa { get; }
public Paczka(int masa) // bez void!
{
Masa = masa;
}
}
Przy new Paczka(3) parametr masa ma wartość 3. Konstruktor ustawia właściwość; get udostępnia późniejszy odczyt. Masa jest tu umowną dodatnią liczbą kilogramów — walidację dodajemy w pełnym modelu.
Konstruktor bez parametrów
Zwykła klasa bez żadnego zadeklarowanego konstruktora instancji dostaje od kompilatora konstruktor bezparametrowy. Po napisaniu własnego konstruktora instancji kompilator nie dodaje już tego wygodnego skrótu. Jeśli nadal potrzebujesz new Bilet(), napisz odpowiedni konstruktor sam. Reguły struktur są inne i poznamy je osobno.
Tak wygląda to w kodzie — Dwie niezależne definicje typów — poza Main. To fragment ilustracyjny, nie cały Program.cs.
class Pusta { } // new Pusta() jest dozwolone
class Paczka
{
public Paczka(int masa) { }
}
// new Paczka() nie jest dozwolone:
// ta klasa wymaga argumentu konstruktora.
Porównaj listę konstruktorów obu klas. Komentarze pokazują konsekwencję dodania własnego konstruktora instancji.
Przeciążanie i this(…)
Kilka konstruktorów różniących się parametrami to przeciążenie. : this("Pokaz", 1)
przekazuje inicjalizację innemu konstruktorowi tej samej klasy. Najpierw wykona się wskazany
konstruktor, potem ciało wywołującego. Unikamy kopiowania tych samych kontroli do każdej wersji.
Łańcuch nie może zapętlać się.
Tak wygląda to w kodzie — Definicja typu — poza Main. To fragment ilustracyjny, nie cały Program.cs.
class Paczka
{
public int Masa { get; }
public Paczka() : this(1) { }
public Paczka(int masa) { Masa = masa; }
}
new Paczka() przekazuje pracę do Paczka(1). new Paczka(5) wybiera od razu drugi konstruktor. W obu przypadkach przypisanie jest zapisane raz.
This ma dwie role
this.nazwa = nazwa; przypisuje parametr do pola bieżącego obiektu. Samo nazwa =
nazwa; przy zasłoniętym polu przypisałoby parametr do niego samego. Natomiast :
this(...) nie jest dostępem do pola, lecz wywołaniem innego konstruktora.
Tak wygląda to w kodzie — Definicja typu — poza Main. To fragment ilustracyjny, nie cały Program.cs.
class Paczka
{
private int masa;
public Paczka() : this(1) { } // inny konstruktor
public Paczka(int masa)
{
this.masa = masa; // pole tego obiektu
}
}
Nawiasy po this oznaczają przekazanie inicjalizacji, kropka po this — dostęp do składowej bieżącego obiektu.
Niepoprawne dane
Konstruktor może zgłosić wyjątek, jeśli dane nie pozwalają utworzyć poprawnego obiektu. Przykład korzysta z ArgumentOutOfRangeException dla liczby miejsc. Klient może wcześniej walidować wpis użytkownika, ale konstruktor nadal pilnuje reguły dla innych klientów. Walidacja konsoli i poprawność modelu mają różne zadania.
Kopiowanie i następne kroki
W zwykłej klasie konstruktor przyjmujący obiekt tej samej klasy piszemy samodzielnie. Trzeba zdecydować, które dane kopiujemy, a które współdzielimy. Nie zawsze potrzebujemy kopii głębokiej. Konstruktorów instancji nie dziedziczymy; wywołanie konstruktora klasy bazowej zapisujemy jako base(…). Osobny temat static wyjaśni konstruktor całego typu.
Przewidź → uruchom → zmień
Kompletny, osobny plik Program.cs. Przed uruchomieniem przewidź wynik. Każdy program
uruchamiaj w oddzielnym projekcie konsolowym.
using System;
class Bilet
{
private string nazwa;
private int miejsca;
public Bilet() : this("Pokaz", 1) { }
public Bilet(string nazwa, int miejsca)
{
if (miejsca < 1) throw new ArgumentOutOfRangeException(nameof(miejsca));
this.nazwa = nazwa;
this.miejsca = miejsca;
}
public string Opis() { return $"{nazwa}: {miejsca}"; }
}
class Program
{
static void Main()
{
Bilet zwykly = new Bilet();
Bilet grupowy = new Bilet("Roboty", 4);
Console.WriteLine(zwykly.Opis());
Console.WriteLine(grupowy.Opis());
}
}
Pokaz: 1
Roboty: 4
Co dokładnie robi kod?
new Bilet() przechodzi do konstruktora z argumentami Pokaz i 1. Drugi obiekt od razu wybiera wersję z dwoma parametrami. Opis jest naszą metodą zwracającą string, bez zmiany stanu. nameof(miejsca) jest operatorem języka dającym napis „miejsca”; nie odczytuje wartości parametru. ArgumentOutOfRangeException tworzy obiekt wyjątku z nazwą wadliwego parametru. throw przerywa normalny przepływ. Console.WriteLine(string) wypisuje opis i kończy wiersz. W przykładzie walidujemy tylko liczbę miejsc; kontrola nazwy to osobne ćwiczenie.
Zmień jedną daną i zapisz przewidywany wynik przed uruchomieniem. Następnie porównaj obserwację z przewidywaniem i wskaż instrukcję odpowiedzialną za różnicę.
Trzy miejsca, w których pole dostaje wartość
Pole obiektu może zostać ustawione na trzy sposoby, a wykonują się one w ustalonej kolejności. Warto ją znać, bo od niej zależy, która wartość zostanie ostatecznie w polu.
class Bilet
{
public string Nazwa = "bez nazwy"; // 1. inicjalizator pola
public int Miejsce = 1; // 1. inicjalizator pola
public bool Oplacony; // brak inicjalizatora - wartosc domyslna false
public Bilet(string nazwa, int miejsce)
{
Nazwa = nazwa; // 2. konstruktor
Miejsce = miejsce;
}
}
Bilet b = new Bilet("Turniej", 12)
{
Oplacony = true // 3. inicjalizator obiektu
};
Console.WriteLine($"{b.Nazwa}, miejsce {b.Miejsce}, oplacony: {b.Oplacony}");
Turniej, miejsce 12, oplacony: True
Kolejność jest zawsze taka sama: najpierw inicjalizatory pól, potem ciało
konstruktora, na końcu inicjalizator obiektu z klamer po new.
Dlatego wartość "bez nazwy" nigdy nie trafia na ekran — konstruktor nadpisuje ją chwilę
później.
Po co inicjalizator pola, skoro jest konstruktor
Przydaje się wtedy, gdy wartość początkowa jest taka sama dla wszystkich konstruktorów — nie
trzeba jej wtedy powtarzać w każdym z nich. Najczęstszy przypadek to pusta kolekcja: private
readonly List<string> miejsca = new List<string>(); gwarantuje, że lista
istnieje zawsze, niezależnie od tego, którym konstruktorem utworzono obiekt.
Konstruktor wywołujący inny konstruktor
Gdy klasa ma kilka konstruktorów, łatwo powielić w nich ten sam kod. Zapis : this(...)
pozwala jednemu konstruktorowi wywołać drugi, zamiast kopiować jego zawartość.
class Bilet
{
public string Nazwa;
public int Miejsce;
public decimal Cena;
// konstruktor glowny - tylko on naprawde ustawia pola
public Bilet(string nazwa, int miejsce, decimal cena)
{
if (string.IsNullOrWhiteSpace(nazwa))
{
throw new ArgumentException("Nazwa nie moze byc pusta.", nameof(nazwa));
}
Nazwa = nazwa;
Miejsce = miejsce;
Cena = cena;
}
// dwa konstruktory pomocnicze - kazdy uzupelnia brakujace dane
public Bilet(string nazwa, int miejsce) : this(nazwa, miejsce, 0m) { }
public Bilet(string nazwa) : this(nazwa, 1, 0m) { }
}
Bilet a = new Bilet("Turniej", 12, 25m);
Bilet b = new Bilet("Turniej", 12);
Bilet c = new Bilet("Turniej");
Console.WriteLine($"{a.Miejsce} za {a.Cena}"); // 12 za 25
Console.WriteLine($"{b.Miejsce} za {b.Cena}"); // 12 za 0
Console.WriteLine($"{c.Miejsce} za {c.Cena}"); // 1 za 0
Konstruktor wskazany przez : this(...) wykonuje się przed ciałem tego,
który go wywołał. Dzięki temu sprawdzenie nazwy zapisano raz, a chroni wszystkie trzy drogi
utworzenia obiektu — także te, w których nazwa przychodzi z zewnątrz.
Dwa błędy, które tu łatwo popełnić
Pierwszy: powielenie sprawdzeń w każdym konstruktorze. Po miesiącu jeden z nich zostanie
poprawiony, a pozostałe nie. Drugi: wzajemne wywołanie — A : this(B) i B :
this(A) — kompilator zgłosi wtedy błąd CS0768 o cyklu w konstruktorach. Zasada: jeden
konstruktor jest główny, reszta tylko uzupełnia brakujące argumenty i wywołuje jego.
Konstruktor kopiujący
Czasem potrzebujesz nowego obiektu o takich samych danych, jak istniejący. W C# nie ma automatycznego konstruktora kopiującego — piszesz go sam. To zwykły konstruktor, który przyjmuje obiekt tej samej klasy i przepisuje z niego pola.
class Bilet
{
public string Nazwa;
public int Miejsce;
public Bilet(string nazwa, int miejsce)
{
Nazwa = nazwa;
Miejsce = miejsce;
}
// konstruktor kopiujacy
public Bilet(Bilet wzor)
{
Nazwa = wzor.Nazwa;
Miejsce = wzor.Miejsce;
}
}
Bilet pierwszy = new Bilet("Ala", 12);
Bilet drugi = new Bilet(pierwszy); // nowy obiekt, te same dane
drugi.Miejsce = 13;
Console.WriteLine(pierwszy.Miejsce); // 12 — oryginal bez zmian
Console.WriteLine(drugi.Miejsce); // 13
Bilet drugi = pierwszy; to co innego: obie nazwy wskazywałyby wtedy ten
sam obiekt. Różnicę omawia osobna lekcja o referencjach i kopiach.
Kopia płytka i głęboka
Jeśli klasa zawiera pole będące obiektem lub kolekcją, przepisanie pola kopiuje referencję, a nie zawartość — obie kopie będą korzystać z tej samej listy. To kopia płytka. Gdy kopia ma być niezależna, w konstruktorze utwórz nową kolekcję i przepisz do niej elementy. Która wersja jest potrzebna, zależy od modelu — zapisz swoją decyzję w komentarzu.
A destruktor?
W C++ obok konstruktora występuje destruktor. W C# pamięć zwalnia odśmiecacz (garbage collector)
i destruktor w tym znaczeniu nie jest potrzebny. Odpowiednikiem dla zasobów zewnętrznych —
plików, połączeń — jest interfejs IDisposable z metodą Dispose i
instrukcja using. Wracamy do tego w lekcji o plikach. Finalizator
~Nazwa() istnieje, ale w kodzie szkolnym się go nie pisze.
Zadania
Nie ma tu gotowych rozwiązań. Oddaj kod, własne przypadki testowe oraz krótkie wyjaśnienie decyzji. Pierwsze zadanie jest podstawowe; ostatnie pozwalają pogłębić temat.
Zaprojektuj Plecak o pojemności od 1 do 40. Zapewnij konstruktor bezparametrowy oraz z parametrem. Sprawdź 1, 40, 0 i 41.
Dodaj do Biletu kontrolę nazwy: null, pusty napis i same spacje nie powinny być akceptowane. Opisz sposób zgłoszenia problemu.
Napisz klasę z jednym konstruktorem przyjmującym int. Sprawdź, co się stanie przy new bez argumentów, i wyjaśnij to własnymi słowami.
Dodaj komunikaty wewnątrz dwóch połączonych konstruktorów i zbadaj kolejność ich wykonania.
Napisz klasę Zgloszenie z polami Imie, Klasa i
Uwagi. Pole Uwagi ma mieć inicjalizator ustawiający je na pusty napis.
Zrób konstruktor główny przyjmujący imię i klasę, sprawdzający, że imię nie jest puste, oraz
konstruktor pomocniczy przyjmujący samo imię i wywołujący główny przez : this(...)
z klasą "nieprzypisana".
W Main utwórz trzy obiekty: oboma konstruktorami oraz jeden z inicjalizatorem
obiektu ustawiającym Uwagi. Wypisz wszystkie trzy i sprawdź, czy sprawdzenie
imienia działa dla każdej drogi utworzenia obiektu.
Sprawdź, czy rozumiesz
Dokumentacja: Microsoft Learn — Konstruktory.