1. Поліморфізм на практиці
У програмуванні поліморфізм — це як універсальний пульт дистанційного керування: ви натискаєте одну кнопку «Гучність+», а пульт керує телевізором, аудіосистемою чи кондиціонером — кожен пристрій реагує по-своєму, зате інтерфейс єдиний! Аналогічно, об’єкти різних типів можуть по-різному реагувати на один і той самий виклик методу, якщо цей метод визначено в їхньому спільному базовому класі як virtual.
У C# поліморфізм проявляється, коли змінна типу базового класу (або інтерфейсу) може «містити» об’єкт будь-якого його нащадка, а виклики virtual-методів для такої змінної призводять до виконання «справжньої» реалізації — тієї, що визначена в самому об’єкті. Це лежить в основі архітектур, де логіка змінюється динамічно.
Чи потрібно це десь, окрім підручників?
Так! Практично в будь-якому проєкті, де є робота з різними, але схожими об’єктами — від тварин у зоопарку до елементів графічного інтерфейсу, від обробників подій до систем документообігу.
- Дозволяє створювати універсальні алгоритми — код, який працює з абстрактними сутностями, не переймаючись деталями конкретної реалізації.
- Гарантує розширюваність — додавайте хоч сотню нових видів «тварин», «фігур», «обробників», не змінюючи наявний код.
- Зменшує залежність компонентів програми один від одного (це одна з важливих тем для співбесід і для архітектури!).
2. Основний синтаксис і механіка
Давайте згадаємо й доопрацюємо наші класи Animal, Dog, Cat, щоб показати поліморфізм у дії. Раз уже ми почали із застосунку «Віртуальний зоопарк», продовжимо його розвивати.
Базові класи з virtual-методами
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
// Віртуальний метод — його можна перевизначити
public virtual void MakeSound()
{
Console.WriteLine($"{Name} видає якийсь звук...");
}
}
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} каже: Гав-гав!");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} каже: Мяу!");
}
}
Використання поліморфізму: приклад із колекцією тварин
Тепер уявімо, що маємо список тварин — як домашніх, так і не дуже — і хочемо всіх їх «змусити» видати звук. Без поліморфізму довелося б писати перевірки типів і робити купу дублювального коду. З поліморфізмом — усе елегантно й компактно!
// Створюємо масив тварин різних видів
Animal[] animals = new Animal[]
{
new Dog("Бровко"),
new Cat("Мурка"),
new Dog("Рекс"),
new Cat("Сніжка"),
};
// Перебираємо весь масив і просимо кожного видати звук
foreach (var animal in animals)
{
animal.MakeSound(); // Викличеться метод із Dog або Cat, а не з Animal!
}
Результат:
Бровко каже: Гав-гав!
Мурка каже: Мяу!
Рекс каже: Гав-гав!
Сніжка каже: Мяу!
Ось і вся магія: один універсальний код — різні результати залежно від реального типу об’єкта.
Діаграма: як працює поліморфізм
Animal (базовий клас)
/ \
Dog Cat
При виклику animal.MakeSound(), де Animal може містити екземпляр Dog або Cat, середовище .NET під час виконання саме визначає, який саме метод MakeSound() потрібно викликати.
3. Рішення типових задач із поліморфізмом
Приклад 1: Універсальний список, різні дії
Припустімо, ви створюєте якусь гру. У вас є базовий клас GameObject, похідні — вороги, союзники, перешкоди. Усі вони можуть рухатися, в усіх є метод Update(), але реалізація різна.
public class GameObject
{
public virtual void Update() { }
}
public class Enemy : GameObject
{
public override void Update()
{
Console.WriteLine("Ворог наступає!");
}
}
public class Friend : GameObject
{
public override void Update()
{
Console.WriteLine("Союзник допомагає!");
}
}
GameObject[] objects = new GameObject[]
{
new Enemy(),
new Friend(),
new Enemy()
};
foreach (var obj in objects)
{
obj.Update();
}
// Виведе:
// Ворог наступає!
// Союзник допомагає!
// Ворог наступає!
Приклад 2: Передавання об’єктів у методи
Можна приймати параметр базового типу, а використовувати при цьому будь-який тип-нащадок. Це дуже економить сили, особливо якщо об’єкт розширюватиметься новими нащадками.
public static void FeedAnimal(Animal animal)
{
Console.Write($"{animal.Name}: ");
animal.MakeSound();
Console.WriteLine("І отримує їжу.");
}
FeedAnimal(new Dog("Рекс"));
FeedAnimal(new Cat("Сіма"));
// Результат:
// Рекс: Рекс каже: Гав-гав!
// І отримує їжу.
// Сіма: Сіма каже: Мяу!
// І отримує їжу.
Зверніть увагу: написати такий метод без поліморфізму було б дуже клопітно — довелося б окремо перевіряти тип тварини, робити багато гілок і викликати правильні методи вручну.
Важливе зауваження: звʼязування на етапі виконання
Поліморфізм працює завдяки тому, що виклики virtual-методів здійснюються динамічно, тобто під час виконання програми. Це називається пізнім звʼязуванням (late binding). Навіть якщо ми знаємо змінну як Animal, метод усе одно буде викликано той, що визначений у реальному, «справжньому» об’єкті.
Якщо ж метод не позначений як virtual, виклик завжди йтиме за тим кодом, який оголошено для змінної (а не для об’єкта). Тож не шкодуйте ключового слова virtual, якщо хочете отримати гнучкість!
4. Практика: розширюємо наш застосунок
Припустімо, ви вирішили додати у свій віртуальний зоопарк нову можливість: тепер кожна тварина має виконувати не лише MakeSound(), а й рухатися (Move()). Але в усіх це реалізується по-різному.
1. Додаємо у базовий клас virtual-метод
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
public virtual void MakeSound()
{
Console.WriteLine($"{Name} видає якийсь звук...");
}
public virtual void Move()
{
Console.WriteLine($"{Name} рухається невизначеним способом...");
}
}
2. У похідних класах реалізуємо Move() по-своєму
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} каже: Гав-гав!");
}
public override void Move()
{
Console.WriteLine($"{Name} біжить за палкою.");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} каже: Мяу!");
}
public override void Move()
{
Console.WriteLine($"{Name} крадеться мʼякими лапками.");
}
}
3. Використовуємо обидва методи в колекції
Animal[] animals = new Animal[]
{
new Dog("Бім"),
new Cat("Люся")
};
foreach (var animal in animals)
{
animal.MakeSound();
animal.Move();
}
Результат:
Бім каже: Гав-гав!
Бім біжить за палкою.
Люся каже: Мяу!
Люся крадеться мʼякими лапками.
У реальних застосунках такий підхід дозволяє писати дуже потужні й повторно використовувані модулі. Наприклад, в іграх це основа для всіх менеджерів об’єктів — від NPC до ефектів.
5. Практична задача: малюємо різні фігури
Погляньмо на приклад не із зоопарку, а з графіки. Створимо базовий клас Shape із virtual-методом Draw(), а далі розширимо його.
public class Shape
{
public virtual void Draw()
{
Console.WriteLine("Малюється невизначена фігура.");
}
}
public class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Малюється коло.");
}
}
public class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("Малюється прямокутник.");
}
}
// Колекція фігур
Shape[] shapes = new Shape[]
{
new Circle(),
new Rectangle(),
new Circle()
};
foreach (var shape in shapes)
{
shape.Draw();
}
Тут Draw() викликається на змінній типу Shape, але фактично викликаються методи з Circle або Rectangle. У реальних графічних редакторах і бібліотеках, наприклад у WinForms чи WPF, усе працює саме так.
6. Загальний підхід: пишемо універсальні алгоритми
Поліморфізм перетворює ваш код на гнучкий і розширюваний інструмент. Наприклад, у колекції можуть бути не лише Dog і Cat, а й Hamster, Parrot — і хто завгодно ще — і ви не пишете жодного нового рядка, щоб обробити їх усіма спільними способами.
Крім того, ви без проблем можете передавати об’єкти-нащадки в методи, які приймають базовий тип:
void PrintAnimalInfo(Animal animal)
{
Console.WriteLine($"Імʼя: {animal.Name}");
animal.MakeSound();
animal.Move();
}
Animal hamster = new Animal("Хома");
Animal dog = new Dog("Лорд");
PrintAnimalInfo(hamster); // Використає метод з Animal
PrintAnimalInfo(dog); // Використає версію Dog
7. Корисні нюанси
Часті запитання й підводні камені
Багато початківців думають, що якщо створити об’єкт Dog, а потім оголосити змінну Dog myDog, то різниці з Animal myDog не буде. Насправді, якщо оголосити змінну як Animal, вона «бачить» лише те, що є в Animal (окрім перевизначених методів), а оголошена як Dog — бачить усе: і Bark(), і специфічні властивості.
Також важливо знати: якщо метод у базовому класі не позначений як virtual, його не можна перевизначити. Якщо ви спробуєте в нащадку написати override для такого методу — компілятор повідомить про помилку.
До речі, якщо ви справді хочете замінити метод, який не virtual, використовуйте ключове слово new, але це вже тема для просунутих (і конфліктних!) випадків.
Чому це люблять питати на співбесідах?
Бо поліморфізм — це як швейцарський ніж для програміста: якщо ви розумієте, як його застосувати, вмієте будувати розширювані й підтримувані системи, ваш код буде не лише працювати, а й жити довго і щасливо. Наприклад, вас можуть попросити реалізувати обробку різних форм оплати (класика: BankCard, PayPal, Bitcoin) — і очікують, що ви зробите спільний інтерфейс (або базовий абстрактний клас) з методом Pay(), а далі клієнти зможуть викликати Pay(BankCard), Pay(PayPal), Pay(Bitcoin) — і навіть не знати, як це працює всередині.
8. Типові помилки початківців
Одна з найчастіших помилок — спроба викликати специфічний метод похідного класу через змінну базового типу. Наприклад, так:
Animal animal = new Dog("Тузик");
animal.Bark(); // Помилка! У Animal немає методу Bark.
Чому це не працює? Тому що змінна типу Animal «бачить» лише те, що оголошено в Animal, навіть якщо насправді там Dog. Викликати можна лише те, що було визначено в базовому класі — і перевизначено (override) у нащадках.
Якщо вам усе ж потрібно викликати метод, який є лише у Dog — доведеться виконати перетворення типу:
Animal animal = new Dog("Тузик");
if (animal is Dog dog)
{
dog.Bark();
}
Але часто, якщо ви так робите, це може свідчити, що десь порушена архітектура (або ви зловживаєте наслідуванням).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ