Модул 12

Наследяване и Полиморфизъм (ООП 2)

Втората среща с ООП — как един клас наследява данните и поведението на друг, как наследникът променя поведение с virtual и override, за какво служи base и защо полиморфизмът е толкова мощен.

Теория

Наследяването позволява един клас да наследи полетата, свойствата и методите на друг: class Student : Person се чете "Student Е Person (плюс още нещо)". Person е базов (родителски) клас, Studentнаследник. Наследникът получава всичко от родителя наготово и добавя своето — без копиране на код.

Правилото за проверка е "is-a" (е-вид): студентът *е* човек, кучето *е* животно → наследяване има смисъл. Колата НЕ е двигател (тя *има* двигател) → там не се наследява, а се влага обект като поле (композиция).

Полиморфизъм ("много форми") означава, че една и съща команда се изпълнява различно според реалния тип на обекта. Базовият клас обявява метода като virtual ("позволявам да ме променят"), а наследникът го заменя с override. Така в List<Animal> можем да държим кучета и котки и едно animal.MakeSound() да дава "Бау!" или "Мяу!" според това какво е животното.

Ключовата дума base дава достъп до родителя: base(...) в конструктора извиква родителския конструктор, а base.Method() — родителската версия на метода. Модификаторът protected е средата между private и public: достъпен за класа и неговите наследници, но не и отвън.

Две думи за хоризонта: abstract клас не може да се инстанцира (служи само за основа), а абстрактен метод дори няма тяло — наследниците са задължени да го override-нат. Обратно, sealed "запечатва" клас срещу наследяване. В началото ще ги срещате главно в чужд код — важно е да ги разпознавате.

Златни правила

Наследяване = "is-a"

Наследявайте само когато наследникът наистина Е вид на базовия клас (Студентът Е Човек). Ако връзката е "има" (Колата ИМА двигател) — използвайте поле, не наследяване.

virtual позволява, override заменя

Метод може да бъде заменен в наследник само ако базовият клас го е обявил като virtual. Наследникът задължително пише override — двете думи вървят по двойки.

base се обръща към родителя

base(...) в конструктора извиква родителския конструктор (задължително, ако той има параметри). base.Method() извиква родителската версия на метод — полезно, когато override-ът само допълва.

Едно ниво стига (засега)

C# позволява само един базов клас (няма множествено наследяване). Дълбоките йерархии (A : B : C : D) бързо стават неуправляеми — в учебните задачи едно-две нива са достатъчни.

Бърз справочник

Речник на наследяването

Термин / ДумаКакво означава?Пример
: БазовКласДекларира наследяванеclass Student : Person
virtualБазовият клас позволява замяна на методаpublic virtual void Introduce()
overrideНаследникът заменя virtual методpublic override void Introduce()
base(...)Извиква конструктора на родителяpublic Student(string name) : base(name)
base.Метод()Извиква родителската версия на методаbase.Introduce();
protectedДостъпно за класа И наследниците муprotected double salary;

Модификатори за достъп (пълна картина)

МодификаторСамият класНаследнициВсички останали
public
protected
private

Код за анализ и пренаписване

Йерархия Animal → Dog/Cat и полиморфизъм в действие

Program.cs
using System;
using System.Collections.Generic;

namespace ModuleTwelve
{
    // Базов клас
    class Animal
    {
        public string Name { get; set; }

        public Animal(string name)
        {
            Name = name;
        }

        // virtual = наследниците МОГАТ да заменят този метод
        public virtual void MakeSound()
        {
            Console.WriteLine($"{Name} издава някакъв звук.");
        }
    }

    // Наследник: Dog Е Animal
    class Dog : Animal
    {
        // base(name) подава името на конструктора на Animal
        public Dog(string name) : base(name) { }

        // override = заменяме поведението от базовия клас
        public override void MakeSound()
        {
            Console.WriteLine($"{Name} казва: Бау-бау!");
        }
    }

    class Cat : Animal
    {
        public Cat(string name) : base(name) { }

        public override void MakeSound()
        {
            Console.WriteLine($"{Name} казва: Мяу!");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Полиморфизъм: списък от базовия тип, обекти от различни наследници
            List<Animal> animals = new List<Animal>
            {
                new Dog("Шаро"),
                new Cat("Писана"),
                new Animal("Нещо мистериозно")
            };

            foreach (Animal animal in animals)
            {
                animal.MakeSound(); // една команда - различно поведение!
            }
        }
    }
}

Какво се случва тук?

  • class Dog : Animal — кучето наследява всичко от Animal: свойството Name и метода MakeSound. В Dog не пишем Name отново — то идва наготово.
  • Конструкторът на Dog не пази името сам — той го предава нагоре с : base(name) към конструктора на Animal. Това е задължително, защото Animal няма конструктор без параметри.
  • virtual в базовия клас казва "наследниците могат да ме заменят"; override в наследника извършва замяната. Без тази двойка компилаторът отказва или поведението не е полиморфно.
  • Ключовият момент е в Main: списъкът е от тип List<Animal>, но вътре живеят Dog, Cat и Animal. Това е позволено, защото всеки от тях Е Animal.
  • В цикъла извикваме animal.MakeSound() върху променлива от тип Animal — но се изпълнява версията на реалния обект: Бау-бау!, Мяу!, или базовата. Една команда, много форми — това е полиморфизмът.
  • Без полиморфизъм щеше да ни трябва проверка на типа и отделен код за всяко животно — а при добавяне на ново животно щяхме да пипаме навсякъде. Сега просто създаваме нов клас с override.

protected и base.Метод(): Employee → Manager

Program.cs
using System;
using System.Collections.Generic;

namespace ModuleTwelveExtra
{
    class Employee
    {
        public string Name { get; set; }

        // protected: видимо за класа И за наследниците му
        protected double baseSalary;

        public Employee(string name, double salary)
        {
            Name = name;
            baseSalary = salary;
        }

        public virtual double CalculateSalary()
        {
            return baseSalary;
        }
    }

    class Manager : Employee
    {
        private double bonus;

        public Manager(string name, double salary, double bonus)
            : base(name, salary)
        {
            this.bonus = bonus;
        }

        public override double CalculateSalary()
        {
            // ползваме базовото изчисление и го НАДГРАЖДАМЕ
            return base.CalculateSalary() + bonus;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<Employee> staff = new List<Employee>
            {
                new Employee("Иван", 2000),
                new Manager("Елена", 2500, 800)
            };

            foreach (Employee person in staff)
            {
                Console.WriteLine($"{person.Name}: {person.CalculateSalary()} лв.");
            }
        }
    }
}

Какво се случва тук?

  • protected double baseSalary — средата между private и public: скрито за външния свят, но достъпно за наследника Manager. Ако беше private, Manager нямаше да може да го ползва (грешка CS0122).
  • Конструкторът на Manager приема три стойности, но пази само бонуса — името и заплатата ги предава нагоре с : base(name, salary).
  • this.bonus = bonus;this различава полето от едноименния параметър.
  • Ключовият ред: return base.CalculateSalary() + bonus; — override-ът не заменя изцяло поведението, а го надгражда. Промени ли се базовата формула, мениджърската се обновява автоматично.
  • В цикъла отново работи полиморфизмът: за Иван се изпълнява базовата версия (2000), за Елена — мениджърската (2500 + 800 = 3300), при едно и също извикване person.CalculateSalary().

Внимавай! Чести грешки

Грешките, които почти всеки прави в тази тема — виж разликата между грешния и правилния код.

Замяна без virtual/override (скриване)

Грешно

class Animal
{
    public void MakeSound() { ... } // без virtual
}
class Dog : Animal
{
    public void MakeSound() { ... } // CS0108: hides inherited member
}

Правилно

class Animal
{
    public virtual void MakeSound() { ... }
}
class Dog : Animal
{
    public override void MakeSound() { ... }
}

Без двойката virtual/override наследникът само скрива метода: през променлива от тип Animal се вика базовата версия и полиморфизмът не работи. Компилаторът предупреждава с CS0108 — не го подминавайте.

Забравено : base(...) в конструктора

Грешно

class Dog : Animal
{
    public Dog(string name) { }
    // CS7036: Animal няма конструктор без параметри
}

Правилно

class Dog : Animal
{
    public Dog(string name) : base(name) { }
}

Когато базовият клас има само конструктор с параметри, наследникът е длъжен да го извика с : base(...) — иначе компилаторът няма как да построи "родителската част" на обекта.

Наследяване при връзка "има", а не "е"

Грешно

class Car : Engine
{
    // колата НЕ Е двигател...
}

Правилно

class Car
{
    private Engine engine; // колата ИМА двигател
}

Тестът е прост: изречението "X е Y" трябва да звучи вярно (Кучето Е животно ✓, Колата Е двигател ✗). При "има" връзка обектът се влага като поле — това се нарича композиция.

Достъп до private поле на родителя

Грешно

class Animal
{
    private string name;
}
class Dog : Animal
{
    public void Print()
    {
        Console.WriteLine(name); // CS0122
    }
}

Правилно

class Animal
{
    protected string name;
    // или: public string Name { get; set; }
}

private отрязва дори наследниците. Ако наследникът има нужда от полето — направете го protected или му дайте публично/защитено свойство.

Задачи за самостоятелна работа

Опитай първо сам — подсказката е там само ако наистина закъсаш.

Задача 1: Йерархия от фигури

Създайте базов клас Shape с virtual метод GetArea(), който връща 0. Наследете го с Circle (радиус) и Rectangle (две страни), всеки със собствено изчисление на лицето. Съберете няколко фигури в List<Shape> и отпечатайте лицата им в цикъл.

Задача 2: Person → Student и Teacher

Създайте клас Person (Име, virtual метод Introduce). Наследете го със Student (има ФакултетенНомер) и Teacher (има Предмет). Всеки override-ва Introduce със своя версия, която ИЗПОЛЗВА и базовата чрез base.Introduce().

Задача 3: Разширете зоопарка

Към йерархията Animal от примера добавете класове Bird (звук "Чик-чирик!") и Fish, чийто MakeSound() отпечатва "...(рибите мълчат)". Съберете по едно животно от всеки вид в List<Animal> и ги "разпейте" в цикъл.

Мини-тест за проверка

Отговори си наум (или на глас!) и чак тогава разкрий отговора.

1. Какво означава записът class Student : Person?

2. За какво служат ключовите думи virtual и override и защо вървят по двойки?

3. Какво е полиморфизъм, с прости думи?

4. Какво дава модификаторът protected и по какво се различава от private?

Речник на термините

Виж пълния речник на курса
Наследяване
Клас получава полетата, свойствата и методите на друг: class Student : Person. Връзка "е-вид" (is-a).
Базов клас
Родителят в йерархията — общите данни и поведение живеят в него.
Наследник (derived class)
Класът, който наследява — получава всичко от базовия и добавя/променя своето.
Полиморфизъм
"Много форми" — едно извикване изпълнява различен код според реалния тип на обекта.
virtual / override
Двойката, която позволява замяна на метод: базовият разрешава с virtual, наследникът заменя с override.
base
Достъп до родителя: base(...) вика родителския конструктор, base.Method() — родителската версия на метода.
protected
Достъпно за класа и наследниците му, скрито за всички останали.
Абстрактен клас
Клас-основа, който не може да се инстанцира; абстрактните му методи са без тяло и наследниците са длъжни да ги override-нат.
Композиция
Влагане на обект като поле — правилният избор при връзка "има" (Колата ИМА двигател).

Провери знанията

Бърз тест с избор от отговори върху термините на модула — с точки и моментална обратна връзка.

Провери знанията си

9 въпроса върху термините от модула. Показваме определение — ти избираш правилния термин.

Препоръки за този модул

  • Нарисувайте йерархията на хартия със стрелки "наследява" преди да пишете код — Person ← Student, Person ← Teacher.
  • Тествайте полиморфизма през List от базовия тип: напълнете List<Animal> с различни животни и извикайте метода в цикъл — това е "аха!" моментът.
  • Махнете virtual или override нарочно и прочетете какво казва компилаторът — така ще запомните защо двете вървят заедно.
  • Override-нете ToString() в собствен клас — Console.WriteLine(obj) изведнъж започва да печата нещо смислено. Това е полиморфизмът, който ползвате от Модул 1, без да знаете.

Начален курс по C# — учебен наръчник и бърз справочник за студенти.

Натисни Ctrl K или / отвсякъде, за да търсиш.