Przejdź do treści
School IT / Przestrzeń do nauki
← C# od podstawLekcja 41 z 58

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.

C# konstruktor : this(…) readonly nameof walidacja konstruktor kopiujący 90 min
CEL LEKCJI

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 this do 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.

TEORIA

Obiekt, który przez chwilę jest niegotowy

W lekcjach 37–39 obiekt powstawał pusty, a dane wkładaliśmy do niego po utworzeniu:

tak było do tej pory
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:

skutek przeoczenia
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.

ProblemNa czym polega
Można zapomnieć o polukompilator nie wie, które pola są obowiązkowe
Te same przypisania w wielu miejscachkażde new trzeba uzupełnić ręcznie
Obiekt przez chwilę jest niegotowymiędzy new a ostatnim przypisaniem ma bezsensowne dane
Nie ma kto sprawdzić wartościpustą 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.

TEORIA

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 void się nie pisze.
definicja typu
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}");
    }
}
Program.cs
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
wynik w konsoli
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ć:

definicja typu
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.

TEORIA

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 klasienew Klasa()new Klasa(5)
ani jednego konstruktoradziała — kompilator dopisał domyślnyCS1729
tylko Klasa(int x)CS1729 — domyślny zniknąłdziała
Klasa() i Klasa(int x)działadziała
definicje typów
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.

TEORIA

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:

dwa konstruktory — wersja do poprawienia
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:

z : this(…) — wersja poprawna
    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ć:

Program.cs
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");
    }
}
wynik w konsoli
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.

ZapisZnaczenieGdzie poznasz szczegóły
: this(a, b)wywołaj inny konstruktor tej samej klasyta lekcja
: base(a, b)wywołaj konstruktor klasy bazowejlekcja 45
this.polepole bieżącego obiektupoprzednia sekcja
static Klasa()konstruktor całego typu, nie obiektulekcja 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.

TEORIA

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ąć.

definicja typu
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.

Program.cs
try
{
    Bilet zly = new Bilet("", 10m, 1);
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message);
}
wynik w konsoli
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óltakto jego jedyne zadanie
sprawdzenie poprawności danychtakobiekt nie powinien powstać z błędnymi danymi
nadanie kolejnego numerutakwzorzec z lekcji 44 — tu jeszcze go nie używamy
pytanie użytkownika przez Console.ReadLinenieklasa przestaje działać poza konsolą
odczyt pliku, połączenie z sieciąniekonstruktor nie powinien się nie udawać z powodów zewnętrznych
wypisywanie na ekranniepoza tą lekcją, gdzie służy do pokazania kolejności
długie obliczenianieod tego są metody wywoływane świadomie
TEORIA

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.

Program.cs
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
    }
}
wynik w konsoli
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śćMiejsceZapisKiedy stosować
1inicjalizator polapublic string Uwagi = "brak";wartość domyślna, taka sama dla wszystkich obiektów
2ciało konstruktoraImie = imie;wartość zależna od argumentów
3inicjalizator obiektunew 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.

TEORIA

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:

definicja typu
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;
    }
}
Program.cs
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ć.

definicja typu
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.

MechanizmKiedy się wykonujeDo czego
konstruktorprzy new, zawszeprzygotowanie obiektu
finalizator ~Klasa()kiedyś, bez gwarancjiostatnia deska ratunku; prawie nigdy go nie pisz
Dispose() + usingna końcu bloku using, pewniezwalnianie plików i połączeń (lekcje 36 i 48)
PRZYKŁAD

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 — pełny plik
// 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}");
    }
}
wynik w konsoli
=== 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 c ma te same dane co a, bo konstruktor kopiujący przekazał je do głównego. Zmiana c.Uwagi nie dotknęła a — to dwa osobne obiekty.
  • Uwagi ma wartość "brak" u biletów a i b, bo tak ustawia inicjalizator pola. Nikt tam tej wartości nie przypisywał.
  • Bilet d dostaje uwagi przez inicjalizator obiektu — klamry po new. Zwróć uwagę, że wiersz [glowny] pojawia się przed wypisaniem uwag, bo klamry wykonują się po konstruktorze.
  • Pusta nazwa to " " — trzy spacje. IsNullOrWhiteSpace je wyłapuje, a IsNullOrEmpty by przepuścił (lekcja 34).
  • Komunikat kończy się dopiskiem (Parameter 'wydarzenie'), którego nie ma w naszym tekście — dopisał go .NET dzięki nameof.
  • 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 Add nie doszło. Tak działa obiekt, którego nie da się utworzyć z błędnymi danymi.

Sprawdź cztery rzeczy

  1. 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.
  2. Dopisz Bilet e = new Bilet();. Dostaniesz CS1729 — konstruktor bezparametrowy zniknął, gdy pojawił się pierwszy własny.
  3. Spróbuj dopisać a.Cena = 5m;. Dostaniesz CS0191, bo pole jest readonly — i właśnie dlatego inicjalizator obiektu nie może obejść sprawdzeń dla ceny, a dla Uwagi może.
  4. W konstruktorze kopiującym usuń : this(...) i przypisz pola wprost. Program się nie skompiluje — pola readonly wolno ustawiać tylko w konstruktorze tej klasy, ale za to zobaczysz, że bez łańcucha trzeba by powtórzyć wszystkie trzy sprawdzenia.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
konstruktormetoda uruchamiana przy newNazwa dokładnie jak klasa, bez typu wyniku.
konstruktor domyślnydopisywany przez kompilatorZnika po napisaniu choćby jednego własnego.
przeciążone konstruktorykilka wersji o różnych parametrachTak jak przeciążanie metod z lekcji 20.
: this(...)wywołanie innego konstruktora tej klasyWykonuje się przed ciałem wywołującego.
this.polepole bieżącego obiektuKonieczne, gdy parametr ma tę samą nazwę.
readonly przy poluzapis tylko w konstruktorzeChroni też przed inicjalizatorem obiektu (CS0191).
inicjalizator polapublic string Uwagi = "brak";Wykonuje się jako pierwszy, przed ciałem konstruktora.
inicjalizator obiektunew Klasa(...) { Pole = x }Zwykłe przypisania po konstruktorze — omija walidację.
throw w konstruktorzeodmowa utworzenia obiektuWyjątki z lekcji 35. Sprawdzenia przed przypisaniami.
nameof(parametr)nazwa parametru jako napis.NET dopisuje ją do komunikatu wyjątku.
konstruktor kopiującypublic Bilet(Bilet inny)W C# piszesz go sam; przeprowadź go przez : this(...).
finalizator ~Klasa()odpowiednik destruktoraMoment wykonania nieprzewidywalny; prawie nigdy nie jest potrzebny.
CS1729brak konstruktora bez argumentówNajczęstszy błąd tej lekcji.
CS0191próba zmiany pola readonlyPoza konstruktorem tej klasy nie wolno.
CS0768zapętlony łańcuch : this(...)Jeden konstruktor główny, reszta prowadzi do niego.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
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 konstruktoraCS1729. Domyślny konstruktor zniknął. Napisz go sam albo popraw wywołania.
nazwa = nazwa; przy parametrze o nazwie polaPrzypisanie parametru do siebie — pole zostaje puste. Potrzebne this.nazwa = nazwa;
Te same przypisania w każdym konstruktorzePrzy 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 przypisaniachObiekt zostaje częściowo wypełniony, a licznik czy numer już się zwiększył. Walidacja idzie pierwsza.
Pola obowiązkowe bez readonlyInicjalizator 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 konstruktorzeKlasa przestaje się nadawać do czegokolwiek poza konsolą i nie da się jej przetestować. Dane przekaż parametrem.
Odczyt pliku albo sieci w konstruktorzeKonstruktor, 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 wprostPomija walidację i powtarza kod. Przeprowadź go przez konstruktor główny.
Kopiowanie pola będącego listą przez przypisanieOba 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

Zadania

ZAD 1Plecak z kontrolą pojemności★☆☆

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.

ZAD 2Bilet bez pustej nazwy★☆☆

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.

ZAD 3Gdy konstruktor bezparametrowy znika★★☆

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.

ZAD 4Kolejność połączonych konstruktorów★★☆

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.

ZAD 5Konstruktor kopiujący, który nie oszukuje★★☆

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(...).

ZAD 6Trzy drogi do jednego obiektu★★★

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.

PODSUMOWANIE

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; bez this przypisanie 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 new wykonują się po konstruktorze, więc omijają jego walidację; pola z regułami rób readonly.
  • 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ży Dispose() i using.

Dokumentacja: Microsoft Learn — temat tej lekcji.

Postęp zapisuje się w tej przeglądarce.