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 — i sprawdzić je, zanim obiekt zaistnieje.
Czego się dziś nauczysz
- Wyjaśnisz, dlaczego obiekt uzupełniany po utworzeniu bywa niegotowy do działania.
- Napiszesz konstruktor z parametrami i wskażesz dwie cechy, które odróżniają go od zwykłej metody.
- Powiesz, kiedy klasa traci konstruktor bezparametrowy, i rozpoznasz komunikat CS1729.
- Użyjesz
thisdo rozróżnienia pola i parametru o tej samej nazwie. - Połączysz kilka konstruktorów zapisem
: this(...)i podasz kolejność ich wykonania. - Odrzucisz niepoprawne dane w konstruktorze i wskażesz parametr przez
nameof. - Wymienisz trzy miejsca, w których pole dostaje wartość, i ustawisz je w kolejności.
- Wyjaśnisz, dlaczego inicjalizator obiektu omija sprawdzenia z konstruktora.
- Napiszesz konstruktor kopiujący prowadzący przez konstruktor główny.
- Odróżnisz finalizator od
Dispose()i powiesz, którego używa się w praktyce.
Przygotowanie: lekcje 01–39. Przewidywany czas: 90 min z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.
Obiekt, który przez chwilę jest niegotowy
W lekcjach 37–39 obiekt powstawał pusty, a dane wkładaliśmy do niego po utworzeniu:
Bilet b = new Bilet();
b.Wydarzenie = "Turniej retro";
b.Cena = 20.50m;
b.Miejsca = 2;
b.Pokaz();
Trzy wiersze przypisań po każdym new. Działa, dopóki nikt nie zapomni ani jednego z nich — a zapomni, bo nic go nie pilnuje:
Bilet c = new Bilet();
c.Wydarzenie = "Noc robotow";
// zapomniana cena i liczba miejsc
c.Pokaz(); // Noc robotow, 0,00 zl, miejsc 0
Bilet za zero złotych na zero miejsc przeszedł bez żadnego ostrzeżenia. I to jest dokładnie ten rodzaj błędu, który wychodzi dopiero przy rozliczeniu kasy.
| Problem | Na czym polega |
|---|---|
| Można zapomnieć o polu | kompilator nie wie, które pola są obowiązkowe |
| Te same przypisania w wielu miejscach | każde new trzeba uzupełnić ręcznie |
| Obiekt przez chwilę jest niegotowy | między new a ostatnim przypisaniem ma bezsensowne dane |
| Nie ma kto sprawdzić wartości | pustą nazwę i zero miejsc trzeba wyłapać osobno przy każdym użyciu |
Czego szukamy
Potrzebujemy sposobu, żeby powiedzieć: „bilet nie może istnieć bez nazwy wydarzenia i liczby miejsc” — i żeby pilnował tego kompilator, a nie czujność programisty. W C# służy do tego konstruktor.
Nazwa klasy, brak typu wyniku i this
Konstruktor to metoda uruchamiana automatycznie przy tworzeniu obiektu. Rozpoznasz go po dwóch cechach, których nie ma żadna inna metoda:
- nazywa się dokładnie tak jak klasa;
- nie ma typu wyniku — nawet
voidsię nie pisze.
class Bilet
{
public readonly string Wydarzenie;
public readonly decimal Cena;
public readonly int Miejsca;
public Bilet(string wydarzenie, decimal cena, int miejsca) // bez void!
{
Wydarzenie = wydarzenie;
Cena = cena;
Miejsca = miejsca;
}
public void Pokaz()
{
Console.WriteLine($"{Wydarzenie}: {Cena:F2} zl, miejsc {Miejsca}");
}
}
Bilet b = new Bilet("Turniej retro", 20.50m, 2);
b.Pokaz();
// Bilet c = new Bilet(); // BLAD CS1729 - ta klasa wymaga argumentow
// b.Wydarzenie = "cos innego"; // BLAD CS0191 - pole jest readonly
Turniej retro: 20,50 zl, miejsc 2
Trzy wiersze przypisań zniknęły z Main. Co ważniejsze — nie da się już utworzyć biletu bez tych danych: zapis new Bilet() nie przechodzi kompilacji.
Pola są readonly (lekcja 38), więc po utworzeniu nikt ich nie podmieni. Konstruktor jest jedynym miejscem, w którym wolno im nadać wartość.
Konstruktor nie zwraca obiektu
Wygląda to tak, jakby new Bilet(...) wołało metodę, która oddaje obiekt — ale nie o to chodzi. Obiekt tworzy słowo new; konstruktor dostaje go już gotowego i tylko wypełnia. Dlatego nie ma w nim return z wartością i nie ma typu wyniku.
Słowo this przy polu o tej samej nazwie
Przyjęło się nazywać parametr tak samo jak pole, tylko małą literą. Gdy nazwy są identyczne, parametr zasłania pole i trzeba to rozróżnić:
class Paczka
{
private int masa;
public Paczka(int masa)
{
masa = masa; // BEZ SKUTKU - przypisanie parametru do siebie
this.masa = masa; // DOBRZE - this.masa to pole tego obiektu
}
}
Pierwszy wiersz kompiluje się i nic nie robi; kompilator wypisze tylko ostrzeżenie o przypisaniu zmiennej do niej samej. this znaczy „bieżący obiekt”, więc this.masa wskazuje jednoznacznie pole, a nie parametr.
this ma w tej lekcji dwa różne znaczenia
this.masa — kropka — to dostęp do pola bieżącego obiektu.
: this(...) — nawiasy po dwukropku — to wywołanie innego konstruktora tej samej klasy, które poznasz w następnej sekcji.
To dwie niezależne rzeczy zapisane tym samym słowem. Nie da się ich pomylić, jeśli patrzysz na znak, który stoi po this.
Prezent, który przepada
Do lekcji 39 pisaliśmy new Bilet() i działało, choć żadnego konstruktora nie było. To nie przypadek: klasa bez ani jednego konstruktora dostaje od kompilatora konstruktor bezparametrowy, który nic nie robi.
Ale ten prezent przepada w chwili, gdy napiszesz własny konstruktor:
| Co jest w klasie | new Klasa() | new Klasa(5) |
|---|---|---|
| ani jednego konstruktora | działa — kompilator dopisał domyślny | CS1729 |
tylko Klasa(int x) | CS1729 — domyślny zniknął | działa |
Klasa() i Klasa(int x) | działa | działa |
class Pusta
{
}
// new Pusta() - dziala, bo kompilator dopisal konstruktor bezparametrowy
class Paczka
{
public readonly int Masa;
public Paczka(int masa)
{
Masa = masa;
}
}
// new Paczka(3) - dziala
// new Paczka() - BLAD CS1729: nie ma konstruktora bez argumentow
CS1729 — najczęstszy błąd tej lekcji
„Klasa Paczka nie zawiera konstruktora, który przyjmuje 0 argumentów” pojawia się zwykle w kodzie, który działał wcześniej. Dopisałeś konstruktor z parametrami, a w innym miejscu programu zostało gdzieś new Paczka(). Lekarstwo: albo dopisz konstruktor bezparametrowy, albo popraw wszystkie wywołania. Zwykle lepsze jest drugie — skoro klasa wymaga danych, to dobrze, że bez nich nie da się jej utworzyć.
Ten sam komunikat zobaczysz w lekcji 45, gdy klasa pochodna zapomni wywołać konstruktor bazowy — przyczyna jest dokładnie ta sama.
Przeciążanie i : this(…)
Klasa może mieć kilka konstruktorów, o ile różnią się listą parametrów. To przeciążanie, które znasz z lekcji 20 — tu działa identycznie:
class Bilet
{
public readonly string Wydarzenie;
public readonly decimal Cena;
public readonly int Miejsca;
public Bilet(string wydarzenie, decimal cena, int miejsca)
{
Wydarzenie = wydarzenie;
Cena = cena;
Miejsca = miejsca;
}
public Bilet(string wydarzenie) // skrot: cena i miejsca domyslne
{
Wydarzenie = wydarzenie;
Cena = 20m;
Miejsca = 1;
}
}
Działa, ale przypisania są przepisane dwa razy. Gdy dojdzie sprawdzanie nazwy, trzeba będzie dopisać je w obu miejscach — i w jednym się o tym zapomni. Dlatego konstruktor może przekazać pracę innemu konstruktorowi tej samej klasy:
public Bilet(string wydarzenie, decimal cena, int miejsca)
{
Wydarzenie = wydarzenie;
Cena = cena;
Miejsca = miejsca;
}
public Bilet(string wydarzenie) : this(wydarzenie, 20m, 1)
{
// ciało może być puste — cała robota jest w konstruktorze głównym
}
Teraz przypisania są zapisane raz. Kolejność wykonania jest ustalona i warto ją zobaczyć:
Bilet b = new Bilet("Noc robotow");
class Bilet
{
public Bilet(string wydarzenie, decimal cena, int miejsca)
{
Console.WriteLine("1. konstruktor glowny");
}
public Bilet(string wydarzenie) : this(wydarzenie, 20m, 1)
{
Console.WriteLine("2. konstruktor skrocony");
}
}
1. konstruktor glowny
2. konstruktor skrocony
Najpierw wykonuje się konstruktor wskazany po : this(...), dopiero potem ciało tego, który go wywołał. Tak samo jak przy : base(...) w lekcji 45 — tam wskazywana jest klasa bazowa, tu inny konstruktor tej samej klasy.
| Zapis | Znaczenie | Gdzie poznasz szczegóły |
|---|---|---|
: this(a, b) | wywołaj inny konstruktor tej samej klasy | ta lekcja |
: base(a, b) | wywołaj konstruktor klasy bazowej | lekcja 45 |
this.pole | pole bieżącego obiektu | poprzednia sekcja |
static Klasa() | konstruktor całego typu, nie obiektu | lekcja 44 |
Łańcuch nie może się zapętlić
Bilet() : this(1) oraz Bilet(int x) : this() to błąd CS0768 — konstruktory wołałyby się bez końca. Ustal jeden konstruktor główny, w którym jest cała robota, i niech wszystkie pozostałe prowadzą do niego.
Reguła, której nie da się ominąć
Teraz to, co jest największym zyskiem z konstruktora. Skoro jest jedyną drogą do utworzenia obiektu, to jest też jedynym miejscem, w którym trzeba sprawdzić dane — a sprawdzenia nie da się ominąć.
class Bilet
{
public readonly string Wydarzenie;
public readonly decimal Cena;
public readonly int Miejsca;
public Bilet(string wydarzenie, decimal cena, int miejsca)
{
if (string.IsNullOrWhiteSpace(wydarzenie))
{
throw new ArgumentException("nazwa wydarzenia nie moze byc pusta",
nameof(wydarzenie));
}
if (miejsca < 1)
{
throw new ArgumentOutOfRangeException(nameof(miejsca),
"musi byc co najmniej jedno miejsce");
}
if (cena < 0m)
{
throw new ArgumentOutOfRangeException(nameof(cena),
"cena nie moze byc ujemna");
}
Wydarzenie = wydarzenie.Trim();
Cena = cena;
Miejsca = miejsca;
}
}
Sprawdzenia stoją przed przypisaniami — nie ma sensu wypełniać obiektu, który zaraz zostanie odrzucony. Wyjątki i słowo throw poznałeś w lekcji 35; nameof(wydarzenie) wstawia nazwę parametru jako napis, dzięki czemu komunikat wskazuje, co dokładnie było nie tak.
try
{
Bilet zly = new Bilet("", 10m, 1);
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
nazwa wydarzenia nie moze byc pusta (Parameter 'wydarzenie')
Nawias z nazwą parametru dopisuje .NET sam — to właśnie zasługa nameof. Przy pięciu parametrach taka wskazówka oszczędza kwadrans szukania.
Dwa rodzaje sprawdzania i obie są potrzebne
Walidacja z lekcji 34 pytała użytkownika w pętli, dopóki nie podał poprawnej wartości — to zadanie interfejsu programu. Sprawdzenie w konstruktorze to zadanie modelu: pilnuje reguły także wtedy, gdy dane przyjdą z pliku, z sieci albo z innej części programu, która o pętli nic nie wie. Jedno nie zastępuje drugiego.
Czego do konstruktora nie wkładamy
| Czynność | Do konstruktora? | Dlaczego |
|---|---|---|
| przypisanie pól | tak | to jego jedyne zadanie |
| sprawdzenie poprawności danych | tak | obiekt nie powinien powstać z błędnymi danymi |
| nadanie kolejnego numeru | tak | wzorzec z lekcji 44 — tu jeszcze go nie używamy |
pytanie użytkownika przez Console.ReadLine | nie | klasa przestaje działać poza konsolą |
| odczyt pliku, połączenie z siecią | nie | konstruktor nie powinien się nie udawać z powodów zewnętrznych |
| wypisywanie na ekran | nie | poza tą lekcją, gdzie służy do pokazania kolejności |
| długie obliczenia | nie | od tego są metody wywoływane świadomie |
Gdzie pole dostaje wartość i w jakiej kolejności
Pole może dostać wartość w trzech różnych miejscach, a kolejność między nimi jest ustalona. Dopóki jej nie znasz, część wyników wygląda na przypadkowe.
Zgloszenie z = new Zgloszenie("Kasia") { Uwagi = "pilne" };
Console.WriteLine(z.Uwagi);
class Zgloszenie
{
public readonly string Imie;
public string Uwagi = "brak"; // 1. INICJALIZATOR POLA
public Zgloszenie(string imie)
{
Imie = imie;
Console.WriteLine($"w konstruktorze Uwagi = {Uwagi}"); // 2. CIALO KONSTRUKTORA
}
}
w konstruktorze Uwagi = brak
pilne
Wynik pokazuje całą kolejność. W chwili wykonywania konstruktora Uwagi ma już wartość "brak" z inicjalizatora pola, a tekst "pilne" trafia tam po zakończeniu konstruktora — bo klamry po new to inicjalizator obiektu, czyli skrót na przypisania wykonywane już na gotowym obiekcie.
| Kolejność | Miejsce | Zapis | Kiedy stosować |
|---|---|---|---|
| 1 | inicjalizator pola | public string Uwagi = "brak"; | wartość domyślna, taka sama dla wszystkich obiektów |
| 2 | ciało konstruktora | Imie = imie; | wartość zależna od argumentów |
| 3 | inicjalizator obiektu | new Zgloszenie("Kasia") { Uwagi = "pilne" } | wypełnienie pól nieobowiązkowych przy tworzeniu |
Po co inicjalizator pola, skoro jest konstruktor
Przy kilku konstruktorach wartość domyślna zapisana przy polu obowiązuje dla wszystkich — nie trzeba jej powtarzać w każdym. Dlatego pola obowiązkowe ustawia konstruktor, a pola z sensowną wartością startową dostają inicjalizator przy deklaracji.
Inicjalizator obiektu nie przechodzi przez sprawdzenia
Klamry po new wykonują zwykłe przypisania po konstruktorze, więc omijają całą walidację z poprzedniej sekcji. Dlatego pola, których reguł trzeba pilnować, deklaruj jako readonly i ustawiaj wyłącznie w konstruktorze — wtedy inicjalizator obiektu ich nie dotknie, bo kompilator na to nie pozwoli (CS0191). Narzędzie, które daje jedno i drugie naraz — kontrolę i zapis przy tworzeniu — to init z lekcji 41.
Nowy obiekt z istniejącego — i co z destruktorem
Czasem nowy obiekt ma powstać na podstawie istniejącego: duplikat biletu dla opiekuna, kopia zgłoszenia do poprawy. W C# nie ma na to nic gotowego — konstruktor przyjmujący obiekt własnej klasy pisze się samemu:
class Bilet
{
public readonly string Wydarzenie;
public readonly decimal Cena;
public readonly int Miejsca;
public string Uwagi = "brak";
public Bilet(string wydarzenie, decimal cena, int miejsca)
{
Wydarzenie = wydarzenie;
Cena = cena;
Miejsca = miejsca;
}
public Bilet(Bilet inny) : this(inny.Wydarzenie, inny.Cena, inny.Miejsca)
{
Uwagi = inny.Uwagi;
}
}
Bilet a = new Bilet("Turniej retro", 20.50m, 2);
a.Uwagi = "rzad pierwszy";
Bilet b = new Bilet(a); // kopia
b.Uwagi = "duplikat"; // zmiana kopii NIE dotyka oryginalu
Console.WriteLine(a.Uwagi); // rzad pierwszy
Console.WriteLine(b.Uwagi); // duplikat
Konstruktor kopiujący wywołuje główny przez : this(...), więc kopia przechodzi przez te same sprawdzenia co nowy obiekt. To nie drobiazg: kopia obiektu wczytanego z pliku nie stanie się nagle obiektem niepoprawnym.
Co się dzieje, gdy polem jest lista
Przypisanie Uczestnicy = inny.Uczestnicy; dla pola typu List<string> nie kopiuje listy — obie klasy wskazują wtedy tę samą listę i dodanie elementu w kopii widać w oryginale. Żeby kopia była niezależna, trzeba utworzyć nową listę: Uczestnicy = new List<string>(inny.Uczestnicy);. To rozróżnienie — kopia płytka i głęboka — jest tematem lekcji 43; tutaj wystarczy wiedzieć, że przy polach będących obiektami samo przypisanie nie wystarcza.
A destruktor?
Podstawa programowa wymienia destruktor obok konstruktora. W C# odpowiada mu finalizator, zapisywany ~Bilet(), ale w praktyce prawie się go nie pisze: pamięć zwalnia automatycznie odśmiecacz, a momentu wykonania finalizatora nie da się przewidzieć.
class Bilet
{
~Bilet() // finalizator - odpowiednik destruktora
{
// wykona sie kiedys, gdy odsmiecacz uzna to za stosowne
}
}
Gdy obiekt trzyma zasób, który trzeba zwolnić w konkretnej chwili — otwarty plik, połączenie — używa się metody Dispose() i bloku using z lekcji 36. Umowa IDisposable, która za tym stoi, to temat lekcji 48.
| Mechanizm | Kiedy się wykonuje | Do czego |
|---|---|---|
| konstruktor | przy new, zawsze | przygotowanie obiektu |
finalizator ~Klasa() | kiedyś, bez gwarancji | ostatnia deska ratunku; prawie nigdy go nie pisz |
Dispose() + using | na końcu bloku using, pewnie | zwalnianie plików i połączeń (lekcje 36 i 48) |
Kasa biletowa
Kasa biletowa: trzy konstruktory prowadzące do jednego, sprawdzanie danych, inicjalizator pola i inicjalizator obiektu. Wypisywanie w konstruktorach jest tu wyłącznie po to, żeby pokazać kolejność — w prawdziwej klasie by go nie było.
// Program.cs - pelny plik
Console.WriteLine("=== tworzenie ===");
Bilet a = new Bilet("Turniej retro", 20.50m, 2);
Bilet b = new Bilet("Noc robotow");
Bilet c = new Bilet(a);
c.Uwagi = "duplikat dla opiekuna";
Console.WriteLine();
Console.WriteLine("=== trzy bilety ===");
a.Pokaz();
b.Pokaz();
c.Pokaz();
Console.WriteLine();
Console.WriteLine("=== inicjalizator obiektu ===");
Bilet d = new Bilet("Pokaz laserow", 15m, 1) { Uwagi = "wejscie od tylu" };
d.Pokaz();
Console.WriteLine();
Console.WriteLine("=== dane, ktorych konstruktor nie przyjmie ===");
try
{
Bilet zly = new Bilet(" ", 10m, 1);
}
catch (ArgumentException ex)
{
Console.WriteLine($"odrzucono: {ex.Message}");
}
try
{
Bilet zly = new Bilet("Koncert", 10m, 0);
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine($"odrzucono parametr: {ex.ParamName}");
}
Console.WriteLine();
List<Bilet> sprzedane = new List<Bilet> { a, b, c, d };
Console.WriteLine($"Biletow na liscie: {sprzedane.Count}");
class Bilet
{
public readonly string Wydarzenie;
public readonly decimal Cena;
public readonly int Miejsca;
public string Uwagi = "brak"; // inicjalizator pola
public Bilet(string wydarzenie, decimal cena, int miejsca)
{
if (string.IsNullOrWhiteSpace(wydarzenie))
{
throw new ArgumentException("nazwa wydarzenia nie moze byc pusta",
nameof(wydarzenie));
}
if (miejsca < 1)
{
throw new ArgumentOutOfRangeException(nameof(miejsca),
"musi byc co najmniej jedno miejsce");
}
if (cena < 0m)
{
throw new ArgumentOutOfRangeException(nameof(cena),
"cena nie moze byc ujemna");
}
Wydarzenie = wydarzenie.Trim();
Cena = cena;
Miejsca = miejsca;
Console.WriteLine($" [glowny] {Wydarzenie}");
}
public Bilet(string wydarzenie) : this(wydarzenie, 20m, 1)
{
Console.WriteLine(" [skrocony]");
}
public Bilet(Bilet inny) : this(inny.Wydarzenie, inny.Cena, inny.Miejsca)
{
Uwagi = inny.Uwagi;
Console.WriteLine(" [kopiujacy]");
}
public void Pokaz()
{
Console.WriteLine($"{Wydarzenie,-16} {Cena,7:F2} zl miejsc {Miejsca} uwagi: {Uwagi}");
}
}
=== tworzenie ===
[glowny] Turniej retro
[glowny] Noc robotow
[skrocony]
[glowny] Turniej retro
[kopiujacy]
=== trzy bilety ===
Turniej retro 20,50 zl miejsc 2 uwagi: brak
Noc robotow 20,00 zl miejsc 1 uwagi: brak
Turniej retro 20,50 zl miejsc 2 uwagi: duplikat dla opiekuna
=== inicjalizator obiektu ===
[glowny] Pokaz laserow
Pokaz laserow 15,00 zl miejsc 1 uwagi: wejscie od tylu
=== dane, ktorych konstruktor nie przyjmie ===
odrzucono: nazwa wydarzenia nie moze byc pusta (Parameter 'wydarzenie')
odrzucono parametr: miejsca
Biletow na liscie: 4
Co dzieje się po kolei
- Pierwszy blok wypisuje pięć wierszy dla trzech obiektów — bo konstruktor skrócony i kopiujący najpierw przekazują pracę głównemu przez
: this(...), a dopiero potem wykonują własne ciało. new Bilet("Noc robotow")trafia do konstruktora z jednym parametrem, ten woła główny z ceną 20 zł i jednym miejscem — stąd wiersz[glowny]przed[skrocony].- Kopia
cma te same dane coa, bo konstruktor kopiujący przekazał je do głównego. Zmianac.Uwaginie dotknęłaa— to dwa osobne obiekty. Uwagima wartość"brak"u biletówaib, bo tak ustawia inicjalizator pola. Nikt tam tej wartości nie przypisywał.- Bilet
ddostaje uwagi przez inicjalizator obiektu — klamry ponew. Zwróć uwagę, że wiersz[glowny]pojawia się przed wypisaniem uwag, bo klamry wykonują się po konstruktorze. - Pusta nazwa to
" "— trzy spacje.IsNullOrWhiteSpaceje wyłapuje, aIsNullOrEmptyby przepuścił (lekcja 34). - Komunikat kończy się dopiskiem
(Parameter 'wydarzenie'), którego nie ma w naszym tekście — dopisał go .NET dziękinameof. - Lista ma cztery pozycje, choć prób utworzenia biletu było sześć. Dwa odrzucone obiekty nigdy nie powstały — wyjątek przerwał konstruktor, więc do
Addnie doszło. Tak działa obiekt, którego nie da się utworzyć z błędnymi danymi.
Sprawdź cztery rzeczy
- Przenieś trzy sprawdzenia na koniec konstruktora głównego, za przypisania. Program zacznie wypisywać wiersz
[glowny]także dla biletów, które zaraz zostaną odrzucone — a przy liczniku z lekcji 44 zwiększyłby się on dla obiektów, które nie powstały. To pokazuje, dlaczego walidacja idzie pierwsza. - Dopisz
Bilet e = new Bilet();. Dostaniesz CS1729 — konstruktor bezparametrowy zniknął, gdy pojawił się pierwszy własny. - Spróbuj dopisać
a.Cena = 5m;. Dostaniesz CS0191, bo pole jestreadonly— i właśnie dlatego inicjalizator obiektu nie może obejść sprawdzeń dla ceny, a dlaUwagimoże. - W konstruktorze kopiującym usuń
: this(...)i przypisz pola wprost. Program się nie skompiluje — polareadonlywolno ustawiać tylko w konstruktorze tej klasy, ale za to zobaczysz, że bez łańcucha trzeba by powtórzyć wszystkie trzy sprawdzenia.
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
| konstruktor | metoda uruchamiana przy new | Nazwa dokładnie jak klasa, bez typu wyniku. |
| konstruktor domyślny | dopisywany przez kompilator | Znika po napisaniu choćby jednego własnego. |
| przeciążone konstruktory | kilka wersji o różnych parametrach | Tak jak przeciążanie metod z lekcji 20. |
: this(...) | wywołanie innego konstruktora tej klasy | Wykonuje się przed ciałem wywołującego. |
this.pole | pole bieżącego obiektu | Konieczne, gdy parametr ma tę samą nazwę. |
readonly przy polu | zapis tylko w konstruktorze | Chroni też przed inicjalizatorem obiektu (CS0191). |
| inicjalizator pola | public string Uwagi = "brak"; | Wykonuje się jako pierwszy, przed ciałem konstruktora. |
| inicjalizator obiektu | new Klasa(...) { Pole = x } | Zwykłe przypisania po konstruktorze — omija walidację. |
throw w konstruktorze | odmowa utworzenia obiektu | Wyjątki z lekcji 35. Sprawdzenia przed przypisaniami. |
nameof(parametr) | nazwa parametru jako napis | .NET dopisuje ją do komunikatu wyjątku. |
| konstruktor kopiujący | public Bilet(Bilet inny) | W C# piszesz go sam; przeprowadź go przez : this(...). |
finalizator ~Klasa() | odpowiednik destruktora | Moment wykonania nieprzewidywalny; prawie nigdy nie jest potrzebny. |
| CS1729 | brak konstruktora bez argumentów | Najczęstszy błąd tej lekcji. |
| CS0191 | próba zmiany pola readonly | Poza konstruktorem tej klasy nie wolno. |
| CS0768 | zapętlony łańcuch : this(...) | Jeden konstruktor główny, reszta prowadzi do niego. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
Konstruktor z typem wyniku, np. public void Bilet(...) | To nie konstruktor, a zwykła metoda o nazwie klasy. Kompilator zgłosi CS0542, bo metoda nie może nazywać się jak klasa. |
new Klasa() po dopisaniu własnego konstruktora | CS1729. Domyślny konstruktor zniknął. Napisz go sam albo popraw wywołania. |
nazwa = nazwa; przy parametrze o nazwie pola | Przypisanie parametru do siebie — pole zostaje puste. Potrzebne this.nazwa = nazwa; |
| Te same przypisania w każdym konstruktorze | Przy pierwszej zmianie reguły jedno miejsce zostanie pominięte. Użyj : this(...). |
Zapętlony łańcuch : this(...) | CS0768. Ustal jeden konstruktor główny. |
| Sprawdzenia po przypisaniach | Obiekt zostaje częściowo wypełniony, a licznik czy numer już się zwiększył. Walidacja idzie pierwsza. |
Pola obowiązkowe bez readonly | Inicjalizator obiektu i każda metoda mogą je podmienić, omijając walidację z konstruktora. |
| Liczenie na inicjalizator obiektu jako kontrolę | Klamry po new to zwykłe przypisania wykonywane po konstruktorze — żadnego sprawdzenia tam nie ma. |
Console.ReadLine() w konstruktorze | Klasa przestaje się nadawać do czegokolwiek poza konsolą i nie da się jej przetestować. Dane przekaż parametrem. |
| Odczyt pliku albo sieci w konstruktorze | Konstruktor, który może się nie udać z powodów zewnętrznych, jest trudny w użyciu. Zrób osobną metodę. |
| Konstruktor kopiujący przypisujący pola wprost | Pomija walidację i powtarza kod. Przeprowadź go przez konstruktor główny. |
| Kopiowanie pola będącego listą przez przypisanie | Oba obiekty dzielą tę samą listę. Utwórz nową: new List<T>(inny.Lista) — szerzej w lekcji 43. |
| Pisanie finalizatora „na wszelki wypadek” | Opóźnia zwalnianie pamięci i prawie nigdy nie jest potrzebny. Do zasobów służy Dispose() i using. |
Zadania
Napisz klasę Plecak z polem readonly int Pojemnosc i metodą Pokaz(). Zrób dwa konstruktory: główny przyjmujący pojemność, który odrzuca wartości poza zakresem od 1 do 40 wyjątkiem ArgumentOutOfRangeException z nameof, oraz bezparametrowy ustawiający 20 przez : this(20). W programie utwórz plecaki o pojemności 1, 40 i jeden bezparametrowy, a próby utworzenia dla 0 i 41 obejmij blokiem try i wypisz komunikaty. Zapisz w komentarzu, dlaczego konstruktor bezparametrowy musi tu być napisany jawnie.
Napisz klasę Bilet z polem readonly string Wydarzenie i kontrolą nazwy w konstruktorze: null, pusty napis i same spacje mają być odrzucone. Zrób to na dwa sposoby, w dwóch wersjach klasy — raz zgłaszając ArgumentException z nameof(wydarzenie), raz podstawiając nazwę zastępczą "bez nazwy" i wypisując ostrzeżenie. Utwórz po trzy obiekty każdą drogą, w tym jeden z nazwą " ". W komentarzu napisz, która droga jest właściwa dla konstruktora i dlaczego — podpowiedź: zastanów się, co zobaczy programista, który użyje Twojej klasy za pół roku.
Napisz klasę Notatka bez żadnego konstruktora, utwórz obiekt przez new Notatka() i sprawdź, że działa. Następnie dopisz konstruktor przyjmujący string i nie usuwaj poprzedniego wywołania. Zapisz w komentarzu numer i pełną treść błędu oraz wiersz, w którym wystąpił. Potem napraw program na dwa sposoby i oba zostaw w pliku, jeden zakomentowany: raz dopisując własny konstruktor bezparametrowy, raz poprawiając wywołanie. W komentarzu napisz, który sposób wybrałbyś dla klasy, w której treść notatki jest obowiązkowa.
Napisz klasę Zgloszenie z trzema konstruktorami: głównym przyjmującym imię i klasę, skróconym przyjmującym samo imię i wywołującym główny przez : this(imie, "nieprzypisana"), oraz kopiującym przyjmującym inne zgłoszenie i również prowadzącym do głównego. W każdym konstruktorze wypisz komunikat z jego nazwą. Dodaj też pole public string Uwagi = "brak"; z inicjalizatorem i wypisz jego wartość wewnątrz konstruktora głównego. Utwórz obiekt każdą z trzech dróg, a jeden dodatkowo z inicjalizatorem obiektu ustawiającym Uwagi. Zapisz w komentarzu pełną kolejność wypisanych wierszy i wyjaśnij, dlaczego wartość Uwagi widziana w konstruktorze różni się od tej, którą ma obiekt po utworzeniu.
Napisz klasę Zespol z polami readonly string Nazwa oraz public List<string> Czlonkowie, konstruktorem głównym sprawdzającym, że nazwa nie jest pusta, i metodą Pokaz() wypisującą nazwę oraz członków. Napisz konstruktor kopiujący w dwóch wersjach: pierwsza przypisuje listę wprost (Czlonkowie = inny.Czlonkowie;), druga tworzy nową listę (new List<string>(inny.Czlonkowie)). Dla każdej wersji utwórz kopię, dodaj do kopii jedną osobę i wypisz oryginał oraz kopię. Zapisz w komentarzu, która wersja zmieniła oryginał i dlaczego. Na koniec sprawdź, czy Twój konstruktor kopiujący przechodzi przez sprawdzenie nazwy — jeśli nie, przepisz go tak, żeby wywoływał konstruktor główny przez : this(...).
Napisz klasę Kurs reprezentującą zapis na kurs. Pola: readonly string Nazwa, readonly int Limit oraz public string Opis = "bez opisu"; z inicjalizatorem. Konstruktor główny przyjmuje nazwę i limit; odrzuca nazwę pustą albo samą z białych znaków (ArgumentException z nameof) i limit poza zakresem 5–30 (ArgumentOutOfRangeException), a nazwę zapisuje po Trim(). Dopisz konstruktor skrócony przyjmujący samą nazwę i ustawiający limit 20 przez : this(...) oraz konstruktor kopiujący, również prowadzący do głównego i przenoszący Opis. Dodaj metodę Pokaz() wypisującą wszystkie dane w wyrównanych kolumnach. W programie: utwórz po jednym obiekcie każdą z trzech dróg oraz jeden z inicjalizatorem obiektu ustawiającym Opis; podejmij cztery próby utworzenia obiektu z danymi błędnymi (nazwa pusta, nazwa z samych spacji, limit 4, limit 31) i każdą obsłuż blokiem catch, wypisując nazwę parametru z ex.ParamName; każdy udany obiekt dodawaj do List<Kurs>, a na końcu wypisz całą listę i jej Count. Sprawdź, że lista ma dokładnie tyle pozycji, ile obiektów naprawdę powstało — odrzucone próby nie mogą się w niej znaleźć; jeśli liczba się nie zgadza, sprawdź, czy sprawdzenia w konstruktorze stoją przed przypisaniami. Na końcu pliku odpowiedz w komentarzu na trzy pytania: dlaczego Nazwa i Limit są readonly, a Opis nie; dlaczego inicjalizator obiektu mógł ustawić Opis, ale nie mógłby ustawić Limit; oraz co by się stało, gdyby konstruktor kopiujący przypisywał pola wprost, zamiast wołać główny.
Co trzeba zapamiętać
- Konstruktor nazywa się dokładnie jak klasa i nie ma typu wyniku — nawet
void. - Obiekt tworzy
new; konstruktor go tylko wypełnia, dlatego nie zwraca wartości. - Klasa bez własnego konstruktora dostaje bezparametrowy od kompilatora — i traci go przy pierwszym własnym (CS1729).
this.pole = pole;rozróżnia pole od parametru o tej samej nazwie; bezthisprzypisanie nic nie robi.: this(...)przekazuje inicjalizację innemu konstruktorowi tej samej klasy i wykonuje się przed ciałem wywołującego.- Jeden konstruktor główny z całą robotą, pozostałe prowadzą do niego — inaczej reguły trzeba powtarzać.
- Konstruktor jest jedyną drogą do obiektu, więc sprawdzenia postawione w nim są nie do ominięcia.
- Sprawdzenia idą przed przypisaniami i przed licznikiem — inaczej odrzucony obiekt zostawi po sobie ślad.
nameof(parametr)wstawia nazwę parametru do komunikatu wyjątku.- Pole dostaje wartość w trzech miejscach, zawsze w tej kolejności: inicjalizator pola, ciało konstruktora, inicjalizator obiektu.
- Klamry po
newwykonują się po konstruktorze, więc omijają jego walidację; pola z regułami róbreadonly. - Konstruktor kopiujący pisze się samemu i warto przeprowadzić go przez konstruktor główny, żeby kopia też przeszła sprawdzenia.
- Finalizator
~Klasa()to odpowiednik destruktora, ale momentu jego wykonania nie da się przewidzieć — do zasobów służyDispose()iusing.
Dokumentacja: Microsoft Learn — temat tej lekcji.