JavaRush /Курси /C# SELF /Вступ до наслідування

Вступ до наслідування

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

1. Вступ

Сьогоднішня тема може видатися трохи абстрактною, але без неї далі — ніяк. Поговоримо про наслідування у програмуванні. Сьогодні лише побіжно торкнемося цієї теми, а згодом розберемо її детально.

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

  • У лева є грива.
  • Тигр смугастий.
  • Слон дуже великий і має хобот.
  • Папуга вміє говорити.

Якби ми описували кожну тварину окремо, код був би дуже схожий:


// Лев
public class Lion
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Species { get; set; } = "Лев"; // Завжди "Лев"
    public void Eat() { Console.WriteLine("Лев їсть мʼясо."); }
    public void Sleep() { Console.WriteLine("Лев спить."); }
    public void Roar() { Console.WriteLine("Лев ричить!"); }
    public string ManeColor { get; set; } // Особливість лева - грива
}

// Тигр
public class Tiger
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Species { get; set; } = "Тигр"; // Завжди "Тигр"
    public void Eat() { Console.WriteLine("Тигр їсть мʼясо."); }
    public void Sleep() { Console.WriteLine("Тигр спить."); }
    public void Stride() { Console.WriteLine("Тигр крадеться."); }
    public string StripePattern { get; set; } // Особливість тигра - смугастість
}

Бачите, скільки дубльованого коду? Name, Age, Eat(), Sleep() — це ж спільне для усіх тварин! А якщо в нас буде 100 видів тварин? Жах!

Ось тут і допоможе наслідування.

2. Що таке наслідування класів у C#?

Наслідування — це один із фундаментальних принципів обʼєктноорієнтованого програмування (ООП). Воно дає змогу створювати нові класи на основі вже наявних. Новий клас (нащадок, дочірній клас, похідний клас) отримує (наслідує) всі властивості (поля) та поведінку (методи) свого батьківського класу (базового класу, батьківського класу).

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

У C# наслідування позначають двокрапкою : після імені дочірнього класу:


            // Батьківський (базовий) клас - Animal
public class Animal
{
    public string Name { get; set; } // Імʼя тварини
    public int Age { get; set; }     // Вік тварини

    public void Eat()
    {
        Console.WriteLine($"{Name} їсть.");
    }

    public void Sleep()
    {
        Console.WriteLine($"{Name} спить.");
    }
}

// Дочірній (похідний) клас - Lion наслідує від Animal
public class Lion : Animal // <-- Ось воно, наслідування!
{
    public string ManeColor { get; set; } // Унікальна властивість лева

    public void Roar() // Унікальна поведінка лева
    {
        Console.WriteLine($"{Name} ричить: РРРРРР!");
    }
}

// Ще один дочірній клас - Tiger наслідує від Animal
public class Tiger : Animal
{
    public string StripePattern { get; set; } // Унікальна властивість тигра

    public void Stride() // Унікальна поведінка тигра
    {
        Console.WriteLine($"{Name} крадеться безшумно.");
    }
}

Тепер, якщо ми створимо обʼєкт Lion або Tiger, у них автоматично будуть властивості Name і Age, а також методи Eat() і Sleep(), адже їх успадковано від Animal.


class Program
{
    static void Main(string[] args)
    {
        Lion simba = new Lion();
        simba.Name = "Сімба";
        simba.Age = 5;
        simba.ManeColor = "Золотий";

        simba.Eat();   // Метод наслідуваний від Animal
        simba.Sleep(); // Метод наслідуваний від Animal
        simba.Roar();  // Власний метод Lion

        Console.WriteLine($"{simba.Name} - вік: {simba.Age}, колір гриви: {simba.ManeColor}");

        Tiger shereKhan = new Tiger();
        shereKhan.Name = "Шерхан";
        shereKhan.Age = 7;
        shereKhan.StripePattern = "Класичний";

        shereKhan.Eat();   // Метод наслідуваний від Animal
        shereKhan.Sleep(); // Метод наслідуваний від Animal
        shereKhan.Stride(); // Власний метод Tiger

        Console.WriteLine($"{shereKhan.Name} - вік: {shereKhan.Age}, візерунок: {shereKhan.StripePattern}");
    }
}

Ключові ідеї наслідування:

  1. Повторне використання коду (Code Reusability): Ви уникаєте дублювання, розміщуючи спільну логіку в базовому класі.
  2. Ієрархія (Hierarchy): Постає логічна структура «загальне — окреме» (AnimalLion/Tiger).
  3. Поліморфізм (Polymorphism): Поки лише згадаємо — деталі будуть далі. Ви можете працювати з обʼєктами дочірніх класів через посилання на їхній базовий клас. Наприклад, мати список Animal, покласти туди і левів, і тигрів і викликати в них метод Eat().

3. Виклик конструкторів базових класів (base)

Коли ви створюєте обʼєкт дочірнього класу (наприклад, new Lion()), C# автоматично викликає конструктор базового класу (у нашому випадку Animal) перед викликом конструктора дочірнього класу. Це гарантує, що базова частина обʼєкта буде правильно ініціалізована.

Іноді потрібно передати параметри в конструктор базового класу. Для цього після сигнатури конструктора дочірнього класу використовують ключове слово base.


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

    // Конструктор базового класу
    public Animal(string name, int age)
    {
        Name = name;
        Age = age;
        Console.WriteLine($"Конструктор Animal: Створено тварину {Name}, вік {Age}.");
    }

    public void Eat()
    {
        Console.WriteLine($"{Name} їсть.");
    }

    public void Sleep()
    {
        Console.WriteLine($"{Name} спить.");
    }
}

public class Lion : Animal
{
    public string ManeColor { get; set; }

    // Конструктор Lion, який викликає конструктор базового класу Animal
    public Lion(string name, int age, string maneColor) : base(name, age) // <-- Ось воно, 'base'!
    {
        ManeColor = maneColor;
        Console.WriteLine($"Конструктор Lion: Колір гриви {ManeColor}.");
    }

    public void Roar()
    {
        Console.WriteLine($"{Name} ричить: РРРРРР!");
    }
}

            class Program
{
    static void Main(string[] args)
    {
        // Під час створення Lion спочатку викликається конструктор Animal, потім — Lion
        Lion simba = new Lion("Сімба", 5, "Золотий");
        simba.Roar();
    }
}

Що відбудеться під час виконання new Lion("Сімба", 5, "Золотий"):

  1. Буде викликано конструктор Lion(string name, int age, string maneColor).
  2. Завдяки : base(name, age) керування буде передано конструктору Animal(string name, int age).
  3. Виконається код конструктора Animal, і ви побачите Конструктор Animal: Створено тварину Сімба, вік 5.
  4. Після цього керування повернеться в конструктор Lion.
  5. Виконається код конструктора Lion, і ви побачите Конструктор Lion: Колір гриви Золотий.

Це дуже важливо: базовий клас завжди ініціалізується першим!

4. Оператори is і as

У програмуванні на C# ви часто стикаєтеся з ситуаціями, коли треба перевірити тип обʼєкта або спробувати перетворити його до іншого типу. Тут на допомогу приходять оператори is і as. Вони особливо корисні під час роботи з наслідуванням і поліморфізмом. Про це докладніше поговоримо пізніше, але основи можна зрозуміти вже зараз.

Оператор is (перевірка типу)

Оператор is дає змогу перевірити, чи сумісний обʼєкт із певним типом. Він повертає true, якщо обʼєкт можна перетворити до вказаного типу, і false — в іншому випадку.

Синтаксис: вираз is Тип


static void AnalyzeObject(object obj)
{
    if (obj is string)
    {
        Console.WriteLine("Це рядок!");
    }
    else if (obj is int)
    {
        Console.WriteLine("Це ціле число!");
    }
    else
    {
        Console.WriteLine("Це щось інше.");
    }
}

static void Main()
{
    AnalyzeObject("Привіт, світ!");      // Це рядок!
    AnalyzeObject(123);                 // Це ціле число!
    AnalyzeObject(3.14);                // Це щось інше.
    AnalyzeObject(new int[] { 1, 2 });  // Це щось інше.
}

is із pattern matching:

Сучасний C# дозволяє використовувати is не лише для перевірки типу, а й для одразу створення змінної цього типу, якщо тип збігся. Це називають «pattern matching» і воно робить код значно лаконічнішим.


static void AnalyzeObjectWithPatternMatching(object obj)
{
    if (obj is string message) // Якщо obj - це рядок, то присвоїти його змінній message
    {
        Console.WriteLine($"Це рядок, його довжина: {message.Length}");
    }
    else if (obj is int number) // Якщо obj - це int, то присвоїти його змінній number
    {
        Console.WriteLine($"Це число, помножене на 2: {number * 2}");
    }
    else
    {
        Console.WriteLine("Тип не розпізнано.");
    }
}

static void Main()
{
    AnalyzeObjectWithPatternMatching("Приклад"); // Це рядок, його довжина: 7
    AnalyzeObjectWithPatternMatching(50);       // Це число, помножене на 2: 100
    AnalyzeObjectWithPatternMatching(new object()); // Тип не розпізнано.
}

Такий підхід із is і pattern matching кращий, ніж традиційне перетворення типів із подальшою перевіркою на null, оскільки він безпечніший і читабельніший.

Оператор as (безпечне перетворення типу)

Оператор as намагається перетворити обʼєкт до вказаного типу. Якщо перетворення можливе, він повертає обʼєкт, перетворений до цього типу. Якщо перетворення неможливе (тобто обʼєкт несумісний із цільовим типом), as повертає null замість того, щоб кидати виняток. Це робить as «безпечним» оператором перетворення.

Синтаксис: вираз as Тип


static void ProcessData(object data)
{
    string text = data as string; // Пробуємо перетворити data на string
    
    if (text != null) // Перевіряємо, чи вдалося перетворення
    {
        Console.WriteLine($"Обробка рядка: {text.ToUpper()}");
    }
    else
    {
        Console.WriteLine("Дані не є рядком.");
    }
}

static void Main()
{
    ProcessData("hello");      // Обробка рядка: HELLO
    ProcessData(123);          // Дані не є рядком.
    ProcessData(null);         // Дані не є рядком.
}

Коли використовувати as замість прямого перетворення (Тип)вираз:

  • Безпека: Якщо ви не впевнені, що обʼєкт справді потрібного типу, as дозволить уникнути InvalidCastException і натомість поверне null, який можна обробити.
  • Для посилальних типів: as працює тільки з посилальними типами (класи, інтерфейси, делегати, масиви). Його не можна використовувати для перетворення типів-значень (наприклад, з int на double).

Важливо: якщо ви впевнені, що обʼєкт завжди буде потрібного типу, або якщо треба перетворити тип-значення, використовуйте пряме перетворення (Тип)вираз. Якщо перетворення неможливе, пряме перетворення спричинить помилку (виняток InvalidCastException). Це може бути бажаною поведінкою, якщо хибний тип — помилка в логіці програми.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ