CodeGym /Kursy /C# SELF /Praktyczne użycie polimorfizmu

Praktyczne użycie polimorfizmu

C# SELF
Poziom 21 , Lekcja 3
Dostępny

1. Polimorfizm w praktyce

W programowaniu polimorfizm to jak uniwersalny pilot: wciskasz jeden przycisk "Głośność+", a steruje telewizorem, audio albo klimatyzacją — każde urządzenie reaguje po swojemu, ale interfejs jest jeden! Podobnie, obiekty różnych typów mogą różnie reagować na to samo wywołanie metody, jeśli ta metoda jest zdefiniowana w ich wspólnym przodku jako virtual.

W C# polimorfizm pojawia się, gdy zmienna typu klasy bazowej (albo interfejsu) może "trzymać" obiekt dowolnego potomka, a wywołania wirtualnych metod na takiej zmiennej prowadzą do wykonania konkretnej, "prawdziwej" implementacji — tej, którą zdefiniował sam obiekt. To podstawa architektur, gdzie logika zmienia się dynamicznie.

Czy to się przydaje gdzieś poza podręcznikami?

Jasne! W praktycznie każdym projekcie, gdzie masz do czynienia z różnymi, ale podobnymi obiektami — od zwierzaków w zoo po elementy GUI, od handlerów zdarzeń po systemy dokumentów.

  • Pozwala tworzyć uniwersalne algorytmy — kod, który działa na abstrakcyjnych bytach, nie przejmując się szczegółami konkretnej implementacji.
  • Gwarantuje rozszerzalność — możesz dodać setkę nowych "zwierzaków", "figur", "handlerów", nie ruszając istniejącego kodu.
  • Zmniejsza zależność komponentów programu od siebie (to ważny temat na rozmowach i w architekturze!).

2. Podstawowa składnia i mechanika

Przypomnijmy i rozbudujmy nasze klasy Animal, Dog, Cat, żeby pokazać polimorfizm w akcji. Skoro zaczęliśmy od aplikacji "Wirtualne zoo", ulepszmy ją.

Klasy bazowe z wirtualnymi metodami


public class Animal
{
    public string Name { get; set; }

    public Animal(string name)
    {
        Name = name;
    }

    // Wirtualna metoda — można ją nadpisać
    public virtual void MakeSound()
    {
        Console.WriteLine($"{Name} wydaje jakiś dźwięk...");
    }
}
        
public class Dog : Animal
{
    public Dog(string name) : base(name) { }

    public override void MakeSound()
    {
        Console.WriteLine($"{Name} mówi: Hau-hau!");
    }
}
public class Cat : Animal
{
    public Cat(string name) : base(name) { }

    public override void MakeSound()
    {
        Console.WriteLine($"{Name} mówi: Miau!");
    }
}

Użycie polimorfizmu: przykład z kolekcją zwierząt

Załóżmy, że mamy listę zwierzaków — domowych i nie tylko — i chcemy, żeby wszystkie wydały dźwięk. Bez polimorfizmu trzeba by pisać sprawdzanie typu i dużo powtarzać kod. Z polimorfizmem — wszystko elegancko i zwięźle!


// Tworzymy tablicę zwierząt różnych typów
Animal[] animals = new Animal[]
{
    new Dog("Bobik"),
    new Cat("Murka"),
    new Dog("Spot"),
    new Cat("Barsik"),
};

// Przechodzimy po całej tablicy i każemy każdemu wydać dźwięk
foreach (var animal in animals)
{
    animal.MakeSound(); // Wywoła się metoda z Dog lub Cat, a nie z Animal!
}
        

Wynik:

Bobik mówi: Hau-hau!
Murka mówi: Miau!
Spot mówi: Hau-hau!
Barsik mówi: Miau!

Cała magia: jeden uniwersalny kod — różne wyniki w zależności od faktycznego typu obiektu.

Diagram: jak działa polimorfizm


Animal (klasa bazowa)
   /           \
Dog           Cat
        
Hierarchia klas: Animal → Dog, Cat

Przy wywołaniu animal.MakeSound(), gdzie Animal może zawierać instancję Dog albo Cat, środowisko .NET w czasie działania samo "ogarnia", którą dokładnie metodę MakeSound() wywołać.

3. Rozwiązywanie typowych zadań z polimorfizmem

Przykład 1: Uniwersalna lista, różne akcje

Załóżmy, że robisz jakąś grę. Masz klasę bazową GameObject, pochodne — wrogowie, sojusznicy, przeszkody. Wszystkie mogą się ruszać, wszystkie mają metodę Update(), ale implementacja jest różna.

public class GameObject
{
    public virtual void Update() { }
}

public class Enemy : GameObject
{
    public override void Update()
    {
        Console.WriteLine("Wróg atakuje!");
    }
}

public class Friend : GameObject
{
    public override void Update()
    {
        Console.WriteLine("Sojusznik pomaga!");
    }
}

GameObject[] objects = new GameObject[]
{
    new Enemy(),
    new Friend(),
    new Enemy()
};

foreach (var obj in objects)
{
    obj.Update();
}
// Wypisze:
// Wróg atakuje!
// Sojusznik pomaga!
// Wróg atakuje!

Przykład 2: Przekazywanie obiektów do metod

Możesz przyjmować parametr typu bazowego, a używać dowolnego typu potomnego. To mega wygodne, zwłaszcza gdy obiekt będzie miał nowych potomków.

public static void FeedAnimal(Animal animal)
{
    Console.Write($"{animal.Name}: ");
    animal.MakeSound();
    Console.WriteLine("I dostaje jedzenie.");
}

FeedAnimal(new Dog("Rex"));
FeedAnimal(new Cat("Kitty"));

// Wynik:
// Rex: Rex mówi: Hau-hau!
// I dostaje jedzenie.
// Kitty: Kitty mówi: Miau!
// I dostaje jedzenie.

Zwróć uwagę: napisanie takiej metody bez polimorfizmu byłoby bardzo uciążliwe — trzeba by sprawdzać typ zwierzaka, robić mnóstwo ifów i ręcznie wywoływać odpowiednie metody.

Ważna uwaga: wiązanie w czasie działania

Polimorfizm działa dzięki temu, że wywołania wirtualnych metod są dynamiczne, czyli w czasie działania programu. To się nazywa późne wiązanie (late binding). Nawet jeśli znamy zmienną jako Animal, metoda i tak zostanie wywołana ta, która jest zdefiniowana w faktycznym obiekcie.

Jeśli jednak metoda nie jest oznaczona jako virtual, wywołanie zawsze pójdzie po tym kodzie, który jest zadeklarowany dla zmiennej (a nie obiektu). Więc nie żałuj słowa kluczowego virtual, jeśli chcesz mieć elastyczność!

4. Praktyka: rozwijamy naszą aplikację

Załóżmy, że chcesz dodać do swojego wirtualnego zoo nową funkcję: teraz każde zwierzę ma nie tylko MakeSound(), ale też się ruszać (Move()). Ale każdy robi to inaczej.

1. Dodajemy do klasy bazowej wirtualną metodę


public class Animal
{
    public string Name { get; set; }

    public Animal(string name)
    {
        Name = name;
    }

    public virtual void MakeSound()
    {
        Console.WriteLine($"{Name} wydaje jakiś dźwięk...");
    }

    public virtual void Move()
    {
        Console.WriteLine($"{Name} porusza się w nieokreślony sposób...");
    }
}
        

2. W klasach pochodnych implementujemy Move() po swojemu

public class Dog : Animal
{
    public Dog(string name) : base(name) { }

    public override void MakeSound()
    {
        Console.WriteLine($"{Name} mówi: Hau-hau!");
    }

    public override void Move()
    {
        Console.WriteLine($"{Name} biegnie za patykiem.");
    }
}

public class Cat : Animal
{
    public Cat(string name) : base(name) { }

    public override void MakeSound()
    {
        Console.WriteLine($"{Name} mówi: Miau!");
    }

    public override void Move()
    {
        Console.WriteLine($"{Name} skrada się miękkimi łapkami.");
    }
}

3. Używamy obu metod w kolekcji

Animal[] animals = new Animal[]
{
    new Dog("Bim"),
    new Cat("Lusia")
};

foreach (var animal in animals)
{
    animal.MakeSound();
    animal.Move();
}

Wynik:

Bim mówi: Hau-hau!
Bim biegnie za patykiem.
Lusia mówi: Miau!
Lusia skrada się miękkimi łapkami.

W prawdziwych aplikacjach takie podejście pozwala pisać bardzo mocne i wielokrotnego użytku moduły. Na przykład w grach to podstawa wszystkich managerów obiektów — od NPC po efekty.

5. Zadanie praktyczne: rysujemy różne figury

Zobaczmy przykład nie ze zoo, a z grafiki. Stwórzmy klasę bazową Shape z wirtualną metodą Draw(), a potem ją rozszerzmy.

public class Shape
{
    public virtual void Draw()
    {
        Console.WriteLine("Rysuje się nieokreślona figura.");
    }
}

public class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Rysuje się koło.");
    }
}

public class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Rysuje się prostokąt.");
    }
}

// Kolekcja figur
Shape[] shapes = new Shape[]
{
    new Circle(),
    new Rectangle(),
    new Circle()
};

foreach (var shape in shapes)
{
    shape.Draw();
}

Tutaj Draw() jest wywoływane na zmiennej typu Shape, ale faktycznie wywołują się metody z Circle albo Rectangle. W prawdziwych edytorach graficznych i bibliotekach, np. WinForms czy WPF — dokładnie tak to działa.

6. Ogólne podejście: piszemy uniwersalne algorytmy

Polimorfizm zamienia twój kod w elastyczne i rozszerzalne narzędzie. Na przykład w kolekcji mogą być nie tylko Dog i Cat, ale też Hamster, Parrot i kto tylko chcesz — i nie piszesz ani jednej nowej linijki, żeby ich obsłużyć tymi samymi metodami.

Poza tym bez problemu możesz przekazywać obiekty-potomki do metod, które przyjmują typ bazowy:

void PrintAnimalInfo(Animal animal)
{
    Console.WriteLine($"Imię: {animal.Name}");
    animal.MakeSound();
    animal.Move();
}

Animal hamster = new Animal("Hamster");
Animal dog = new Dog("Lord");
PrintAnimalInfo(hamster); // Użyje metody z Animal
PrintAnimalInfo(dog);     // Użyje wersji Dog

7. Przydatne niuanse

Często spotykane pytania i pułapki

Wielu początkujących myśli, że jeśli stworzą obiekt Dog, a potem zadeklarują zmienną Dog myDog, to nie ma różnicy z Animal myDog. W rzeczywistości, jeśli zadeklarujesz zmienną jako Animal, "widzi" ona tylko to, co jest w Animal (oprócz nadpisanych metod), a zadeklarowana jako Dog — widzi wszystko: i Bark(), i specyficzne właściwości.

Ważne też: jeśli metoda w klasie bazowej nie jest oznaczona jako virtual, nie da się jej nadpisać. Jeśli spróbujesz w potomku napisać override dla takiej metody — kompilator się obrazi i wywali błąd.

A jeśli naprawdę chcesz podmienić metodę, która nie jest wirtualna, użyj słowa kluczowego new, ale to już temat na zaawansowane (i konfliktowe!) przypadki.

Dlaczego to lubią pytać na rozmowach?

Bo polimorfizm to taki szwajcarski scyzoryk programisty: jeśli wiesz, jak go użyć, potrafisz budować rozszerzalne i łatwe w utrzymaniu systemy, twój kod nie tylko działa, ale i żyje długo i szczęśliwie. Na przykład poproszą cię o obsługę różnych form płatności (klasyka: BankCard, PayPal, Bitcoin) — i oczekują, że zrobisz wspólny interfejs (albo bazową klasę abstrakcyjną) z metodą Pay(), a potem klienci mogą przyjmować Pay(BankCard), Pay(PayPal), Pay(Bitcoin) — i nie muszą wiedzieć, jak to działa w środku.

8. Typowe błędy początkujących

Jeden z najczęstszych błędów — próba wywołania specyficznej metody klasy pochodnej przez zmienną typu bazowego. Na przykład tak:


Animal animal = new Dog("Buddy");
animal.Bark(); // Błąd! Animal nie ma metody Bark.
        

Dlaczego to nie działa? Bo zmienna typu Animal "widzi" tylko to, co zadeklarowano w Animal, nawet jeśli faktycznie tam jest Dog. Można wywołać tylko to, co było zdefiniowane w klasie bazowej — i nadpisane (override) w potomkach.

Jeśli jednak musisz wywołać metodę, która jest tylko w Dog — musisz zrobić rzutowanie typu:

Animal animal = new Dog("Buddy");
if (animal is Dog dog)
{
    dog.Bark();
}

Ale często, jeśli tak robisz, to znaczy, że gdzieś architektura jest popsuta (albo nadużywasz dziedziczenia).

2
Zadanie
C# SELF, poziom 21, lekcja 3
Niedostępne
Implementacja podstawowego polimorfizmu z użyciem metody wirtualnej
Implementacja podstawowego polimorfizmu z użyciem metody wirtualnej
2
Zadanie
C# SELF, poziom 21, lekcja 3
Niedostępne
Uniwersalna funkcja pracy z obiektami polimorficznymi
Uniwersalna funkcja pracy z obiektami polimorficznymi
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION