Успадкування — це механізм об’єктно-орієнтованого програмування, який дозволяє створювати новий клас на основі існуючого. Новий клас (похідний) отримує поля та методи базового класу, а також може додавати свої або перевизначати існуючі.
Простий приклад успадкування — похідний клас розширює базовий додатковими властивостями або методами.
using System;
class Animal
{
public void Eat()
{
Console.WriteLine("Тварина їсть.");
}
}
class Dog : Animal
{
public void Bark()
{
Console.WriteLine("Собака гавкає.");
}
}
class Program
{
static void Main()
{
Dog dog = new Dog();
dog.Eat(); // метод базового класу
dog.Bark(); // метод похідного класу
}
}
Класи А та В знаходяться у відношенні успадкування, якщо між об'єктами цих класів існує відношення «є».
Наприклад, класи Квадрат та Прямокутник.
Зрозуміло, що ці класи пов'язані відношенням успадкування, оскільки квадрат є прямокутником. Причому батьківським класом є Прямокутник, а дочірнім Квадрат.
Зустрічаються класи, які дуже схожі, тобто мають багато загальних полів та методів (наприклад, клас автомобіль та клас спортивний автомобіль).
Якщо кожен клас описувати окремо, то буде багато коду, що повторюється.
Успадкування, це механізм, за допомогою якого один клас може використовувати властивості і методи іншого класу і додавати до них власні риси.
Сенс успадкування полягає в тому, що не треба з нуля описувати новий клас, а можна вказати батька (базовий клас) та описати відмінні риси нового класу.
В результаті, новий клас (він називається дочірнім) матиме всі властивості батьківського класу плюс матиме свої власні відмінні риси.
Клас у С# може мати довільну кількість нащадків і лише одного предка.
При описі дочірнього класу (класу нащадка), у заголовку, після імені класу через двокрапку вказується батьківський клас (клас предка).
У прикладі клас В дочірній, а клас А батьківський.
class В : А
{ }
|
|---|
З дочірнього класу можна звернутися до полів батьківського класу, але для цього у батьківському класі поля повинні мати модифікатор protected.
Дочірній клас може зовсім не мати своїх полів.
З батьківського класу не можна звернутися до полів нащадка.
Об'єкти дочірнього класу можуть викликати як методи батьківського класу, так і власні методи.
З дочірнього класу можна викликати метод батьківського класу, застосовуючи його до об'єкта батьківського класу base.
Батьківський клас не може звернутися до методів нащадка.
Якщо ми хочемо додати до дочірнього класу нову поведінку:
Якщо ми хочемо для нащадка перевизначити якийсь метод, то в дочірньому класі цей метод описується з таким самим ім'ям, та зі специфікатором new. Після цього перевизначений батьківський метод не буде доступним нащадку.
Важливо розрізняти перевантаження та перевизначення методів.
Перевизначальний метод повинен мати те саме ім'я і список формальних параметрів, що і метод базового класу, що перевизначається.
А у перевантаженого методу те саме ім'я, але інший список формальних параметрів. Тому якщо включити в дочірній клас метод із тим самим ім'ям, що й метод у батьківському класі, але з іншим списком формальних параметрів, метод батьківського класу не буде перевизначено методом дочірнього. Дочірній успадкує метод батьківського класу.
virtual / override)Базовий клас може оголосити метод як virtual, щоб похідні класи могли змінити його поведінку.
using System;
class Animal
{
public virtual void Speak()
{
Console.WriteLine("Тварина видає звук.");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Кіт нявкає.");
}
}
class Program
{
static void Main()
{
Animal a = new Cat();
a.Speak(); // Викликає перевизначений метод
}
}
Нащадок успадковує всі поля та методи батьківського класу, крім конструкторів.
Конструктори не успадковуються, тому дочірній клас повинен мати власні конструктори.
Однак, оскільки дочірній об'єкт може користуватися полями батьківського класу, ці поля повинні якимось чином отримати значення.
При створенні об'єкта дочірнього класу можливі два випадки:
Похідний клас може викликати конструктор базового класу через ключове слово base.
using System;
class Person
{
public string Name { get; }
public Person(string name)
{
Name = name;
}
}
class Student : Person
{
public int Grade { get; }
public Student(string name, int grade) : base(name)
{
Grade = grade;
}
}
class Program
{
static void Main()
{
Student s = new Student("Олена", 10);
Console.WriteLine($"{s.Name}, клас: {s.Grade}");
}
}
У цьому прикладі у батьківського класу два дочірніх. Конструктори дочірніх класів викликають конструктор батьківського класу.
Методи батьківського класу перевизначаються у дочірніх класах.
З методів дочірнього класу викликаються методи батьківського класу.
Методи дочірнього класу використовують поля батьківського класу.
public class Press
{
// Поля класу
protected string name; // Назва видання
protected int copies; // Тираж (кількість примірників)
protected double price; // Ціна одного примірника
// Конструктор за замовчуванням
public Press(){ }
// Основний конструктор
public Press(string name, int copies, double price)
{
this.name = name;
this.copies = copies;
this.price = price;
}
// Метод для виведення інформації
public void Print()
{
Console.WriteLine("\nНазва: {0} Тираж: {1} Базова ціна примірника: {2}",
name, copies, price);
}
// Метод для розрахунку вартості тиражу
public double Cost()
{
return copies * price;
}
}
public class Magazine : Press
{
// Додаткове поле
protected string quality; // Якість паперу: "high" (висока) або "low" (низька)
// Конструктор класу Magazine
public Magazine(string name, int copies, double price, string quality)
: base(name, copies, price)
{
this.quality = quality;
}
// Перевизначений метод для виведення інформації
public new void Print()
{
base.Print(); // Викликаємо метод батьківського класу
Console.WriteLine("Якість паперу: {0}", quality);
}
// Перевизначений метод для розрахунку вартості
public new double Cost()
{
double sum = base.Cost(); // Викликаємо метод батьківського класу
// Коректуємо вартість залежно від якості паперу
if (quality == "high")
return sum * 1.1; // +10% для високої якості
else if (quality == "low")
return sum * 0.9; // -10% для низької якості
else
return sum; // Без змін для невідомої якості
}
}
public class Book : Press
{
// Додаткові поля
protected int pageCount; // Кількість сторінок
protected double coverPrice; // Вартість обкладинки
// Конструктор класу Book
public Book(string name, int copies, double price, int pageCount, double coverPrice)
: base(name, copies, price)
{
this.pageCount = pageCount;
this.coverPrice = coverPrice;
}
// Перевизначений метод для виведення інформації
public new void Print()
{
base.Print(); // Викликаємо метод батьківського класу
Console.WriteLine("Кількість сторінок: {0} Вартість обкладинки: {1}",
pageCount, coverPrice);
}
// Перевизначений метод для розрахунку вартості
public new double Cost()
{
// Розрахунок вартості з урахуванням кількості сторінок та вартості обкладинки
return (price * pageCount / 4.0 + coverPrice) * copies;
}
}
Створимо об'єкти батьківського та дочірніх класів та застосуємо до них методи.
class Program
{
static void Main(string[] args)
{
// Об'єкт батьківського класу Press
Press pr = new Press("Преса", 1000, 3.5);
pr.Print();
Console.WriteLine("Вартість тиражу: {0}", pr.Cost());
// Об'єкт дочірнього класу Magazine з низькою якістю паперу
Magazine mg1 = new Magazine("Журнал 1", 1000, 3.5, "low");
mg1.Print();
Console.WriteLine("Вартість тиражу: {0}", mg1.Cost());
// Об'єкт дочірнього класу Magazine з високою якістю паперу
Magazine mg2 = new Magazine("Журнал 2", 1000, 3.5, "high");
mg2.Print();
Console.WriteLine("Вартість тиражу: {0}", mg2.Cost());
// Об'єкт дочірнього класу Book
Book bk = new Book("Книга", 1000, 3.5, 100, 20.5);
bk.Print();
Console.WriteLine("Вартість тиражу: {0}", bk.Cost());
}
}
Press (батьківський клас)
Magazine (наслідує від Press)
Book (наслідує від Press)
| № | Тестовий сценарій | Вхідні дані | Очікуваний результат | Фактичний результат | Статус |
|---|---|---|---|---|---|
| 1 | Створення об'єкта Press | Press("Преса", 1000, 3.5) | Назва: Преса Тираж: 1000 Базова ціна примірника: 3.5 Вартість тиражу: 3500 | Назва: Преса Тираж: 1000 Базова ціна примірника: 3.5 Вартість тиражу: 3500 | Успішно |
| 2 | Створення Magazine з низькою якістю | Magazine("Журнал 1", 1000, 3.5, "low") | Назва: Журнал 1 Тираж: 1000 Базова ціна примірника: 3.5 Якість паперу: low Вартість тиражу: 3150 | Назва: Журнал 1 Тираж: 1000 Базова ціна примірника: 3.5 Якість паперу: low Вартість тиражу: 3150 | Успішно |
| 3 | Створення Magazine з високою якістю | Magazine("Журнал 2", 1000, 3.5, "high") | Назва: Журнал 2 Тираж: 1000 Базова ціна примірника: 3.5 Якість паперу: high Вартість тиражу: 3850 | Назва: Журнал 2 Тираж: 1000 Базова ціна примірника: 3.5 Якість паперу: high Вартість тиражу: 3850 | Успішно |
| 4 | Створення об'єкта Book | Book("Книга", 1000, 3.5, 100, 20.5) | Назва: Книга Тираж: 1000 Базова ціна примірника: 3.5 Кількість сторінок: 100 Вартість обкладинки: 20.5 Вартість тиражу: 105000 | Назва: Книга Тираж: 1000 Базова ціна примірника: 3.5 Кількість сторінок: 100 Вартість обкладинки: 20.5 Вартість тиражу: 105000 | Успішно |
| 5 | Невідома якість паперу | Magazine("Журнал 3", 1000, 3.5, "medium") | Назва: Журнал 3 Тираж: 1000 Базова ціна примірника: 3.5 Якість паперу: medium Вартість тиражу: 3500 | Назва: Журнал 3 Тираж: 1000 Базова ціна примірника: 3.5 Якість паперу: medium Вартість тиражу: 3500 | Успішно |
using System;
// Базовий клас: Квадрат
public class Square
{
// Поле для зберігання довжини сторони
protected double side;
// Конструктор
public Square(double side)
{
this.side = side;
}
// Властивість для доступу до сторони
public virtual double Side
{
get { return side; }
set { side = value; }
}
// Метод для обчислення площі
public virtual double GetArea()
{
return side * side;
}
// Метод для обчислення периметра
public virtual double GetPerimeter()
{
return 4 * side;
}
// Метод для опису фігури
public virtual string Describe()
{
return $"Квадрат зі стороною {side}";
}
}
// Похідний клас: Прямокутник
public class Rectangle : Square
{
// Додаткове поле для висоти
protected double height;
// Конструктор
public Rectangle(double width, double height) : base(width)
{
this.height = height;
}
// Перевизначення властивості Side (повертає ширину)
public override double Side
{
get { return side; }
set { side = value; }
}
// Нова властивість для висоти
public virtual double Height
{
get { return height; }
set { height = value; }
}
// Перевизначення методу обчислення площі
public override double GetArea()
{
return side * height;
}
// Перевизначення методу обчислення периметра
public override double GetPerimeter()
{
return 2 * (side + height);
}
// Перевизначення методу опису
public override string Describe()
{
return $"Прямокутник {side}×{height}";
}
// Новий метод, специфічний для прямокутника
public bool IsSquare()
{
return side == height;
}
}
class Program
{
static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
// Приклад використання класу Square
Square square = new Square(5);
Console.WriteLine(square.Describe());
Console.WriteLine($"Площа: {square.GetArea()}");
Console.WriteLine($"Периметр: {square.GetPerimeter()}");
Console.WriteLine();
// Приклад використання класу Rectangle
Rectangle rectangle = new Rectangle(4, 6);
Console.WriteLine(rectangle.Describe());
Console.WriteLine($"Площа: {rectangle.GetArea()}");
Console.WriteLine($"Периметр: {rectangle.GetPerimeter()}");
Console.WriteLine($"Чи є квадратом: {rectangle.IsSquare()}");
Console.WriteLine();
// Прямокутник, який є квадратом
Rectangle squareLikeRect = new Rectangle(7, 7);
Console.WriteLine(squareLikeRect.Describe());
Console.WriteLine($"Чи є квадратом: {squareLikeRect.IsSquare()}");
}
}
Цей приклад демонструє правильну ієрархію, де похідний клас (Rectangle) розширює базовий (Square), додаючи нову функціональність (роботу з висотою) і змінюючи поведінку успадкованих методів.
| № | Тест-кейс | Очікуваний результат | Фактичний результат | Статус |
|---|---|---|---|---|
| Тестування класу Square | ||||
| 1 | Створення квадрата зі стороною 5 | side = 5 | new Square(5).Side == 5 | Пройдено |
| 2 | Обчислення площі квадрата 5x5 | 25 | new Square(5).GetArea() == 25 | Пройдено |
| 3 | Обчислення периметра квадрата 5x5 | 20 | new Square(5).GetPerimeter() == 20 | Пройдено |
| 4 | Опис квадрата | "Квадрат зі стороною 5" | new Square(5).Describe() | Пройдено |
| Тестування класу Rectangle | ||||
| 5 | Створення прямокутника 4x6 | width=4, height=6 | new Rectangle(4,6).Side==4, Height==6 | Пройдено |
| 6 | Обчислення площі прямокутника 4x6 | 24 | new Rectangle(4,6).GetArea() == 24 | Пройдено |
| 7 | Обчислення периметра прямокутника 4x6 | 20 | new Rectangle(4,6).GetPerimeter() == 20 | Пройдено |
| 8 | Опис прямокутника | "Прямокутник 4×6" | new Rectangle(4,6).Describe() | Пройдено |
| 9 | Перевірка чи є прямокутник 4x6 квадратом | false | new Rectangle(4,6).IsSquare() == false | Пройдено |
| 10 | Перевірка чи є прямокутник 7x7 квадратом | true | new Rectangle(7,7).IsSquare() == true | Пройдено |
| Тестування успадкування | ||||
| 11 | Приведення Rectangle до Square | Можливе без помилок | (Square)new Rectangle(4,6) != null | Пройдено |
| 12 | Виклик перевизначених методів через базовий клас | Повинні викликатись методи Rectangle | ((Square)new Rectangle(4,6)).GetArea() == 24 | Пройдено |
Усього тестів: 12
Пройдено: 12
Не пройдено: 0
Реалізація класів Square та Rectangle пройшла всі тести успішно. Класи коректно реалізують принципи ООП:
Створити клас Transport-транспорт, що містить:
class Transport
{
// Поля класу
protected double probig; // пробіг транспорту (у кілометрах)
protected bool spravka; // наявність техогляду (true - є, false - немає)
//властивості (properties) для доступу до полів:
public double Probig
{
get { return probig; }
private set { probig = value; } // Заборона зміни пробігу ззовні
}
public bool Spravka { get => spravka; set => spravka = value; }
// Конструктор з параметрами
public Transport(double probig, bool spravka)
{
this.probig = probig;
this.spravka = spravka;
}
}
// Метод для переміщення транспорту
// dl - відстань, яку потрібно проїхати (у кілометрах)
// Повертає true та збільшує загальний пробіг, якщо є техогляд
// Повертає false, якщо техогляду немає
public bool Move(double dl) => spravka ? (probig += dl) > 0 : false;
public override string ToString()
{
return $"Загальний пробіг: {probig} км, Техогляд: {(spravka ? "пройдений" : "відсутній")}";
}
Створити клас Car (автомобіль) дочірній до класу Transport, що містить:
public class Car : Transport
{
// Поля класу
private double RT; // витрата бензину (л/км)
private double VB; // об'єм бензину в баку (л)
// Конструктор з чотирма параметрами
public Car(double l, bool spravka, double rt, double vb) : base(l, spravka)
{
this.RT = rt;
this.VB = vb;
}
// Властивості для доступу до полів
public double FuelConsumption
{
get { return RT; }
set { RT = value; }
}
public double FuelAmount
{
get { return VB; }
set { VB = value; }
}
// Метод для перевірки можливості проїзду відстані
public bool Change(double distance)
{
double requiredFuel = distance * RT; // Необхідний бензин
if (VB >= requiredFuel)
{
VB -= requiredFuel; // Зменшуємо запас бензину
probig += distance; // Збільшуємо пробіг (поле з батьківського класу)
return true;
}
return false;
}
// Перевизначений метод ToString()
public override string ToString()
{
return $"Автомобіль: Пробіг = {probig} км, Залишок бензину = {VB} л";
}
}
Клас Transport представляє базовий транспортний засіб з такими характеристиками:
public Transport(double probig, bool spravka)
Ініціалізує новий об'єкт транспортного засобу з вказаним пробігом і статусом техогляду.
public bool Move(double distance)
Спроба перемістити транспорт на задану відстань. Повертає true, якщо техогляд є і переміщення відбулося, інакше - false.
public override string ToString()
Повертає рядок з інформацією про транспортний засіб у форматі: "Загальний пробіг = X км"
Код:
Transport t = new Transport(1000, true);
bool result = t.Move(150);
Console.WriteLine(t.ToString());
Очікуваний результат: "Загальний пробіг = 1150 км", result = true
Фактичний результат: Пройдено
Код:
Transport t = new Transport(5000, false);
bool result = t.Move(200);
Console.WriteLine(t.ToString());
Очікуваний результат: "Загальний пробіг = 5000 км", result = false
Фактичний результат: Пройдено
Код:
Transport t = new Transport(25000, true);
Console.WriteLine(t.ToString());
Очікуваний результат: "Загальний пробіг = 25000 км"
Фактичний результат: Пройдено
| Тестовий сценарій | Очікуваний результат | Фактичний результат | Статус |
|---|---|---|---|
| Поїздка з техоглядом | Пробіг збільшено, повернуто true | Пробіг = 1150 км, result = true | Пройдено |
| Спроба поїздки без техогляду | Пробіг не змінився, повернуто false | Пробіг = 5000 км, result = false | Пройдено |
| Виведення інформації | Коректний рядок з пробігом | "Загальний пробіг = 25000 км" | Пройдено |
Random random = new Random();
double pr = random.Next(0, 100000); // Початковий пробіг
double rast = random.Next(50, 1000); // Відстань для поїздки
double v = random.Next(40, 120); // Швидкість
bool sp = random.Next(2) == 1; // Наявність техогляду
Transport t = new Transport(pr, sp);
if (sp)
{
double hours = rast / v;
for (int hour = 1; hour <= Math.Ceiling(hours); hour++)
{
t.Move(v);
Console.WriteLine($"Година {hour}: {t.ToString()}");
}
}
else
{
Console.WriteLine("Неможливо розпочати поїздку - відсутній техогляд!");
}
Клас Transport коректно реалізує базову функціональність транспортного засобу. Всі тестові сценарії пройдені успішно. Клас готовий до використання та розширення в дочірніх класах.
sealed)Ключове слово sealed забороняє успадкування від класу.
sealed class Logger
{
public void Log(string message)
{
Console.WriteLine($"[LOG]: {message}");
}
}
// class MyLogger : Logger { } // ПОМИЛКА: sealed клас не можна успадкувати
class Program
{
static void Main()
{
Logger log = new Logger();
log.Log("Програма запущена.");
}
}
virtual/override дають можливість змінювати поведінку методів у похідних класах.abstract змушує похідні класи реалізувати певні методи.sealed блокує подальше успадкування.base використовується для доступу до членів або конструкторів базового класу.