1. Вступ
Уявіть світ без ієрархій: тисячі класів Person, Animal, Vehicle — і все це — абсолютно окремо. Не дивно, що програмісти в такому світі не дожили б навіть до обіду — заплуталися б остаточно! У реальних проєктах нам часто потрібні об’єкти, здатні виконувати спільні дії (наприклад, усі тварини можуть рухатися), але водночас кожна має свою «родзинку» (риба плаває, а птах літає).
Саме ієрархії класів дають змогу відобразити зв’язки між сутностями, щоб програмування було творчою роботою, а не нескінченною боротьбою з копіюванням коду.
Розгляньмо приклад. Припустімо, у нас є базовий клас Animal. Усі тварини можуть видавати звук. Але лише кішки нявкають, собаки гавкають, а папуги інколи навіть розповідають кілька анекдотів. Ми хочемо відобразити це в коді через ієрархію.
Базовий клас
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
// Базовий метод: можна перевизначити в похідних класах
public virtual void Speak()
{
Console.WriteLine("Тварина видає якийсь звук...");
}
}
Ми позначили метод Speak() словом virtual. Це позначка для похідних класів: за потреби ви можете перевизначити цей метод.
Створюємо ієрархію: похідні класи
Створімо клас Cat, який успадковує від Animal:
public class Cat : Animal
{
public Cat(string name) : base(name) { }
// Перевизначаємо Speak — кішки ж не можуть просто гарчати!
public override void Speak()
{
Console.WriteLine($"{Name} каже: Мяу!");
}
}
І клас Dog:
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} каже: Гав!");
}
}
А якщо у нас буде звичайна тварина, яка не вміє говорити? Тоді можна використати базовий клас, нічого не перевизначаючи.
Візуалізація — дерево ієрархії
. Animal
/ \
Cat Dog
- Animal — базовий клас
- Cat, Dog — дочірні (похідні) класи
2. Давайте напишемо код, який використовує таку ієрархію
Продовжимо роботу над нашим консольним застосунком.
Припустімо, у нас є колекція тварин, і ми хочемо, щоб кожна з них сказала щось характерне:
Animal[] zoo = new Animal[]
{
new Cat("Барсик"),
new Dog("Рекс"),
new Animal("Загадкова істота")
};
foreach (Animal animal in zoo)
{
animal.Speak();
}
Очікуваний результат:
Барсик каже: Мяу!
Рекс каже: Гав!
Тварина видає якийсь звук...
Ось так, завдяки ієрархії та поліморфізму (ми невдовзі розглянемо його докладно; суть у тому, що викликається правильна версія методу залежно від фактичного типу об’єкта), ваш застосунок стає гнучким і розширюваним.
3. Додаємо нові методи та поля
Отже, з «озвученням» розібралися. Але «усі тварини» — це занадто загально. Наприклад, кішка може мати девʼять життів, а собака вміє приносити палицю.
Додаємо унікальну поведінку
У класі-нащадку можна додавати власні методи та поля:
public class Cat : Animal
{
public int Lives { get; private set; } = 9;
public Cat(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} каже: Мяу! У мене {Lives} життів.");
}
public void LoseLife()
{
if (Lives > 0)
{
Lives--;
Console.WriteLine($"{Name} втратив одне життя. Залишилося: {Lives}");
}
else
{
Console.WriteLine($"{Name} вже використав усі життя!");
}
}
}
Використовуємо в коді:
var barsik = new Cat("Барсик");
barsik.Speak(); // Барсик каже: Мяу! У мене 9 життів.
barsik.LoseLife(); // Барсик втратив одне життя. Залишилося: 8
Додаємо нові класи: розширюємо «зоопарк»
Ви вже вмієте створювати похідні класи. Додамо, наприклад, папугу:
public class Parrot : Animal
{
public Parrot(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} каже: Привіт, людино!");
}
public void Repeat(string phrase)
{
Console.WriteLine($"{Name} повторює: {phrase}");
}
}
Тепер ви легко розширюєте систему, не змінюючи наявний код:
var keshka = new Parrot("Кеша");
keshka.Speak(); // Кеша каже: Привіт, людино!
keshka.Repeat("Вчись, студенте!"); // Кеша повторює: Вчись, студенте!
4. Порівняння поведінки тварин
| Тип | Метод Speak() | Власне поле | Додаткова поведінка |
|---|---|---|---|
| Animal | Так (virtual) | Name | — |
| Cat | Так (override) | Lives | LoseLife() |
| Dog | Так (override) | — | — |
| Parrot | Так (override) | — | Repeat(string) |
Як виглядає ієрархія класів у пам’яті (блок-схема)
Animal (Name)
├── Cat (Lives)
├── Dog
└── Parrot (Repeat)
5. Практика у нашому застосунку
Давайте пов’яжемо ідею ієрархії із застосунком — наприклад, у нас є задачі різних типів:
- Task (базовий клас): Будь-яка задача має назву та статус виконання.
- WorkTask (робоча): Додатково має дедлайн.
- HomeTask (домашня): Може мати пріоритет («Дуже важливо», «Так собі»).
Почнемо з базового класу:
public class Task
{
public string Title { get; set; }
public bool IsCompleted { get; private set; }
public Task(string title)
{
Title = title;
}
public virtual void Complete()
{
IsCompleted = true;
Console.WriteLine($"Задача «{Title}» виконана!");
}
}
Тепер додаємо робочу задачу:
public class WorkTask : Task
{
public DateTime Deadline { get; set; }
public WorkTask(string title, DateTime deadline)
: base(title)
{
Deadline = deadline;
}
public override void Complete()
{
base.Complete();
Console.WriteLine($"Термін виконання: {Deadline:d}");
}
}
І домашню задачу:
public class HomeTask : Task
{
public string Priority { get; set; }
public HomeTask(string title, string priority)
: base(title)
{
Priority = priority;
}
// Можна не перевизначати Complete, якщо поведінки базового класу достатньо
}
Створімо список задач у програмі:
List<Task> tasks = new List<Task>
{
new WorkTask("Відправити звіт", DateTime.Today.AddDays(2)),
new HomeTask("Помити посуд", "Дуже важливо"),
new Task("Прочитати лекцію про наслідування")
};
foreach (Task task in tasks)
{
Console.WriteLine($"Задача: {task.Title}");
task.Complete();
}
Очікуваний результат:
Задача: Відправити звіт
Задача «Відправити звіт» виконана!
Термін виконання: 13.07.2025
Задача: Помити посуд
Задача «Помити посуд» виконана!
Задача: Прочитати лекцію про наслідування
Задача «Прочитати лекцію про наслідування» виконана!
Бачите, як зручно: усі задачі зберігаються разом, ми обробляємо їх однаково, а специфіка проявляється саме там, де потрібно.
6. Типові помилки при використанні наслідування
Помилка № 1: спроба перевизначити метод, який не оголошено як virtual.
Якщо метод у базовому класі не позначено як virtual, його не можна перевизначити у похідних класах. У результаті вся гнучкість поліморфізму втрачається, та ієрархія стає марною.
Помилка № 2: наслідування без логічного зв’язку між сутностями.
Не варто використовувати наслідування, якщо об’єкти не пов’язані за змістом. Наприклад, Коло справді є Фігурою, але Кінь як Транспортний засіб — сумнівне рішення. Виняток — специфічні контексти (наприклад, середньовічна гра), де такий зв’язок може бути виправданий.
Помилка № 3: надто глибокі ієрархії.
Коли структура класів має 5–6 і більше рівнів, код стає складно читати, супроводжувати й тестувати. Це сигнал, що варто розглянути композицію як альтернативу наслідуванню.
Помилка № 4: забули викликати базовий конструктор.
Під час додавання нових властивостей у похідному класі легко забути явно викликати base(...) у конструкторі. Це може призвести до неповної або неправильної ініціалізації базової частини об’єкта і до помилок, які важко відстежити.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ