1. Вступ
Уявіть собі ситуацію: у вас удома є телевізор, музичний центр, кондиціонер і розумна лампочка. І в кожного з них свій пульт керування. Щоб увімкнути телевізор, ви берете пульт від телевізора і натискаєте кнопку «Увімкнути». Щоб увімкнути музичний центр, берете пульт від музичного центру і натискаєте ту саму кнопку «Увімкнути». Уявили? Хаос, чи не так?
А що, якби у вас був універсальний пульт? Ви берете його, натискаєте кнопку «Увімкнути», і він якось магічно розуміє, який пристрій зараз треба увімкнути, та надсилає йому правильну команду. При цьому сам пульт не знає, як саме вмикається телевізор чи як саме вмикається музичний центр. Йому просто відомо, що в усіх цих пристроїв є спільна «функція увімкнення».
Ось це й є аналогія з поліморфізмом у програмуванні!
Поліморфізм
Слово «поліморфізм» походить від грецьких слів poly (багато) і morph (форма). Буквально це означає «багато форм». У контексті ООП — це здатність об’єкта набувати різних форм або, точніше, здатність одного й того самого методу поводитися по‑різному залежно від типу об’єкта, на якому його викликають.
У C# поліморфізм досягається передусім завдяки наслідуванню та використанню методів virtual і override.
Ключова ідея поліморфізму полягає у висхідному перетворенні (upcasting). Що це таке?
Давайте повернімося до нашої ієрархії Animal → Dog, Cat. Ми знаємо, що собака — тварина. Кішка — тварина. Це відношення «is-a» — фундаментальна основа наслідування.
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Якийсь звук...");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Гав-гав!");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Мяу!");
}
}
Завдяки відношенню «is-a» ми можемо присвоїти об’єкт похідного класу змінній базового класу. Це називається висхідним перетворенням (upcasting), бо ми ніби «піднімаємо» об’єкт до більш загального, базового типу. Компілятор C# дозволяє це робити автоматично:
// У нас є конкретна собака
Dog myDog = new Dog();
// Ми можемо присвоїти об'єкт Dog змінній типу Animal!
// Це і є висхідне перетворення (upcasting).
// Справа - конкретний Dog, зліва - більш загальний Animal.
Animal generalAnimal = myDog;
// А от викликати MakeSound() ми можемо!
generalAnimal.MakeSound(); // Виведе "Гав-гав!" (Не "Якийсь звук...", а саме "Гав-гав!")
Що тут сталося?
- Коли ми написали Animal generalAnimal = myDog;, ми не створили нову тварину. Ми просто взяли об’єкт myDog (який насправді є Dog) і поклали його в «коробку», позначену як Animal.
- На етапі компіляції (коли код перетворюється на байт-код) змінна generalAnimal має тип Animal. Тому компілятор «знає», що вона може викликати тільки ті методи й властивості, які є в класі Animal.
- Але найцікавіше: коли доходить до виконання програми (на етапі runtime), і ми викликаємо generalAnimal.MakeSound(), середовище виконання .NET дивиться не на тип змінної (Animal), а на фактичний тип об’єкта, який у ній міститься (Dog)! І оскільки метод MakeSound() був позначений як virtual у Animal і override у Dog, викликається саме реалізація з Dog.
Ось ця магія, коли поведінка методу визначається фактичним типом об’єкта під час виконання, а не типом змінної, називається динамічною диспетчеризацією або поліморфізмом часу виконання.
Уявіть собі коробку: зовні на ній написано «Фрукт» (це тип змінної Animal). Усередину ви кладете яблуко (це об’єкт Dog). Коли ви просите: «Фрукт, видай звук!» (виклик MakeSound()), коробка, знаючи, що всередині яблуко, відтворює звук «хрум», а не якийсь загальний «фруктовий» звук.
2. Демонстрація поліморфізму в дії
Найкращий спосіб зрозуміти поліморфізм — побачити його в дії, особливо коли у вас не один, а цілий набір об’єктів.
Давайте розширимо наш застосунок «Мій маленький зоопарк». Уявімо, що ми працюємо у ветеринарній клініці, і до нас привозять різних тварин на огляд. Ми хочемо, щоб кожен огляд проходив за своїми правилами, але водночас код для виклику огляду залишався універсальним.
Спочатку додамо в наш базовий клас Animal і його похідні класи Dog і Cat новий віртуальний метод Examine():
public class Animal
{
public string Name { get; set; }
public Animal(string name) { Name = name; }
public virtual void MakeSound() { Console.WriteLine($"{Name} видає якийсь звук..."); }
public virtual void Examine()
{
Console.WriteLine($"Огляд {Name}:");
Console.WriteLine(" - Перевірка дихання.");
Console.WriteLine(" - Вимірювання температури.");
}
}
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound() { Console.WriteLine($"{Name} каже: Гав!"); }
public override void Examine()
{
base.Examine();
Console.WriteLine(" - Перевірка зубів.");
Console.WriteLine(" - Щеплення від сказу.");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound() { Console.WriteLine($"{Name} каже: Мяу!"); }
public override void Examine()
{
base.Examine();
Console.WriteLine(" - Перевірка кігтів.");
Console.WriteLine(" - Таблетка від глистів.");
}
}
Тепер у нас є спеціалізовані методи Examine() для собак і котів. Зверніть увагу на base.Examine(); у перевизначених методах. Це дає змогу спочатку виконати загальні кроки огляду (з базового класу Animal), а потім додати специфічні для кожного виду тварини. Це дуже зручно!
Тепер уявімо нашу ветеринарну клініку. У нас є список тварин, які приїхали на огляд:
class Program
{
static void Main()
{
Animal[] animals = {
new Dog("Рекс"),
new Cat("Мурка"),
new Animal("Кролик")
};
Console.WriteLine("--- Огляд тварин ---");
foreach (Animal animal in animals)
{
animal.Examine();
Console.WriteLine();
}
Console.WriteLine("--- Час голосів ---");
foreach (Animal animal in animals)
animal.MakeSound();
}
}
Результат:
--- Огляд тварин ---
Огляд Рекс:
- Перевірка дихання.
- Вимірювання температури.
- Перевірка зубів.
- Щеплення від сказу.
Огляд Мурка:
- Перевірка дихання.
- Вимірювання температури.
- Перевірка кігтів.
- Таблетка від глистів.
Огляд Кролик:
- Перевірка дихання.
- Вимірювання температури.
--- Час голосів ---
Рекс каже: Гав!
Мурка каже: Мяу!
Кролик видає якийсь звук...
Ось вона, сила поліморфізму! Ми написали один і той самий цикл foreach (Animal animal in animals), і в кожній ітерації викликали один і той самий метод animal.Examine() (або animal.MakeSound()). Але щоразу виконувалася правильна, специфічна реалізація цього методу для собаки, кота чи просто загальної тварини. Код залишається простим, чистим і універсальним, а деталі реалізації сховані всередині кожного класу.
Якщо завтра до нас привезуть хом’ячка Hamster, буде достатньо створити клас Hamster : Animal, перевизначити в ньому Examine() — і він працюватиме в нашій загальній системі ветеринарного огляду без жодних змін у циклі foreach! Оце й є справжня сила поліморфізму.
3. Роль поліморфізму в ООП
Поліморфізм — це не просто гарне слово чи трюк із кодом. Це фундаментальний принцип, який робить ваші програми гнучкими, розширюваними й легкими для підтримки. Давайте розберемося, яку роль він відіграє:
Гнучкість і розширюваність (Open/Closed Principle)
Це, мабуть, найголовніша перевага. Поліморфізм дає змогу писати код, який відкритий для розширення, але закритий для змін. Що це означає?
- Відкритий для розширення: Ви можете додавати нові типи тварин (наприклад, Bird, Fish, Hamster), просто створюючи нові класи, що наслідують Animal і перевизначають потрібні методи.
- Закритий для змін: Вам не потрібно змінювати наявний код, який працює з Animal[]. Цикл foreach, що викликає animal.Examine(), залишиться абсолютно незмінним, скільки б нових видів тварин ви не додали.
Уявіть, скільки операторів if-else if або switch вам довелося б писати й постійно змінювати, якби не було поліморфізму! Це був би справжній головний біль.
Спрощення коду
Поліморфізм значно спрощує код, що працює з колекціями різнотипних, але пов’язаних об’єктів. Замість того щоб визначати тип кожного об’єкта й потім викликати відповідний метод, ви просто викликаєте спільний метод базового класу — і система сама визначає, яку саме реалізацію викликати.
Це як мати одну кнопку «Відкрити» для різних дверей: звичайної, розсувної, обертової. Вам не потрібно щоразу тягнути, штовхати чи крутити — просто «відкрити», а конкретні двері зроблять це по‑своєму.
Абстракція
Поліморфізм тісно пов’язаний з абстракцією — ще одним стовпом ООП, про який ми детально поговоримо у наступних лекціях. Він дозволяє зосередитися на тому, що робить об’єкт (наприклад, «видає звук», «оглядається»), замість як він це робить. Ви працюєте з абстрактною ідеєю «тварини», а не з конкретною «собакою» чи «кішкою». Це дає змогу створювати високорівневий, чистий код, який не залежить від низькорівневих деталей реалізації.
Повторне використання коду
Хоча наслідування саме по собі забезпечує повторне використання коду шляхом перевикористання властивостей і методів базового класу, поліморфізм підсилює це. Він дозволяє створювати універсальні алгоритми й структури даних (наприклад, Animal[]), які можуть оперувати будь-якими об’єктами, успадкованими від базового класу, без потреби дублювати логіку для кожного похідного типу.
По суті, поліморфізм робить ваш код більш «професійним» і готовим до змін — це критично важливо в реальній розробці, де вимоги постійно змінюються.
4. Корисні нюанси
Схема поліморфізму
classDiagram
class Animal {
+MakeSound()
}
class Dog {
+MakeSound()
}
class Cat {
+MakeSound()
}
class Parrot {
+MakeSound()
}
Animal <|-- Dog
Animal <|-- Cat
Animal <|-- Parrot
Під час виклику MakeSound() через посилання типу Animal буде викликаний метод того класу, об’єкт якого фактично міститься у змінній.
Використання поліморфізму з масивами й колекціями
Дуже поширене завдання — пройтися по колекції різнорідних сутностей і «запустити» для всіх одну й ту саму логіку.
// У нашому основному тренувальному додатку:
Animal[] zoo = { new Dog("Бровко"), new Cat("Мурчик"), new Parrot("Коко") };
foreach (Animal animal in zoo)
{
animal.MakeSound();
}
У реальних проєктах так реалізують, наприклад, обробку подій, малювання на екрані (кожна фігура — по‑своєму), обробку платежів (різні типи карт і сервісів).
Як працює поліморфізм
| Тип об’єкта | Тип змінної | Який метод викликається? |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Головне — метод має бути virtual або abstract у базовому класі й override у нащадку!
5. Типові помилки під час роботи з поліморфізмом
На практиці студенти часто припускаються таких помилок:
- Не позначаєте метод virtual у базовому класі — і замість поліморфізму отримуєте завжди один варіант поведінки.
- Плутаєтеся: який тип потрібен для змінної? Завжди пам’ятайте: змінна має тип (наприклад, Animal), а об’єкт, який ви туди кладете, — екземпляр (наприклад, new Dog()).
- Намагаєтеся використовувати члени, яких немає в базовому класі, через змінну базового типу. Наприклад:
Animal pet = new Dog();
pet.Bark(); // Помилка! У Animal немає Bark()
Що з цим робити? Якщо дуже потрібно, скористайтеся перетворенням типу, але намагайтеся структурувати код так, щоб працювати лише з тими методами, які є в базовому класі.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ