Konstruktory w C# – fabryka obiektów
Nauczysz się tworzyć konstruktory – specjalne metody odpowiedzialne za „budowanie” obiektów. Poznasz przeciążanie, słowo this i konstruktor kopiujący!
Po co konstruktor?
Gdy tworzysz obiekt używając new, chcesz aby od razu miał sensowne wartości. Bez konstruktora musisz ręcznie ustawiać każde pole – to uciążliwe i łatwo o błąd.
// 😰 Bez konstruktora – musisz ręcznie ustawić każde pole public class Samochod { public string Marka; public string Model; public int Rok; public double Przebieg; } // Tworzenie obiektu – pola mają domyślne wartości (null, 0) Samochod auto = new Samochod(); // Musisz pamiętać o ustawieniu KAŻDEGO pola! auto.Marka = "Toyota"; auto.Model = "Corolla"; auto.Rok = 2020; auto.Przebieg = 45000; // Co jeśli zapomnisz o jednym polu? Samochod auto2 = new Samochod(); auto2.Marka = "Ford"; // Ups! Zapomniałem o Model, Rok, Przebieg... // auto2.Model jest null – może spowodować błąd!
- Można zapomnieć – łatwo pominąć jedno z pól
- Pola mają domyślne wartości – string to null, int to 0 (nie zawsze sensowne)
- Duplikacja kodu – za każdym razem te same 4 linie
- Brak kontroli – nie można wymusić podania wartości
Konstruktor rozwiązuje te problemy – wymusza podanie wartości i inicjalizuje obiekt w jednym kroku.
Czym jest konstruktor?
Konstruktor to specjalna metoda wywoływana automatycznie w momencie tworzenia obiektu (new). Jej zadaniem jest przygotowanie obiektu do użycia – ustawienie początkowych wartości pól.
Pomyśl o konstruktorze jak o linii montażowej w fabryce:
- Konstruktor = linia montażowa
- Parametry konstruktora = instrukcje montażu (jaki kolor, jaki silnik)
- new = „uruchom linię produkcyjną”
- Nowy obiekt = gotowy samochód zjeżdżający z taśmy
Gdy zamawiasz samochód, podajesz specyfikację (parametry), a fabryka (konstruktor) produkuje gotowy pojazd!
Inna analogia:
- Konstruktor bezparametrowy = formularz z domyślnymi wartościami
- Konstruktor z parametrami = formularz, który MUSISZ wypełnić
Bank nie założy Ci konta bez podania danych – podobnie konstruktor może wymusić podanie wartości!
Model = „Corolla”
Rok = 2024
Cechy konstruktora
| Cecha | Opis |
|---|---|
| Nazwa | Taka sama jak nazwa klasy |
| Typ zwracany | BRAK! Nie piszemy void ani nic innego |
| Wywołanie | Automatyczne przy new |
| Parametry | Może mieć dowolną liczbę (0, 1, 2, …) |
| Przeciążanie | Może być wiele konstruktorów w klasie |
Podstawowa składnia
Konstruktor definiujemy wewnątrz klasy. Jego nazwa musi być identyczna z nazwą klasy.
public class Samochod { // Pola klasy public string Marka; public string Model; public int Rok; // ↓ modyfikator ↓ nazwa = nazwa klasy (BEZ typu zwracanego!) public Samochod() { // Kod wykonywany przy tworzeniu obiektu Marka = "Nieznana"; Model = "Nieznany"; Rok = 2024; Console.WriteLine("Konstruktor wywołany!"); } } // Użycie – konstruktor wywoła się AUTOMATYCZNIE Samochod auto = new Samochod(); // Wyświetli: "Konstruktor wywołany!" Console.WriteLine(auto.Marka); // "Nieznana" Console.WriteLine(auto.Model); // "Nieznany" Console.WriteLine(auto.Rok); // 2024
❌ To NIE jest konstruktor
// Ma typ zwracany void!
public void Samochod()
{
// To zwykła metoda
// o nazwie "Samochod"
}
✅ To JEST konstruktor
// Brak typu zwracanego!
public Samochod()
{
// To konstruktor
// wywoływany przy new
}
Konstruktor nie ma typu zwracanego – ani void, ani nic innego. Jeśli napiszesz public void Samochod(), to jest zwykła metoda, nie konstruktor!
Konstruktor domyślny (bezparametrowy)
Jeśli nie napiszesz żadnego konstruktora, C# automatycznie tworzy pusty konstruktor domyślny. Ale uwaga – gdy napiszesz własny konstruktor, domyślny znika!
Konstruktor domyślny – automatyczny
public class Osoba { public string Imie; public int Wiek; // Nie napisaliśmy żadnego konstruktora // C# automatycznie dodaje pusty: // public Osoba() { } } // Działa! Używamy automatycznego konstruktora domyślnego Osoba osoba = new Osoba(); // Pola mają domyślne wartości typów: Console.WriteLine(osoba.Imie); // null (domyślna dla string) Console.WriteLine(osoba.Wiek); // 0 (domyślna dla int)
⚠️ PUŁAPKA: Konstruktor domyślny znika!
public class Student { public string Imie; public int Wiek; // Napisaliśmy konstruktor z parametrem public Student(string imie) { Imie = imie; Wiek = 18; } // UWAGA: Konstruktor domyślny ZNIKNĄŁ! } // To DZIAŁA – używamy naszego konstruktora Student s1 = new Student("Jan"); // To NIE DZIAŁA – brak konstruktora bezparametrowego! // Student s2 = new Student(); // ❌ BŁĄD KOMPILACJI! // "There is no argument given that corresponds to the required // formal parameter 'imie'"
Gdy napiszesz jakikolwiek konstruktor z parametrami, konstruktor domyślny (bezparametrowy) automatycznie znika. Jeśli nadal go potrzebujesz, musisz go napisać ręcznie!
Rozwiązanie – dodaj oba konstruktory
public class Student { public string Imie; public int Wiek; // Konstruktor bezparametrowy – ręcznie dodany public Student() { Imie = "Nieznane"; Wiek = 0; } // Konstruktor z parametrem public Student(string imie) { Imie = imie; Wiek = 18; } } // Teraz OBA działają: Student s1 = new Student(); // ✅ OK Student s2 = new Student("Jan"); // ✅ OK
Konstruktor z parametrami
Konstruktor może przyjmować parametry – wartości przekazywane przy tworzeniu obiektu. Pozwala to na inicjalizację pól konkretnymi danymi.
Jeden parametr
public class Ksiazka { public string Tytul; public string Autor; public int Strony; public Ksiazka(string tytul) { Tytul = tytul; Autor = "Nieznany"; // Wartość domyślna Strony = 0; // Wartość domyślna } } // Użycie – MUSISZ podać tytuł Ksiazka k = new Ksiazka("Hobbit"); Console.WriteLine(k.Tytul); // "Hobbit" Console.WriteLine(k.Autor); // "Nieznany"
Wiele parametrów
public class Prostokat { public double Szerokosc; public double Wysokosc; public string Kolor; public Prostokat(double szerokosc, double wysokosc, string kolor) { Szerokosc = szerokosc; Wysokosc = wysokosc; Kolor = kolor; } public double ObliczPole() { return Szerokosc * Wysokosc; } } // Użycie – parametry w ODPOWIEDNIEJ KOLEJNOŚCI! Prostokat p = new Prostokat(10.5, 7.2, "czerwony"); Console.WriteLine($"Prostokąt {p.Kolor}: {p.Szerokosc} x {p.Wysokosc}"); Console.WriteLine($"Pole: {p.ObliczPole()}");
Parametry są przekazywane w kolejności zdefiniowanej w konstruktorze:
new Prostokat(10.5, 7.2, "czerwony");
↓ ↓ ↓
szerokosc wysokosc kolor
Jeśli pomylisz kolejność, program może działać niepoprawnie!
Przeciążanie konstruktorów
Przeciążanie (overloading) oznacza, że klasa może mieć wiele konstruktorów z różnymi parametrami. C# wybiera odpowiedni konstruktor na podstawie przekazanych argumentów.
public class Telefon { public string Marka; public string Model; public decimal Cena; // Konstruktor 1 – bez parametrów public Telefon() { Marka = "Nieznana"; Model = "Nieznany"; Cena = 0; } // Konstruktor 2 – tylko marka public Telefon(string marka) { Marka = marka; Model = "Nieznany"; Cena = 0; } // Konstruktor 3 – marka i model public Telefon(string marka, string model) { Marka = marka; Model = model; Cena = 0; } // Konstruktor 4 – wszystkie parametry public Telefon(string marka, string model, decimal cena) { Marka = marka; Model = model; Cena = cena; } }
// C# automatycznie wybiera odpowiedni konstruktor! Telefon t1 = new Telefon(); // → Konstruktor 1 Telefon t2 = new Telefon("Samsung"); // → Konstruktor 2 Telefon t3 = new Telefon("Apple", "iPhone 15"); // → Konstruktor 3 Telefon t4 = new Telefon("Xiaomi", "Mi 13", 1200.50m); // → Konstruktor 4 Console.WriteLine($"{t1.Marka}"); // "Nieznana" Console.WriteLine($"{t2.Marka}"); // "Samsung" Console.WriteLine($"{t3.Model}"); // "iPhone 15" Console.WriteLine($"{t4.Cena:C}"); // "1 200,50 zł"
Na podstawie liczby i typów przekazanych argumentów:
- 0 argumentów → szuka konstruktora bez parametrów
- 1 string → szuka konstruktora z jednym string
- 2 stringi → szuka konstruktora z dwoma string
Słowo kluczowe this
this to odwołanie do bieżącego obiektu. Używamy go głównie gdy nazwa parametru jest taka sama jak nazwa pola – this rozróżnia je.
Problem: Takie same nazwy
public class Auto { public string marka; // Pole klasy public string model; // Pole klasy // ❌ PROBLEM: parametry mają te same nazwy co pola! public Auto(string marka, string model) { marka = marka; // 🤔 Które "marka"? Parametr = parametr! model = model; // To nic nie robi – przypisuje parametr do siebie } } Auto a = new Auto("Toyota", "Corolla"); Console.WriteLine(a.marka); // null! Pole nie zostało ustawione!
Rozwiązanie: this
public class Auto { public string marka; public string model; public Auto(string marka, string model) { this.marka = marka; // this.marka = POLE, marka = PARAMETR this.model = model; // this.model = POLE, model = PARAMETR } } Auto a = new Auto("Toyota", "Corolla"); Console.WriteLine(a.marka); // "Toyota" ✅
(w obiekcie)
(przekazany)
Możesz też użyć innych nazw parametrów – wtedy this nie jest potrzebne:
public Auto(string nowaMarka, string nowyModel)
{
marka = nowaMarka; // OK – nazwy są różne
model = nowyModel;
}
Ale konwencja z this jest popularna i czytelna.
Wywoływanie innych konstruktorów – this(…)
Konstruktor może wywoływać inny konstruktor tej samej klasy używając : this(...). Pozwala to uniknąć duplikacji kodu.
// 😰 PROBLEM: Duplikacja kodu w konstruktorach public class Punkt { public int X; public int Y; public string Kolor; public Punkt() { X = 0; Y = 0; Kolor = "Czarny"; } public Punkt(int x, int y) { X = x; Y = y; Kolor = "Czarny"; // Powtórzenie! } public Punkt(int x, int y, string kolor) { X = x; // Powtórzenie! Y = y; // Powtórzenie! Kolor = kolor; } }
// 😊 ROZWIĄZANIE: Konstruktory wywołują się nawzajem public class Punkt { public int X; public int Y; public string Kolor; // Konstruktor "główny" – pełna logika public Punkt(int x, int y, string kolor) { X = x; Y = y; Kolor = kolor; } // Wywołuje konstruktor główny z domyślnym kolorem public Punkt(int x, int y) : this(x, y, "Czarny") { // Dodatkowy kod (opcjonalny) } // Wywołuje konstruktor z domyślnymi współrzędnymi public Punkt() : this(0, 0, "Czarny") { // Dodatkowy kod (opcjonalny) } }
Dzięki this(...) logika inicjalizacji jest w jednym miejscu. Jeśli coś zmienisz, zmieniasz tylko konstruktor główny!
Konstruktor kopiujący
Konstruktor kopiujący tworzy nowy obiekt na podstawie istniejącego. W C# nie ma go automatycznie – musisz napisać go sam.
Problem: Kopiowanie referencji
public class Osoba { public string Imie; public int Wiek; public Osoba(string imie, int wiek) { Imie = imie; Wiek = wiek; } } // Tworzę obiekt Osoba osoba1 = new Osoba("Jan", 25); // ❌ TO NIE TWORZY KOPII! Osoba osoba2 = osoba1; // Tylko kopia REFERENCJI! // Zmiana w osoba2 zmienia też osoba1! osoba2.Wiek = 30; Console.WriteLine(osoba1.Wiek); // 30! 😱 To ten sam obiekt! Console.WriteLine(osoba2.Wiek); // 30
Wiek: 30
Rozwiązanie: Konstruktor kopiujący
public class Osoba { public string Imie; public int Wiek; // Konstruktor normalny public Osoba(string imie, int wiek) { Imie = imie; Wiek = wiek; } // KONSTRUKTOR KOPIUJĄCY – parametr to obiekt tego samego typu public Osoba(Osoba inna) { if (inna != null) { this.Imie = inna.Imie; this.Wiek = inna.Wiek; } } } // Tworzę obiekt Osoba osoba1 = new Osoba("Jan", 25); // ✅ Tworzę PRAWDZIWĄ KOPIĘ Osoba osoba2 = new Osoba(osoba1); // Konstruktor kopiujący! // Teraz to są RÓŻNE obiekty osoba2.Wiek = 30; Console.WriteLine(osoba1.Wiek); // 25 ✅ Nie zmieniło się! Console.WriteLine(osoba2.Wiek); // 30
Kopiowanie zagnieżdżonych obiektów
public class Adres { public string Miasto; public string Ulica; public Adres(string miasto, string ulica) { Miasto = miasto; Ulica = ulica; } public Adres(Adres inny) // Konstruktor kopiujący dla Adres { if (inny != null) { Miasto = inny.Miasto; Ulica = inny.Ulica; } } } public class Pracownik { public string Imie; public Adres AdresZamieszkania; // Zagnieżdżony obiekt! public Pracownik(string imie, Adres adres) { Imie = imie; AdresZamieszkania = adres; } public Pracownik(Pracownik inny) { if (inny != null) { Imie = inny.Imie; // WAŻNE! Tworzymy KOPIĘ adresu, nie kopiujemy referencji! if (inny.AdresZamieszkania != null) { AdresZamieszkania = new Adres(inny.AdresZamieszkania); } } } }
- Kopia płytka – kopiujesz pola proste, ale referencje wskazują na te same obiekty
- Kopia głęboka – tworzysz też kopie zagnieżdżonych obiektów (jak powyżej)
Dla obiektów zawierających inne obiekty, zawsze twórz kopię głęboką!
Walidacja w konstruktorze
Konstruktor to idealne miejsce na walidację danych. Możesz sprawdzić, czy przekazane wartości są poprawne i zareagować na błędy.
public class Student { public string Imie; public int Wiek; public double Srednia; public Student(string imie, int wiek, double srednia) { // Walidacja imienia if (string.IsNullOrWhiteSpace(imie)) { throw new ArgumentException("Imię nie może być puste!"); } // Walidacja wieku if (wiek < 16 || wiek > 100) { throw new ArgumentOutOfRangeException("Wiek musi być między 16 a 100!"); } // Walidacja średniej if (srednia < 1.0 || srednia > 6.0) { throw new ArgumentOutOfRangeException("Średnia musi być między 1.0 a 6.0!"); } // Wszystko OK – przypisujemy wartości Imie = imie; Wiek = wiek; Srednia = srednia; } } // Użycie: try { Student s1 = new Student("Jan", 20, 4.5); // ✅ OK Student s2 = new Student("", 20, 4.5); // ❌ Wyjątek! Student s3 = new Student("Anna", 150, 4.0); // ❌ Wyjątek! } catch (Exception ex) { Console.WriteLine($"Błąd: {ex.Message}"); }
Zamiast rzucać wyjątek, możesz ustawić wartość domyślną:
if (wiek < 0)
{
wiek = 0; // Napraw zamiast rzucać wyjątek
}
Wiek = wiek;
Zależy od wymagań – czasem błąd powinien być widoczny!
Praktyczne przykłady
Przykład 1: Konto bankowe
public class KontoBankowe { public string NumerKonta; public string Wlasciciel; public decimal Saldo; // Nowe konto – zawsze zaczyna od 0 zł public KontoBankowe(string numer, string wlasciciel) { NumerKonta = numer; Wlasciciel = wlasciciel; Saldo = 0; } // Konto z początkowym saldem (np. przelew z innego banku) public KontoBankowe(string numer, string wlasciciel, decimal saldo) : this(numer, wlasciciel) { if (saldo >= 0) { Saldo = saldo; } } public void WyswietlInfo() { Console.WriteLine($"Konto: {NumerKonta}"); Console.WriteLine($"Właściciel: {Wlasciciel}"); Console.WriteLine($"Saldo: {Saldo:C}"); } }
Przykład 2: Uczeń z wieloma konstruktorami
public class Uczen { public string Imie; public string Nazwisko; public string Klasa; public double Srednia; // Konstruktor główny public Uczen(string imie, string nazwisko, string klasa, double srednia) { Imie = imie; Nazwisko = nazwisko; Klasa = klasa; Srednia = srednia; } // Nowy uczeń – jeszcze bez ocen public Uczen(string imie, string nazwisko, string klasa) : this(imie, nazwisko, klasa, 0.0) { } // Konstruktor kopiujący public Uczen(Uczen inny) : this(inny.Imie, inny.Nazwisko, inny.Klasa, inny.Srednia) { } public string PelneImie() => $"{Imie} {Nazwisko}"; }
Częste błędy
❌ Błąd 1: Typ zwracany w konstruktorze
❌ Źle
public void Samochod()
{
// To NIE jest konstruktor!
// To zwykła metoda void
}
✅ Dobrze
public Samochod()
{
// To JEST konstruktor
// Brak typu zwracanego!
}
❌ Błąd 2: Inna nazwa niż klasa
❌ Źle
public class Auto
{
public Samochod() // Zła nazwa!
{
}
}
✅ Dobrze
public class Auto
{
public Auto() // Taka sama nazwa!
{
}
}
❌ Błąd 3: Brak konstruktora bezparametrowego
❌ Źle
public class Osoba
{
public Osoba(string imie) { }
}
// Nie działa!
Osoba o = new Osoba();
✅ Dobrze
public class Osoba
{
public Osoba() { } // Dodaj!
public Osoba(string imie) { }
}
// Teraz działa
Osoba o = new Osoba();
❌ Błąd 4: Brak this przy tych samych nazwach
❌ Źle
public class Auto
{
public string marka;
public Auto(string marka)
{
marka = marka; // Nic nie robi!
}
}
✅ Dobrze
public class Auto
{
public string marka;
public Auto(string marka)
{
this.marka = marka; // OK!
}
}
❌ Błąd 5: Płytka kopia zamiast głębokiej
❌ Źle
public Pracownik(Pracownik inny)
{
Imie = inny.Imie;
// Kopia referencji!
Adres = inny.Adres;
}
✅ Dobrze
public Pracownik(Pracownik inny)
{
Imie = inny.Imie;
// Kopia obiektu!
Adres = new Adres(inny.Adres);
}
Podsumowanie
- Konstruktor – specjalna metoda wywoływana przy
new - Nazwa – identyczna z nazwą klasy
- Brak typu zwracanego – nie piszemy void ani nic innego
- Konstruktor domyślny – znika gdy napiszesz własny!
- Przeciążanie – wiele konstruktorów z różnymi parametrami
- this – odwołanie do bieżącego obiektu
- this(...) – wywołanie innego konstruktora
- Konstruktor kopiujący – tworzy kopię obiektu
Rodzaje konstruktorów
| Rodzaj | Opis | Przykład |
|---|---|---|
| Domyślny | Bez parametrów, automatyczny lub ręczny | public Klasa() |
| Z parametrami | Przyjmuje wartości początkowe | public Klasa(string s) |
| Kopiujący | Tworzy kopię istniejącego obiektu | public Klasa(Klasa inna) |
Schemat składni
public class NazwaKlasy { public typ Pole; // Konstruktor bezparametrowy public NazwaKlasy() { Pole = wartoscDomyslna; } // Konstruktor z parametrami public NazwaKlasy(typ parametr) { this.Pole = parametr; } // Konstruktor wywołujący inny public NazwaKlasy(typ a, typ b) : this(a) { // Dodatkowy kod } // Konstruktor kopiujący public NazwaKlasy(NazwaKlasy inny) { this.Pole = inny.Pole; } }
Zadania praktyczne
📝 Zadanie 1: Klasa Film
Utwórz klasę Film z polami: Tytul, Rezyser, RokProdukcji, CzasTrwania (w minutach).
Dodaj konstruktory:
- Bezparametrowy (domyślne wartości)
- Z tytułem i reżyserem
- Z wszystkimi parametrami
Utwórz 3 filmy używając różnych konstruktorów.
💡 Podpowiedź: Użyj this(...) aby uniknąć duplikacji
📝 Zadanie 2: Klasa Komputer
Utwórz klasę Komputer z polami: Procesor, Ram (GB), Dysk (GB), Cena.
Dodaj:
- Konstruktor pełny
- Konstruktor "budżetowy" (tylko procesor, Ram=8, Dysk=256)
- Konstruktor kopiujący
💡 Podpowiedź: Konstruktor budżetowy może wywoływać pełny z this(...)
📝 Zadanie 3: Znajdź błędy
Popraw poniższy kod:
public class Zwierze
{
public string Gatunek;
public int Wiek;
public int Zwierze(string gatunek)
{
Gatunek = gatunek;
return 0;
}
}
💡 Podpowiedź: Jest tu kilka błędów związanych z konstruktorem
⭐ Zadanie 4: Walidacja w konstruktorze
Utwórz klasę Produkt z polami: Nazwa, Cena, Ilosc.
Dodaj walidację w konstruktorze:
- Nazwa nie może być pusta
- Cena musi być większa od 0
- Ilość nie może być ujemna
Rzuć odpowiedni wyjątek przy błędnych danych.
💡 Podpowiedź: Użyj ArgumentException i ArgumentOutOfRangeException
⭐⭐ Zadanie 5: System zamówień
Utwórz klasy:
- Adres: Miasto, Ulica, KodPocztowy + konstruktor kopiujący
- Klient: Imie, Email, Adres (obiekt!) + konstruktor kopiujący (głęboka kopia!)
- Zamowienie: Numer, Klient, DataZlozenia, Wartosc
Każda klasa powinna mieć konstruktor pełny i kopiujący. Zamówienie przy tworzeniu automatycznie ustawia DataZlozenia na DateTime.Now.
💡 Podpowiedź: Pamiętaj o głębokiej kopii zagnieżdżonych obiektów!