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.

CEL LEKCJI

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

TEORIA

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.

Fragment C#
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.

Fragment C#
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.

Fragment C#
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.

Fragment C#
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.

PRZYKŁAD

Przewidź → uruchom → zmień

Kompletny, osobny plik Program.cs. Przed uruchomieniem przewidź wynik. Każdy program uruchamiaj w oddzielnym projekcie konsolowym.

Program.cs
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());
    }
}
Wynik przykładu
Pokaz: 1
Roboty: 4
TEORIA

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ę.

TEORIA

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.

definicja typu
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;
    }
}
w Main
Bilet b = new Bilet("Turniej", 12)
{
    Oplacony = true                      // 3. inicjalizator obiektu
};

Console.WriteLine($"{b.Nazwa}, miejsce {b.Miejsce}, oplacony: {b.Oplacony}");
wynik w konsoli
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ść.

definicja typu
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) { }
}
w Main
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.

TEORIA

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.

definicja typu
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;
    }
}
w Main
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

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.

ZAD 1Ćwiczenie 1★☆☆

Zaprojektuj Plecak o pojemności od 1 do 40. Zapewnij konstruktor bezparametrowy oraz z parametrem. Sprawdź 1, 40, 0 i 41.

ZAD 2Ćwiczenie 2★☆☆

Dodaj do Biletu kontrolę nazwy: null, pusty napis i same spacje nie powinny być akceptowane. Opisz sposób zgłoszenia problemu.

ZAD 3Ćwiczenie 3★★☆

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.

ZAD 4Ćwiczenie 4★★☆

Dodaj komunikaty wewnątrz dwóch połączonych konstruktorów i zbadaj kolejność ich wykonania.

ZAD 5Zadanie: trzy drogi do jednego obiektu★★★

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.

PODSUMOWANIE

Sprawdź, czy rozumiesz

Dokumentacja: Microsoft Learn — Konstruktory.