Klasy i obiekty - instrukcja i smok LEGO
Klasa to instrukcja LEGO, obiekt to konkretny smok, którego właśnie poskładałeś.
Instrukcja i smok #
Wyobraź sobie, że kupujesz w sklepie zestaw LEGO - „Smok ognisty”. Otwierasz pudełko i widzisz dwie rzeczy:
- Instrukcję (tę kartkę z rysunkami)
- Klocki, z których zbudujesz smoka
Instrukcja to nie jest smok. To tylko przepis. Napisane jest: smok ma ogon, skrzydła, zieje ogniem.
Dopiero gdy poskładasz klocki według instrukcji - powstaje prawdziwy smok na dywanie.
Klasa = instrukcja. Obiekt = smok zbudowany z klocków.
Możesz z tej samej instrukcji zbudować 10 różnych smoków: jeden czerwony, drugi zielony, trzeci z pogryzionym ogonem - ale wszystkie mają ten sam przepis.
W kodzie #
class Smok {
constructor(imie, kolor) {
this.imie = imie;
this.kolor = kolor;
}
ziejOgniem() {
console.log(`${this.imie} zieje ogniem! 🔥`);
}
}
const pafnucy = new Smok('Pafnucy', 'zielony');
const błyskawica = new Smok('Błyskawica', 'czarny');
pafnucy.ziejOgniem(); // Pafnucy zieje ogniem! 🔥
błyskawica.ziejOgniem(); // Błyskawica zieje ogniem! 🔥
Jedna instrukcja → dwa różne smoki.
Dziedziczenie - ciężarówka LEGO #
Masz instrukcję auta. Pod spodem: cztery koła, silnik, kierownica. Teraz chcesz zrobić ciężarówkę. Co robisz?
Nie rysujesz od zera. Mówisz: „ciężarówka jest typem auta, ale dodatkowo ma pakę”. Instrukcja auta → dziedziczysz koła, silnik, kierownicę. Dodajesz tylko pakę.
class Pojazd {
jedz() { console.log('Jadę...'); }
}
class Ciężarówka extends Pojazd {
wyładuj() { console.log('Wyładowuję pakę!'); }
}
Polimorfizm - jeden przycisk, różne reakcje #
Masz pilot z jednym przyciskiem „JEDŹ”. Naciskasz - wyścigówka warczy, ciężarówka rusza powoli, skuter wznosi się w powietrze. Ten sam sygnał, różne reakcje. To polimorfizm.
Po co to wszystko w 2026 roku? #
W erze AI, gdy modele generują Ci 100 linii kodu na sekundę, to Ty musisz wiedzieć, co w tym kodzie siedzi. OOP to sposób organizacji - pokazuje, czym jest smok, a czym ciężarówka. Bez tego kod to puzzle bez pudełka.
Klasa = deklaracja typu #
Klasa w C# to definicja typu: pola, właściwości, metody, kontrakty. Obiekt to instancja tego typu w pamięci. Rozróżnienie istotne - jeden kod źródłowy klasy, wiele niezależnych obiektów z własnym stanem.
// title: Dragon.cs
public class Dragon
{
public string Name { get; }
public string Color { get; }
public Dragon(string name, string color)
{
Name = name;
Color = color;
}
public virtual void BreatheFire() => Console.WriteLine($"{Name} breathes fire!");
}
var pafnucy = new Dragon("Pafnucy", "green");
var blyskawica = new Dragon("Blyskawica", "black");
Dwa obiekty, dwa miejsca w pamięci, wspólna metoda BreatheFire.
Class vs struct vs record #
C# ma trzy smaki “obiektów”:
| class | struct | record | |
|---|---|---|---|
| Semantyka | reference | value | reference (record class) / value (record struct) |
| Alokacja | sterta | stos (zwykle) | sterta |
| Równość | po referencji | po wartości (ValueType.Equals, reflection - wolne) | po wartości (kompilator generuje) |
| Dziedziczenie | tak | nie | tak (tylko record <- record) |
with-expression | nie | nie (struct record - tak) | tak |
| Typowy use case | encje domenowe, long-living | małe POD-y (Point, Vector) | DTO, message, immutable dane |
public record OrderDto(int Id, string CustomerName, decimal Total);
var a = new OrderDto(1, "Alice", 100);
var b = a with { Total = 150 }; // nowy record, a niezmienny
Console.WriteLine(a == b); // false - equality po wartości
Dziedziczenie - klasa bazowa + specjalizacja #
public abstract class Vehicle
{
public abstract void Drive(); // musi być zaimplementowane w podklasie
public virtual void Stop() // może być nadpisane, ale ma sensowne domyślne
{
Console.WriteLine("Braking...");
}
}
public class Truck : Vehicle
{
public override void Drive() => Console.WriteLine("Rolling with cargo...");
public override void Stop()
{
Console.WriteLine("Hissing air brakes...");
base.Stop();
}
}
Zasady:
abstract- brak ciała, podklasa musi nadpisać.virtual- jest ciało, podklasa może nadpisać.override- podklasa nadpisujevirtual/abstract.sealed- blokuje dalsze nadpisanie / dziedziczenie.new(keyword) - ukrywa metodę z klasy bazowej. Prawie nigdy tego nie chcesz (łamie polimorfizm).
Polimorfizm - jedno wywołanie, różne implementacje #
List<Vehicle> fleet = [new Truck(), new Motorcycle(), new Car()];
foreach (var v in fleet)
v.Drive(); // każdy jeździ po swojemu
Mechanizm: vtable. Obiekt ma wskaźnik do tablicy metod swojej klasy rzeczywistej (Truck.Drive), nie deklarowanej (Vehicle.Drive). Kompilator generuje lookup przy virtual. Koszt: jedna pośrednia dereferencja - dla 99% kodu zero różnicy.
Interfejs - kontrakt bez implementacji #
public interface IPayable
{
decimal CalculatePay();
}
public class Employee : IPayable { public decimal CalculatePay() => 5000; }
public class Contractor : IPayable { public decimal CalculatePay() => HoursWorked * Rate; }
void Process(IPayable p) => Console.WriteLine(p.CalculatePay());
Klasa może dziedziczyć po jednej klasie, implementować wiele interfejsów. Dla większości nowego kodu zaczynaj od interfejsów (łatwe mockowanie, testowanie, DI).
C# 8+ pozwala na default interface methods (domyślne implementacje w interfejsie), ale używaj ostrożnie - łamie klasyczną dychotomię “interface = kontrakt”.
SOLID - mapowanie na C# idiomy #
- S - single responsibility: klasa / metoda ma jeden powód do zmiany.
- O - open/closed: rozszerzaj przez dziedziczenie / kompozycję, nie modyfikuj istniejących klas.
- L - Liskov: podklasa zachowuje kontrakt klasy bazowej (podklasa
SquarezRectanglełamie to). - I - interface segregation: lepiej 5 małych interfejsów niż jeden
IGodObject. - D - dependency inversion: zależ od abstrakcji (interfejsu), nie od konkretnej klasy. W praktyce: DI container + constructor injection.
Kompozycja > dziedziczenie #
Reguła kciuka: gdy nie jesteś pewien czy użyć dziedziczenia - użyj kompozycji. Dziedziczenie tworzy sztywną hierarchię; kompozycja (klasa trzyma referencję do innej i deleguje) jest elastyczna, testowalna, łatwa do zmiany.
// Zamiast: class Truck : Vehicle
// Rozważ: class Truck { private readonly IEngine _engine; ... }
Pułapki #
- Constructor chaining - inicjalizator
: base(...)wywołuje konstruktor klasy bazowej. Bez tego działa domyślny bezparametrowy - jeśli nie istnieje, błąd. thisw konstruktorze - metodyvirtualwywołane w konstruktorze klasy bazowej wołają nadpisaną wersję, ale pola klasy pochodnej jeszcze nie istnieją. Klasyczny bug.- Equals / GetHashCode - domyślna implementacja dla class to tożsamość referencji. Dla DTO / value-semantic użyj record lub nadpisz oba.
- Finalizery -
~ClassName()- prawie nigdy nie potrzebujesz.IDisposable+usingto standard.
Skrótowo #
Klasa to szablon, obiekt to instancja w pamięci. Pola = stan, metody = zachowanie, virtual + override = polimorfizm przez vtable, interface = kontrakt. Dla nowego kodu: preferuj record na dane, interface na zależności, kompozycję na strukturę.
Podobne wpisy
Programowanie obiektowe w C# - klasy, dziedziczenie, polimorfizm
Klasy i obiekty, hermetyzacja przez właściwości, dziedziczenie z virtual/override, polimorfizm w praktyce i interfejsy jako kontrakty. Druga część kursu C#.
Komputer to bardzo szybki, ale całkowicie dosłowny wykonawca
Dlaczego kod musi być pisany z dokładnością, której nigdy nie żądamy od człowieka - i co z tego wynika dla całej branży.
Merge Sort - jak dzieci szybciej sprzątają klocki
„Dziel i zwyciężaj" to po prostu mycie klocków w kilka osób naraz.