1. Вступ
У цій лекції ми розглянемо, як у C# працює синтаксис наслідування: навчимося оголошувати похідні класи, з’ясуємо, що таке ключове слово base, і як за його допомогою звертатися до батьківського класу. Це не лише полегшить вам життя під час роботи з кодом, а й зробить програму значно елегантнішою (а іноді навіть дозволить зайвий раз поспати — адже виправляти помилки копіпасту не доведеться).
Ось простий приклад із життя: маємо клас Vehicle (транспортний засіб) — базовий, універсальний шаблон. Усі транспортні засоби можуть їхати, у них є колір, виробник тощо:
public class Vehicle
{
public string Brand { get; set; }
public string Color { get; set; }
public void Drive()
{
Console.WriteLine("Поїхали!");
}
}
А тепер ми хочемо додати новий вид транспорту — автомобіль. Автомобіль усе ще транспортний засіб, але має свої особливості (наприклад, кількість дверей). Було б дивно знову описувати колір і виробника — це вже визначено.
Наслідування дає змогу сказати: «Автомобіль — це транспортний засіб, лише з низкою додаткових можливостей».
2. Як оголосити похідний клас
У C# для створення похідного класу використовується двокрапка :. Після імені класу зазначаємо базовий клас.
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
}
Можна читати так: клас Car наслідує всі поля й методи класу Vehicle і додає власні можливості — наприклад, властивість із кількістю дверей.
Зверніть увагу: у C# підтримується лише одиничне наслідування класів (тобто клас може мати тільки один базовий клас). Натомість інтерфейсів клас може реалізовувати скільки завгодно (про них поговоримо згодом).
Як це працює?
Коли ви оголошуєте:
Car myCar = new Car();
…то ваш об’єкт myCar має доступ і до всіх полів/методів Vehicle, і до власних властивостей. Приклад:
myCar.Brand = "Toyota";
myCar.Color = "Синій";
myCar.NumberOfDoors = 4;
myCar.Drive(); // Наслідуваний метод
Практичний сценарій:
У нашому навчальному застосунку можна зробити меню транспортних засобів, де користувач обирає тип транспорту, а потім програма показує всі його властивості. Додаючи новий тип транспорту, ви писатимете мінімум коду!
3. Наслідування конструкторів
Коли ми створюємо об’єкти похідного класу, часто потрібно ініціалізувати властивості базового класу через конструктор. Наприклад, щоб напевно не забути вказати марку й колір:
public class Vehicle
{
public string Brand { get; set; }
public string Color { get; set; }
public Vehicle(string brand, string color)
{
Brand = brand;
Color = color;
}
}
Тепер у конструкторі похідного класу можна й потрібно викликати конструктор базового класу. Для цього використовують ключове слово base:
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public Car(string brand, string color, int numberOfDoors)
: base(brand, color) // викликаємо конструктор Vehicle!
{
NumberOfDoors = numberOfDoors;
}
}
Як це виглядає на практиці?
Car bmw = new Car("BMW", "Чорний", 4);
Console.WriteLine($"{bmw.Brand}, {bmw.Color}, дверей: {bmw.NumberOfDoors}");
bmw.Drive(); // Все ще працює!
Отже, bmw — і автомобіль, і транспортний засіб водночас. Він пам’ятає про свій базовий клас.
4. Відмінність base від this
У C# є два ключові слова для роботи з членами класу: this і base.
- this — звернення до поточного об’єкта (наприклад, його власного поля чи методу).
- base — звернення до члена базового класу (тобто до «батька»).
Коли варто використовувати base? Уявіть, що хочете розширити логіку методу з базового класу або ініціалізувати базовий клас із похідного. Наприклад:
public class Bicycle : Vehicle
{
public int NumberOfGears { get; set; }
public Bicycle(string brand, string color, int gears)
: base(brand, color) // викликаємо конструктор базового класу
{
NumberOfGears = gears;
}
public void ShowInfo()
{
// Використовуємо властивості з базового класу!
Console.WriteLine($"{Brand} ({Color}), передач: {NumberOfGears}");
}
}
5. Ключове слово base у методах
base — це не лише про конструктори! Іноді ви хочете змінити поведінку методу базового класу, але при цьому використати частину його логіки.
Припустімо, ми хочемо, щоб метод Drive у класі Car ще й виводив, яким саме автомобілем ви кермуєте:
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public Car(string brand, string color, int numberOfDoors)
: base(brand, color)
{
NumberOfDoors = numberOfDoors;
}
public void DriveCar()
{
base.Drive(); // викликаємо базову реалізацію
Console.WriteLine($"Ведемо {Brand} з {NumberOfDoors} дверима!");
}
}
Пояснення:
Виклик base.Drive(); — це ніби сказати: «Спочатку виконай базову частину, а я додам подробиці». У результаті під час виклику DriveCar() буде виведено:
Поїхали!
Ведемо BMW з 4 дверима!
Про перевизначення методів (override) поговоримо в наступних лекціях.
6. Схема наслідування
Спробуймо візуалізувати відносини між класами. Ось проста схема (бо UML — річ непроста, особливо зранку!):
Vehicle (базовий)
/ \
Car Bicycle
- Усі стрілки йдуть від похідного до базового.
- Усі похідні класи мають доступ до public/protected членів базового класу.
Відмінності між public, protected і private
Невелике нагадування, щоб не було плутанини.
У похідному класі видно:
| Модифікатор | Видимість у нащадку |
|---|---|
| public | Так |
| protected | Так |
| private | Ні |
Поширена помилка:
Спроба звернутися до private-поля з похідного класу:
public class Vehicle
{
private string secret = "Ніхто не дізнається!";
}
public class Car : Vehicle
{
public void RevealSecret()
{
Console.WriteLine(secret); // Помилка! Не видно.
}
}
Порівняння: базовий і похідний клас
| Vehicle | Car (нащадок) | |
|---|---|---|
| Brand | є | наслідує |
| Color | є | наслідує |
| Drive() | є | наслідує (або розширює) |
| NumberOfDoors | немає | власна (унікальна властивість) |
7. Приклад
Якщо ми продовжуємо розвивати простий застосунок «Облік транспорту», то можемо побудувати його так:
public class Vehicle
{
public string Brand { get; set; }
public string Color { get; set; }
public Vehicle(string brand, string color)
{
Brand = brand;
Color = color;
}
public void Drive()
{
Console.WriteLine($"{Brand} {Color} поїхав!");
}
}
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public Car(string brand, string color, int numberOfDoors)
: base(brand, color)
{
NumberOfDoors = numberOfDoors;
}
public void ShowCar()
{
Console.WriteLine($"Марка: {Brand}, Колір: {Color}, Дверей: {NumberOfDoors}");
}
}
public class Bicycle : Vehicle
{
public int NumberOfGears { get; set; }
public Bicycle(string brand, string color, int numberOfGears)
: base(brand, color)
{
NumberOfGears = numberOfGears;
}
}
У головному методі можна створити список транспорту й по-різному його описувати:
Car ford = new Car("Ford", "Червоний", 4);
ford.ShowCar();
ford.Drive();
Bicycle trek = new Bicycle("Trek", "Зелений", 21);
trek.Drive();
8. Помилки, особливості й трохи гумору
Дуже поширена помилка — забувати викликати базовий конструктор у похідному класі. Якщо у Vehicle немає конструктора без параметрів, а ви не викликали base(...), отримаєте помилку компіляції: компілятор поскаржиться, що не може ініціалізувати базовий клас. Це як намагатися звести другий поверх без першого — не працює. Дані потрібно передати «наверх».
Друга помилка: забувати, що поля/методи базового класу, оголошені з модифікатором private, у нащадках не видимі. Якщо хочете щось залишити для «дітей», використовуйте protected (про це детальніше — у наступних лекціях).
І головне: наслідування — дуже зручний механізм, але не варто вибудовувати надто заплутані ієрархії. У реальному житті, якщо у вашого ланцюжка наслідувань більше двох-трьох рівнів, щось, швидше за все, пішло не так. Дуже легко заплутатися, хто від кого наслідує, і навіщо все це взагалі потрібно.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ