JavaRush /Курси /C# SELF /Створення ієрархії класів

Створення ієрархії класів

C# SELF
Рівень 20 , Лекція 4
Відкрита

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. Практика у нашому застосунку

Давайте пов’яжемо ідею ієрархії із застосунком — наприклад, у нас є задачі різних типів:

  1. Task (базовий клас): Будь-яка задача має назву та статус виконання.
  2. WorkTask (робоча): Додатково має дедлайн.
  3. 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(...) у конструкторі. Це може призвести до неповної або неправильної ініціалізації базової частини об’єкта і до помилок, які важко відстежити.

1
Опитування
Ponyattya nasliduvannya, рівень 20, лекція 4
Недоступний
Ponyattya nasliduvannya
Nasliduvannya ta ierarkhiya klasiv
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ