Wszystkie wpisy

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”:

classstructrecord
Semantykareferencevaluereference (record class) / value (record struct)
Alokacjastertastos (zwykle)sterta
Równośćpo referencjipo wartości (ValueType.Equals, reflection - wolne)po wartości (kompilator generuje)
Dziedziczenietaknietak (tylko record <- record)
with-expressionnienie (struct record - tak)tak
Typowy use caseencje domenowe, long-livingmał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 nadpisuje virtual / 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 Square z Rectangle ł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.
  • this w konstruktorze - metody virtual wywoł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 + using to 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