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