Назад Вперед Зміст

Відношення між класами: Успадкування у C#

Успадкування — це механізм об’єктно-орієнтованого програмування, який дозволяє створювати новий клас на основі існуючого. Новий клас (похідний) отримує поля та методи базового класу, а також може додавати свої або перевизначати існуючі.


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

Простий приклад успадкування — похідний клас розширює базовий додатковими властивостями або методами.


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(); // Викликає перевизначений метод
    }
}

Конструктори у дочірньому класі

Нащадок успадковує всі поля та методи батьківського класу, крім конструкторів.

Конструктори не успадковуються, тому дочірній клас повинен мати власні конструктори.

Однак, оскільки дочірній об'єкт може користуватися полями батьківського класу, ці поля повинні якимось чином отримати значення.

При створенні об'єкта дочірнього класу можливі два випадки:

  1. Поля батьківського класу заповнюються конструктором батьківського класу. Тому в конструкторі дочірнього класу потрібно явно вказати, який (з якою кількістю параметрів та яких типів) конструктор батьківського класу потрібно викликати (ключове слово base). Якщо такого конструктора немає, виникає помилка. Якщо такий конструктор є, він викликається і заповнює поля батьківського класу. Потім викликається конструктор дочірнього класу, який заповнює поля дочірнього класу.
  2. Поля батьківського класу заповнюються конструктором дочірнього класу. Тож у конструкторі дочірнього класу немає ключового слова base. При цьому автоматично викликається конструктор батьківського класу без параметрів. Якщо такого конструктора немає, виникає помилка. Якщо такий конструктор є, він викликається і заповнює поля батьківського класу порожніми значеннями. Потім викликається конструктор дочірнього класу, який заповнює поля батьківського та дочірнього класу.

Виклик конструктора базового класу

Похідний клас може викликати конструктор базового класу через ключове слово 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}");
    }
}

Приклад 1

У цьому прикладі у батьківського класу два дочірніх. Конструктори дочірніх класів викликають конструктор батьківського класу.

Методи батьківського класу перевизначаються у дочірніх класах.

З методів дочірнього класу викликаються методи батьківського класу.

Методи дочірнього класу використовують поля батьківського класу.

Створимо батьківський клас Press, який описує друковану продукцію та містить:


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;
    }
}

Створимо дочірній клас Magazine, який описує журнал і містить:


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;        // Без змін для невідомої якості
    }
}

Створимо дочірній клас Book, який описує книгу та містить:


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;
    }
}

У методі Main класу Program:

Створимо об'єкти батьківського та дочірніх класів та застосуємо до них методи.


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 Успішно

Висновки

Приклад 2

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()}");
    }
}

Особливості C#:

Цей приклад демонструє правильну ієрархію, де похідний клас (Rectangle) розширює базовий (Square), додаючи нову функціональність (роботу з висотою) і змінюючи поведінку успадкованих методів.

Тестування класів Square та Rectangle

Тест-кейс Очікуваний результат Фактичний результат Статус
Тестування класу 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 пройшла всі тести успішно. Класи коректно реалізують принципи ООП:

Приклад 3

Створити клас Transport-транспорт, що містить:

Створити клас 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)

Ініціалізує новий об'єкт транспортного засобу з вказаним пробігом і статусом техогляду.

Метод Move

public bool Move(double distance)

Спроба перемістити транспорт на задану відстань. Повертає true, якщо техогляд є і переміщення відбулося, інакше - false.

Метод ToString

public override string ToString()

Повертає рядок з інформацією про транспортний засіб у форматі: "Загальний пробіг = X км"

Тестування класу

Тестовий сценарій 1: Поїздка з техоглядом

Код:


Transport t = new Transport(1000, true);
bool result = t.Move(150);
Console.WriteLine(t.ToString());

Очікуваний результат: "Загальний пробіг = 1150 км", result = true

Фактичний результат: Пройдено

Тестовий сценарій 2: Спроба поїздки без техогляду

Код:


Transport t = new Transport(5000, false);
bool result = t.Move(200);
Console.WriteLine(t.ToString());

Очікуваний результат: "Загальний пробіг = 5000 км", result = false

Фактичний результат: Пройдено

Тестовий сценарій 3: Виведення інформації про транспорт

Код:


Transport t = new Transport(25000, true);
Console.WriteLine(t.ToString());

Очікуваний результат: "Загальний пробіг = 25000 км"

Фактичний результат: Пройдено

Результати тестування

Тестовий сценарій Очікуваний результат Фактичний результат Статус
Поїздка з техоглядом Пробіг збільшено, повернуто true Пробіг = 1150 км, result = true Пройдено
Спроба поїздки без техогляду Пробіг не змінився, повернуто false Пробіг = 5000 км, result = false Пройдено
Виведення інформації Коректний рядок з пробігом "Загальний пробіг = 25000 км" Пройдено

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


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 коректно реалізує базову функціональність транспортного засобу. Всі тестові сценарії пройдені успішно. Клас готовий до використання та розширення в дочірніх класах.

4. Запечатані класи (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("Програма запущена.");
    }
}

Висновок

Назад Вперед Зміст