OOP Course
Сьогодні

Підрозділ 4.1

Успадкування

Пояснює успадкування класів, відношення is-a, доступ до членів базового класу, ключове слово base і порядок виклику конструкторів.

4.1. Успадкування

Успадкування (inheritance) — один із трьох фундаментальних принципів об'єктно-орієнтованого програмування поряд із інкапсуляцією та поліморфізмом. Воно дозволяє одному класу перейняти функціональність іншого: поля, властивості та методи базового класу стають доступними в похідному без повторного написання коду. Це не просто зручність — це архітектурний інструмент для моделювання реальних відносин між сутностями.

Уявімо клінічну систему, де є різні типи людей: пацієнти, лікарі, адміністратори. Всі вони мають спільні атрибути (ім'я, вік) і спільні дії (ідентифікація, виведення інформації). Замість того, щоб дублювати цей код у кожному класі окремо, ми виносимо спільне в базовий клас Person і успадковуємо від нього.

Базовий та похідний клас

Клас, від якого успадковують, називається базовим класом (base class, superclass, батьківський клас). Клас, який успадковує, — похідним класом (derived class, subclass, дочірній клас). Для позначення успадкування після назви похідного класу через двокрапку вказується базовий клас:

class ПохіднийКлас : БазовийКлас
{
    // нові члени похідного класу
}

Нехай у нас є базовий клас Person:

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void Print()
    {
        Console.WriteLine($"{Name}, {Age} років");
    }
}

Тоді Patient і Doctor — похідні класи від Person:

Клас Patient успадкував від Person властивості Name, Age та метод Print() — і не потрібно було їх писати заново. Додатково Patient визначає власну властивість Diagnosis та метод PrintInfo(). Те саме для Doctor.

Ієрархія успадкування: Person → Patient, Doctor

Відношення is-a та has-a

Успадкування моделює відношення is-a («є»): об'єкт похідного класу є об'єктом базового класу. Patient is-a Person — пацієнт є людиною. Це означає, що там, де очікується Person, можна передати Patient:

Змінна типу Person може зберігати будь-який об'єкт з ієрархії — це основа поліморфізму, який детально розглядається в наступних розділах.

Відношення is-a слід відрізняти від has-a («має»): якщо одна сутність містить іншу як компонент, використовується композиція, а не успадкування. Наприклад, клас Appointment (прийом) має Patient і Doctor — це has-a, тому Appointment не має від них успадковувати. Чіткий вибір між is-a і has-a є ознакою правильно спроектованої об'єктної моделі.

Клас Object як базовий для всіх

За замовчуванням усі класи в C# неявно успадковують від базового класу System.Object (або просто object). Це означає, що навіть клас Person, для якого не вказано явного базового класу, насправді успадковує від Object. А Patient і Doctor перебувають у ланцюжку: Patient → Person → Object.

Саме тому будь-який об'єкт у C# завжди має такі методи:

  • ToString() — рядкове представлення об'єкта
  • Equals(object obj) — перевірка рівності
  • GetHashCode() — хеш-код для колекцій
  • GetType() — тип об'єкта під час виконання

Обмеження успадкування

C# накладає кілька важливих обмежень:

  • Одиночне успадкування — клас може мати лише один безпосередній базовий клас. Множинне успадкування класів не підтримується (на відміну від інтерфейсів, які розглядаються в наступних розділах).
  • Рівні доступу — якщо базовий клас має модифікатор internal, похідний клас може бути лише internal або private, але не public. Якщо класи в різних збірках, похідний клас може успадковувати лише від public-класу.
  • Статичний клас — від статичного класу неможливо успадковувати.
  • Запечатаний клас — клас з модифікатором sealed не допускає спадкоємців.
sealed class AdministratorAccount
{
    // Від цього класу не можна успадковувати
}

Модифікатор sealed корисний там, де необхідно запобігти зміні поведінки класу через похідні класи — наприклад, для критичних класів безпеки або коли клас спроектований настільки специфічно, що розширення може порушити його коректність. У стандартній бібліотеці .NET клас String є sealed саме з міркувань безпеки та продуктивності.

Доступ до членів базового класу

Похідний клас успадковує члени базового, але рівні доступу залишаються в силі. Розглянемо ситуацію, коли Person має приватне поле:

Похідний клас може звертатися лише до членів базового класу з такими модифікаторами:

  • public — доступний усюди
  • protected — доступний у базовому та всіх похідних класах
  • internal — доступний у межах однієї збірки
  • protected internal — union: збірка або похідний клас
  • private protected — intersection: та сама збірка і похідний клас

Члени з private залишаються виключно в тому класі, де оголошені.

Модифікатор protected

Модифікатор protected створений спеціально для ієрархій успадкування. Він дозволяє базовому класу відкрити доступ до своїх членів для всіх похідних класів, але закрити їх від зовнішнього світу. Це золота середина між public (надто відкрито) і private (повністю закрито):

Поле medicalRecordId оголошене як protected: клас Patient може його читати і використовувати, але зовнішній код (patient.medicalRecordId) отримає помилку компіляції. Поле _internalCode залишається private — недоступне навіть для Patient.

Ключове слово base

Ключове слово base дозволяє явно звернутися до членів безпосереднього базового класу: викликати конструктор або метод. Це особливо важливо, коли похідний клас перевизначає метод базового і водночас хоче використати оригінальну реалізацію.

У конструкторі Doctor вираз : base(name, age) передає ім'я та вік конструктору Person — немає потреби дублювати код ініціалізації. У методі PrintInfo() виклик base.Print() виконує базову реалізацію, після чого виводить додаткову інформацію.

Конструктори у похідних класах

Конструктори не передаються похідному класу при успадкуванні — це принципова відмінність від методів і властивостей. Кожен клас повинен мати власні конструктори.

Якщо в базовому класі є лише конструктори з параметрами (і немає конструктора без параметрів), то кожен конструктор похідного класу зобов'язаний явно викликати один із конструкторів базового через base(...):

Якщо Patient не викличе base(name, age), компілятор видасть помилку — він не знає, як ініціалізувати успадковані властивості Name і Age.

Якщо ж базовий клас має конструктор без параметрів, похідний клас може не викликати base() явно — компілятор підставить його неявно. У такому разі наступні два конструктори еквівалентні:

// Явний виклик
public Patient(string diagnosis) : base()
{
    Diagnosis = diagnosis;
}

// Неявний виклик (компілятор додасть base() автоматично)
public Patient(string diagnosis)
{
    Diagnosis = diagnosis;
}

Порядок виклику конструкторів

При створенні об'єкта похідного класу конструктори викликаються в строго визначеному порядку: від найбазовішого до найпохіднішого. Тіла конструкторів виконуються у зворотному порядку відносно делегування — спочатку відпрацьовують більш загальні класи, потім — специфічні.

Розглянемо приклад з ланцюжком:

Виконання розгортається так:

  1. new Doctor(...) → викликається Doctor(name, age, spec), але спочатку делегує base(name, age)
  2. Person(name, age) → не виконується одразу, делегує this(name)
  3. Person(name) → делегує неявно Object()
  4. Виконується Object() — найбазовіший конструктор
  5. Виконується тіло Person(name) → виводить Person(name): Олена Коваль
  6. Виконується тіло Person(name, age) → виводить Person(name, age): Олена Коваль, 38
  7. Виконується тіло Doctor(...) → виводить Doctor(name, age, spec): Кардіологія

Порядок виклику конструкторів у ланцюжку успадкування

Цей порядок гарантує, що до моменту виконання конструктора похідного класу базовий клас вже повністю ініціалізований. Якби Doctor намагався звернутися до Name у своєму конструкторі — властивість вже була б встановлена конструктором Person.

Запечатаний клас: sealed

Модифікатор sealed повністю забороняє успадкування від класу. Це явна архітектурна декларація: «цей клас спроектований як кінцевий, розширення через успадкування не передбачено».

sealed клас може сам успадковувати від інших класів (як CertifiedSurgeon від Doctor), але не дозволяє мати власних нащадків. У стандартній бібліотеці .NET таким чином оголошені String, StringBuilder та багато інших класів — їх не можна розширити через успадкування.

Окрім захисту архітектури, sealed дає компілятору можливість генерувати ефективніший код: якщо клас запечатаний, виклики його методів можна не диспетчеризувати через таблицю віртуальних методів.

Розроблено Tomka Yurii · © 2026 ·