Dziedziczenie i polimorfizm to fundamenty programowania obiektowego - i w przeciwieństwie do większości ficzerów C#, te NIE zmieniły się od 2002 roku! To dobra wiadomość - jeśli znałeś virtual, override i abstract w 2015, działają IDENTYCZNIE w 2026.
Ale są nowości! Default interface implementations (C# 8) rewolucjonizują interfejsy, a pattern matching z hierarchiami (C# 9+) daje nowe możliwości pracy z polimorfizmem. W tym wpisie poznasz fundamenty + nowoczesne użycia!
Zasada: "Favor composition over inheritance" - ale dziedziczenie ma swoje miejsce!
Podstawy dziedziczenia - niezmienne od 2002!
Składnia dziedziczenia
// Base class (klasa bazowa)
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
public void Eat()
{
Console.WriteLine($"{Name} is eating.");
}
public void Sleep()
{
Console.WriteLine($"{Name} is sleeping.");
}
}
// Derived class (klasa pochodna) - dziedziczy po Animal
public class Dog : Animal // Dog "is-a" Animal
{
public string Breed { get; set; }
public void Bark()
{
Console.WriteLine($"{Name} says: Woof!");
}
}
// Użycie
var dog = new Dog
{
Name = "Rex",
Age = 5,
Breed = "Labrador"
};
dog.Eat(); // Metoda z Animal
dog.Sleep(); // Metoda z Animal
dog.Bark(); // Metoda z Dog
// Dog ma dostęp do WSZYSTKIEGO z Animal + własne członki
Konstruktory w dziedziczeniu
// Base class z konstruktorem
public class Animal
{
public string Name { get; }
public int Age { get; }
public Animal(string name, int age)
{
Name = name;
Age = age;
Console.WriteLine($"Animal constructor: {name}");
}
}
// Derived class MUSI wywołać base constructor
public class Dog : Animal
{
public string Breed { get; }
// : base(name, age) - wywołanie konstruktora bazowego
public Dog(string name, int age, string breed) : base(name, age)
{
Breed = breed;
Console.WriteLine($"Dog constructor: {breed}");
}
}
var dog = new Dog("Rex", 5, "Labrador");
// Output:
// Animal constructor: Rex
// Dog constructor: Labrador
// Konstruktor bazowy ZAWSZE wykonuje się PRZED derived!
C# 12 - Primary constructors w hierarchii
// C# 12 - primary constructors z dziedziczeniem
public class Animal(string name, int age)
{
public string Name { get; } = name;
public int Age { get; } = age;
}
// Derived class - musi przekazać parametry do base
public class Dog(string name, int age, string breed) : Animal(name, age)
{
public string Breed { get; } = breed;
}
var dog = new Dog("Rex", 5, "Labrador");
Console.WriteLine($"{dog.Name}, {dog.Age}, {dog.Breed}");
Modyfikatory dostępu w dziedziczeniu
public class Animal
{
public string Name { get; set; } // Dostępne wszędzie
protected int Age { get; set; } // Tylko w Animal i klasach pochodnych
private string _id; // Tylko w Animal
internal string Species { get; set; } // Tylko w tym samym assembly
private void InternalMethod() { } // Tylko Animal
protected void ProtectedMethod() { } // Animal i klasy pochodne
public void PublicMethod() { } // Wszędzie
}
public class Dog : Animal
{
public void Test()
{
var name = Name; // ✅ OK - public
var age = Age; // ✅ OK - protected (dostęp w klasie pochodnej)
// var id = _id; // ❌ BŁĄD - private (tylko w Animal)
var sp = Species; // ✅ OK - internal (ten sam assembly)
PublicMethod(); // ✅ OK
ProtectedMethod(); // ✅ OK - protected
// InternalMethod(); // ❌ BŁĄD - private
}
}
⚠️ C# pozwala tylko na SINGLE INHERITANCE!
Klasa może dziedziczyć tylko po JEDNEJ klasie bazowej!
// ❌ To NIE działa w C# - multiple inheritance
public class Dog : Animal, Mammal // BŁĄD!
{
}
// ✅ Możesz implementować wiele INTERFEJSÓW
public class Dog : Animal, IRunnable, ISwimmable // OK!
{
}
Virtual, Override - polimorfizm w akcji
virtual - metody które można nadpisać
// Base class z virtual method
public class Animal
{
public string Name { get; set; }
// virtual - klasy pochodne MOGĄ nadpisać (ale nie muszą)
public virtual void MakeSound()
{
Console.WriteLine("Some generic animal sound");
}
public virtual string GetDescription()
{
return $"Animal: {Name}";
}
}
// Derived class - override virtual method
public class Dog : Animal
{
// override - nadpisz metodę z base class
public override void MakeSound()
{
Console.WriteLine("Woof! Woof!");
}
public override string GetDescription()
{
return $"Dog: {Name}";
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Meow!");
}
}
// Polimorfizm - ta sama referencja, różne zachowania!
Animal animal1 = new Dog { Name = "Rex" };
Animal animal2 = new Cat { Name = "Whiskers" };
animal1.MakeSound(); // Woof! Woof! (wywołuje Dog.MakeSound)
animal2.MakeSound(); // Meow! (wywołuje Cat.MakeSound)
// Runtime decyduje która wersja zostanie wywołana!
base - wywołanie metody bazowej
public class Animal
{
public virtual void Introduce()
{
Console.WriteLine("I'm an animal");
}
}
public class Dog : Animal
{
public override void Introduce()
{
// base. - wywołaj wersję z klasy bazowej
base.Introduce(); // "I'm an animal"
Console.WriteLine("And I'm a dog!");
}
}
var dog = new Dog();
dog.Introduce();
// Output:
// I'm an animal
// And I'm a dog!
override vs new - kluczowa różnica!
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Animal sound");
}
}
// override - polimorfizm (dynamic dispatch)
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
}
// new - ukrywa metodę bazową (NO polimorfizm!)
public class Cat : Animal
{
public new void MakeSound() // ⚠️ new, nie override!
{
Console.WriteLine("Meow!");
}
}
// Test polimorfizmu
Animal dog = new Dog();
Animal cat = new Cat();
dog.MakeSound(); // Woof! (override - polimorfizm działa! ✅)
cat.MakeSound(); // Animal sound (new - NIE polimorfizm! ❌)
// Cat jako Cat
Cat catDirect = new Cat();
catDirect.MakeSound(); // Meow! (bezpośrednia referencja działa)
// Zasada: ZAWSZE używaj override, NIE new (chyba że masz BARDZO dobry powód)
❌ new - ukrywa, nie nadpisuje
public class Base
{
public virtual void Method()
{
Console.WriteLine("Base");
}
}
public class Derived : Base
{
public new void Method() // ❌ new!
{
Console.WriteLine("Derived");
}
}
Base obj = new Derived();
obj.Method(); // Base (❌ nie polimorfizm!)
// new UKRYWA metodę - to NIE jest polimorfizm!
✅ override - prawdziwy polimorfizm
public class Base
{
public virtual void Method()
{
Console.WriteLine("Base");
}
}
public class Derived : Base
{
public override void Method() // ✅ override!
{
Console.WriteLine("Derived");
}
}
Base obj = new Derived();
obj.Method(); // Derived (✅ polimorfizm!)
// override daje prawdziwy polimorfizm! ✨
Abstract classes i members - kontrakty do implementacji
abstract class - nie można utworzyć instancji
// abstract class - szablon dla klas pochodnych
public abstract class Shape
{
public string Color { get; set; }
// abstract method - MUSI być zaimplementowana w klasie pochodnej
public abstract double GetArea();
// virtual method - MOŻE być nadpisana (ale nie musi)
public virtual void Draw()
{
Console.WriteLine($"Drawing {Color} shape");
}
// Normalna metoda - NIE może być nadpisana
public void Display()
{
Console.WriteLine($"Area: {GetArea()}, Color: {Color}");
}
}
// ❌ Nie możesz stworzyć instancji abstract class
// var shape = new Shape(); // BŁĄD kompilacji!
// Musisz dziedziczyć i zaimplementować wszystkie abstract members
public class Circle : Shape
{
public double Radius { get; set; }
// MUSISZ zaimplementować abstract method
public override double GetArea()
{
return Math.PI * Radius * Radius;
}
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double GetArea()
{
return Width * Height;
}
// Możesz nadpisać virtual method (ale nie musisz)
public override void Draw()
{
Console.WriteLine($"Drawing {Color} rectangle: {Width}x{Height}");
}
}
// Użycie - polimorfizm z abstract class
Shape circle = new Circle { Color = "Red", Radius = 5 };
Shape rectangle = new Rectangle { Color = "Blue", Width = 10, Height = 20 };
Console.WriteLine(circle.GetArea()); // ~78.5
Console.WriteLine(rectangle.GetArea()); // 200
circle.Display(); // Area: 78.5..., Color: Red
rectangle.Display(); // Area: 200, Color: Blue
Abstract properties
public abstract class Animal
{
// Abstract property - musi być zaimplementowana
public abstract string Species { get; }
// Abstract get/set property
public abstract int LegCount { get; set; }
// Virtual property
public virtual string Sound => "Generic sound";
}
public class Dog : Animal
{
// Implementacja abstract properties
public override string Species => "Canis familiaris";
public override int LegCount { get; set; } = 4;
// Override virtual property
public override string Sound => "Woof!";
}
Kiedy używać abstract vs interface?
Pytanie
Abstract Class
Interface
Wspólna implementacja?
✅ Tak - może mieć metody z kodem
Tylko default implementations (C# 8+)
State (fields)?
✅ Tak - może mieć pola
❌ Nie
Konstruktor?
✅ Tak
❌ Nie
Multiple inheritance?
❌ Nie - tylko jedna base class
✅ Tak - wiele interfejsów
Access modifiers?
✅ Tak - public, protected, private
Tylko public (domyślnie)
Kiedy używać?
"is-a" relationship + wspólny kod
"can-do" relationship + kontrakt
💡 Przykłady - abstract class vs interface
// ✅ Abstract class - wspólna implementacja
public abstract class Vehicle
{
public string Brand { get; set; }
public int Year { get; set; }
// Wspólna logika dla wszystkich vehicles
public string GetAge() => $"{DateTime.Now.Year - Year} years old";
// Każdy vehicle musi zaimplementować
public abstract void Start();
}
// ✅ Interface - kontrakt, bez implementacji
public interface IFlyable
{
void TakeOff();
void Land();
double MaxAltitude { get; }
}
// Klasa może dziedziczyć po abstract + implementować interfejsy
public class Airplane : Vehicle, IFlyable
{
public override void Start()
{
Console.WriteLine("Starting engines...");
}
public void TakeOff()
{
Console.WriteLine("Taking off...");
}
public void Land()
{
Console.WriteLine("Landing...");
}
public double MaxAltitude => 12000;
}
Sealed - blokowanie dziedziczenia
sealed class - nie można dziedziczyć
// sealed class - końcowa klasa w hierarchii
public sealed class FinalClass
{
public void Method()
{
Console.WriteLine("This class cannot be inherited");
}
}
// ❌ To NIE zadziała
// public class Derived : FinalClass // BŁĄD kompilacji!
// {
// }
// Przykłady sealed classes w .NET:
// - System.String (sealed!)
// - System.Int32, Int64, etc. (sealed!)
// - Record types są domyślnie sealed
sealed override - blokowanie dalszego override
public class Base
{
public virtual void Method()
{
Console.WriteLine("Base");
}
}
public class Middle : Base
{
// sealed override - można nadpisać Base, ale STOP tutaj!
public sealed override void Method()
{
Console.WriteLine("Middle - no more overrides!");
}
}
public class Derived : Middle
{
// ❌ Nie możesz override sealed method
// public override void Method() // BŁĄD!
// {
// }
}
Kiedy używać sealed?
🔍 Używaj sealed gdy:
✅ Performance - sealed classes są szybsze (compiler optimizations)
✅ Design intent - klasa nie jest zaprojektowana do dziedziczenia
✅ Breaking changes - chronisz przed zmianami w przyszłości
Zasada: Domyślnie rób klasy sealed, chyba że jawnie projektujesz do dziedziczenia!
// ✅ DOBRZE - sealed domyślnie (records)
public sealed record UserDto(int Id, string Email);
// ✅ DOBRZE - sealed explicit
public sealed class StringHelper
{
public static string Reverse(string input) => /* ... */;
}
// ⚠️ Tylko jeśli PROJEKTUJESZ hierarchię - pozostaw otwarte
public class BaseController // NIE sealed - zaprojektowane do dziedziczenia
{
public virtual void OnInit() { }
}
🔥 Default Interface Implementations (C# 8)
Problem przed C# 8
// Przed C# 8 - interfejs bez implementacji
public interface ILogger
{
void Log(string message);
}
// 100 klas implementuje ILogger
public class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine(message);
}
// ... 99 innych klas ...
// ❌ Problem: chcesz dodać nową metodę do ILogger
public interface ILogger
{
void Log(string message);
void LogError(string error); // Nowa metoda!
}
// 💥 BREAKING CHANGE! Wszystkie 100 klas muszą zaimplementować LogError!
// To jest NIEMOŻLIWE w istniejących projektach!
Rozwiązanie - Default Interface Implementation!
🎉 C# 8 - Default Interface Members
Interfejsy mogą mieć implementację! Dodawaj nowe metody BEZ breaking changes!
// C# 8 - interfejs z domyślną implementacją
public interface ILogger
{
void Log(string message);
// Default implementation - klasy NIE MUSZĄ implementować!
void LogError(string error)
{
Log($"ERROR: {error}"); // używa Log() z implementacji
}
void LogWarning(string warning)
{
Log($"WARNING: {warning}");
}
}
// Stara klasa - działa bez zmian! ✅
public class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine(message);
// LogError i LogWarning są dostępne z domyślnej implementacji!
}
// Użycie
ILogger logger = new ConsoleLogger();
logger.Log("Info"); // ConsoleLogger.Log
logger.LogError("Error!"); // Default implementation z ILogger!
logger.LogWarning("Warning"); // Default implementation z ILogger!
// Output:
// Info
// ERROR: Error!
// WARNING: Warning
Override default implementation
public interface ILogger
{
void Log(string message);
void LogError(string error)
{
Log($"ERROR: {error}");
}
}
// Możesz nadpisać default implementation
public class FileLogger : ILogger
{
public void Log(string message)
{
File.AppendAllText("log.txt", message + "\n");
}
// Override default implementation - własna logika!
public void LogError(string error)
{
File.AppendAllText("errors.txt", $"[{DateTime.Now}] {error}\n");
}
}
ILogger logger = new FileLogger();
logger.LogError("Critical error");
// Używa FileLogger.LogError, NIE default implementation
Default properties i static members
// C# 8 - interfejsy mogą mieć więcej!
public interface IConfigurable
{
// Default property
string ConfigPath => "config.json";
// Default method
void LoadConfig()
{
var json = File.ReadAllText(ConfigPath);
Console.WriteLine($"Loaded: {json}");
}
// Static member w interfejsie!
static IConfigurable CreateDefault() => new DefaultConfig();
}
public class DefaultConfig : IConfigurable
{
// ConfigPath i LoadConfig są dostępne z domyślnej implementacji
}
// Użycie
IConfigurable config = IConfigurable.CreateDefault(); // static member!
config.LoadConfig(); // default implementation
⚠️ Default interface members - ważne zasady!
Default members są dostępne TYLKO przez interface reference
NIE są dostępne przez class reference (chyba że explicit override)
Nie mogą mieć fields (pól) - tylko properties, methods
Access modifiers: domyślnie public, można użyć private, protected
public interface ILogger
{
void LogError(string error) => Log($"ERROR: {error}");
}
public class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine(message);
}
// ✅ Przez interface reference - działa
ILogger logger1 = new ConsoleLogger();
logger1.LogError("Error"); // ✅ OK
// ❌ Przez class reference - NIE MA dostępu!
ConsoleLogger logger2 = new ConsoleLogger();
// logger2.LogError("Error"); // ❌ BŁĄD - LogError nie jest w ConsoleLogger!
// Przykład 1: Repository pattern z default query methods
public interface IRepository
{
Task GetByIdAsync(int id);
Task> GetAllAsync();
// Default implementation dla common operations
async Task FindFirstAsync(Func predicate)
{
var all = await GetAllAsync();
return all.FirstOrDefault(predicate);
}
async Task CountAsync()
{
var all = await GetAllAsync();
return all.Count();
}
}
// Przykład 2: IDisposable pattern
public interface IResource : IDisposable
{
void Close();
// Default Dispose wywołuje Close
void IDisposable.Dispose()
{
Close();
GC.SuppressFinalize(this);
}
}
// Implementacja - tylko Close!
public class FileResource : IResource
{
public void Close()
{
// zamknij plik
}
// Dispose jest z default implementation!
}
Pattern Matching z hierarchiami (C# 9+)
Type patterns - switch na typach
// Hierarchia
public abstract class Shape
{
public string Color { get; init; }
}
public class Circle : Shape
{
public double Radius { get; init; }
}
public class Rectangle : Shape
{
public double Width { get; init; }
public double Height { get; init; }
}
public class Triangle : Shape
{
public double Base { get; init; }
public double Height { get; init; }
}
// C# 9+ - pattern matching na hierarchii
double CalculateArea(Shape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
Triangle t => 0.5 * t.Base * t.Height,
_ => throw new ArgumentException("Unknown shape")
};
// Użycie
Shape circle = new Circle { Color = "Red", Radius = 5 };
Shape rectangle = new Rectangle { Color = "Blue", Width = 10, Height = 20 };
Console.WriteLine(CalculateArea(circle)); // ~78.5
Console.WriteLine(CalculateArea(rectangle)); // 200
Property patterns w hierarchii
// Pattern matching z property patterns
string DescribeShape(Shape shape) => shape switch
{
Circle { Radius: > 10 } => "Large circle",
Circle { Radius: <= 10 } => "Small circle",
Rectangle { Width: var w, Height: var h } when w == h => "Square",
Rectangle { Width: > 20 } => "Wide rectangle",
Rectangle => "Normal rectangle",
Triangle { Base: > 10, Height: > 10 } => "Large triangle",
_ => "Unknown shape"
};
// Relational patterns
string GetSize(Shape shape) => shape switch
{
Circle { Radius: < 5 } => "Tiny",
Circle { Radius: >= 5 and < 10 } => "Medium",
Circle { Radius: >= 10 } => "Large",
Rectangle { Width: < 10, Height: < 10 } => "Small",
_ => "Various size"
};
Nested property patterns
// Nested hierarchie
public class Canvas
{
public Shape? MainShape { get; init; }
public string Theme { get; init; }
}
// Nested pattern matching
string DescribeCanvas(Canvas canvas) => canvas switch
{
{ MainShape: Circle { Radius: > 10 }, Theme: "Dark" }
=> "Dark canvas with large circle",
{ MainShape: Rectangle { Width: var w, Height: var h }, Theme: "Light" } when w == h
=> "Light canvas with square",
{ MainShape: Triangle { Base: > 20 } }
=> "Canvas with wide triangle",
{ MainShape: null }
=> "Empty canvas",
_ => "Generic canvas"
};
List patterns z hierarchiami (C# 11)
// C# 11 - list patterns
string DescribeShapes(Shape[] shapes) => shapes switch
{
[] => "No shapes",
[Circle] => "Single circle",
[Circle, Circle] => "Two circles",
[Circle, ..] => "Starts with circle",
[.., Rectangle] => "Ends with rectangle",
[Circle, Rectangle, Triangle] => "Circle, rectangle, triangle in order",
[var first, .., var last] => $"Multiple shapes: first {first.GetType().Name}, last {last.GetType().Name}",
_ => "Various shapes"
};
// Użycie
var shapes1 = new Shape[] { new Circle { Radius = 5 } };
var shapes2 = new Shape[] { new Circle { Radius = 5 }, new Rectangle { Width = 10, Height = 20 } };
Console.WriteLine(DescribeShapes(shapes1)); // Single circle
Console.WriteLine(DescribeShapes(shapes2)); // Starts with circle
💡 Praktyczny przykład - visitor pattern z pattern matching
// Hierarchia - Expression Tree (AST)
public abstract record Expr;
public record Constant(double Value) : Expr;
public record Variable(string Name) : Expr;
public record BinaryOp(Expr Left, string Op, Expr Right) : Expr;
// Evaluator używa pattern matching zamiast visitor pattern!
double Evaluate(Expr expr, Dictionary variables) => expr switch
{
Constant c => c.Value,
Variable v => variables[v.Name],
BinaryOp { Op: "+", Left: var l, Right: var r }
=> Evaluate(l, variables) + Evaluate(r, variables),
BinaryOp { Op: "-", Left: var l, Right: var r }
=> Evaluate(l, variables) - Evaluate(r, variables),
BinaryOp { Op: "*", Left: var l, Right: var r }
=> Evaluate(l, variables) * Evaluate(r, variables),
BinaryOp { Op: "/", Left: var l, Right: var r }
=> Evaluate(l, variables) / Evaluate(r, variables),
_ => throw new InvalidOperationException()
};
// Użycie: (x + 5) * 2
var expr = new BinaryOp(
new BinaryOp(new Variable("x"), "+", new Constant(5)),
"*",
new Constant(2)
);
var vars = new Dictionary { ["x"] = 10 };
var result = Evaluate(expr, vars); // (10 + 5) * 2 = 30
Podsumowanie
Dziedziczenie i polimorfizm - fundamenty OOP + nowoczesne użycia:
✅ Pattern matching z hierarchiami - type patterns, property patterns
✅ List patterns (C# 11) - pattern matching na kolekcjach hierarchii
✅ Abstract vs Interface - kiedy czego używać
✅ override vs new - kluczowa różnica!
W kolejnym wpisie poznasz Interfejsy zaawansowane - static abstract members (C# 11), generic math, covariance/contravariance!
Zadanie dla Ciebie 🎯
Stwórz hierarchię klas dla systemu płatności:
// Abstract base class
public abstract class Payment
{
public decimal Amount { get; init; }
public DateTime Date { get; init; }
// Abstract - każda płatność musi implementować
public abstract bool Process();
// Virtual - może być nadpisane
public virtual string GetReceipt()
{
return $"Payment: ${Amount} on {Date:yyyy-MM-dd}";
}
}
// Klasy pochodne do zaimplementowania:
// 1. CreditCardPayment(Amount, Date, CardNumber, CVV)
// 2. PayPalPayment(Amount, Date, Email)
// 3. CryptoPayment(Amount, Date, WalletAddress, CryptoType)
Wymagania:
Każda klasa implementuje Process() inaczej
CreditCardPayment override GetReceipt() - dodaje ostatnie 4 cyfry karty
Interfejs IRefundable z default implementation Refund()