Към съдържанието
wannacode

Наследяване

Проблем

Две копия на един и същ код

Програмата за заплати има клас Employee с име, заплата и метод за повишение. После трябваше и мениджър, който има и бонус. Най-бързо беше да копираш Employee в нов клас Manager и да добавиш бонуса.

Месец по-късно някой въвежда повишение -10% вместо 10%. Затова в Employee добавяш проверка. В Manager я забравяш:

// в Employee
public void Raise(int percent)
{
if (percent > 0)
{
Salary += Salary * percent / 100;
}
}
// в Manager: копие отпреди проверката
public void Raise(int percent)
{
Salary += Salary * percent / 100;
}
Employee ivan = new Employee("Иван", 2000);
Manager maria = new Manager("Мария", 3000, 500);
ivan.Raise(-10);
maria.Raise(-10);
Console.WriteLine(ivan);
Console.WriteLine(maria);
Изход
Иван: 2000 €
Мария: 2700 €

Заплатата на Иван е защитена, а на Мария падна с 300 €. Двата класа са еднакви на 90%, но всяка поправка трябва да се прави два пъти.

Идеята

Общото е в родителя

Мениджърът е служител, само с нещо повече. Кажи го на езика: class Manager : Employee. В Manager пишеш само новото, бонуса:

class Manager : Employee
{
public int Bonus { get; private set; }
public Manager(string name, int salary, int bonus) : base(name, salary)
{
Bonus = bonus;
}
}
Manager maria = new Manager("Мария", 3000, 500);
maria.Raise(-10);
maria.Raise(10);
Console.WriteLine(maria);
Console.WriteLine("Бонус: " + maria.Bonus + " €");
Изход
Мария: 3300 €
Бонус: 500 €

Manager вече има всичко на Employee: име, заплата, Raise() с проверката, ToString(). Затова повишението с -10% не мина. Проверката е написана веднъж и важи и за двата класа.

Това се казва наследяване. Employee е родителят (базов клас), а Manager е наследникът. Наследникът взима всичко на родителя и добавя своето.

Проверката дали наследяването има смисъл е изречението „X е Y“. „Мениджърът е служител“ е вярно. „Фактурата е служител“ не е, затова фактурата не наследява Employee, дори да има сума и име.

Пробвай

Какво получава стажантът?

Intern няма нищо свое, освен конструктор:

Какво ще отпечата програмата?

class Intern : Employee
{
public Intern(string name, int salary) : base(name, salary)
{
}
}
Intern ana = new Intern("Ана", 1200);
ana.Raise(5);
Console.WriteLine(ana);
Отговор

Идеята

Първо родителят

name и salary са private в Employee. Наследникът ги има, но не ги вижда: за него те са скрити като за всеки друг клас. Как тогава Manager им дава стойност?

Като извика конструктора на родителя:

public Manager(string name, int salary, int bonus) : base(name, salary)
{
Bonus = bonus;
}

: base(name, salary) вика конструктора на Employee с тези аргументи. Той подрежда името и заплатата, а после тялото на конструктора на Manager подрежда бонуса. base се пише след скобите на конструктора, преди тялото.

Обектът е един. В него има и частта на родителя, и частта на наследника, а всяка част се подрежда от своя конструктор.

public Manager(string name, int salary, int bonus) : base(name, salary)
{
Bonus = bonus;
}
Manager maria = new Manager("Мария", 3000, 500);
Console.WriteLine(maria + ", бонус " + maria.Bonus + " €");
Стъпка по стъпка, с думи
  1. new заделя един обект. В него има частта на Employee и частта на Manager.
  2. Започва конструкторът на Manager с трите аргумента.
  3. Първо родителят: base(name, salary) вика конструктора на Employee, преди тялото.
  4. Конструкторът на Employee подрежда своята част: име и заплата.
  5. Чак после тялото на Manager подрежда бонуса.
  6. Обектът е един, а всяка част е подредена от своя конструктор.
По-точно, за любопитните
  • Конструкторите не се наследяват. Manager няма конструктор Manager(string name, int salary), освен ако не го напишеш.
  • Ако родителят има само конструктор с параметри, а наследникът не подаде нищо, програмата не се компилира. Компилаторът не знае какво име и каква заплата да даде.

Пробвай

Кой конструктор е пръв?

Тук наследникът не вика родителя изрично:

Какво ще отпечата програмата?

class Account
{
public Account()
{
Console.WriteLine("Сметката е открита");
}
}
class SavingsAccount : Account
{
public SavingsAccount()
{
Console.WriteLine("Лихва: 3%");
}
}
SavingsAccount savings = new SavingsAccount();
Отговор

Пробвай

Намери грешката

Мария трябва да има заплата 3000 € и бонус 500 €. Програмата обаче печата:

Изход
Мария: 500 €

На кой ред е грешката?

Избери реда с грешката.

Идеята

protected: само за наследниците

В края на годината бонусът на мениджъра влиза в заплатата му. Методът е в Manager, но заплатата е private в Employee. Getter не стига, трябва да я промениш.

Има трето ниво на достъп между private и public: protected. Такъв член виждат класът и наследниците му, а останалите не го виждат:

class Employee
{
public string Name { get; private set; }
public int Salary { get; protected set; }
// ... останалото е както преди
}
class Manager : Employee
{
// ... Bonus и конструкторът
public void MergeBonus()
{
Salary += Bonus;
Bonus = 0;
}
}
Manager maria = new Manager("Мария", 3000, 500);
maria.MergeBonus();
Console.WriteLine(maria);
Console.WriteLine("Бонус: " + maria.Bonus + " €");
Изход
Мария: 3500 €
Бонус: 0 €

В C# по-често се отваря не цялото свойство, а само записът: { get; protected set; }. Всеки може да прочете заплатата, но само Employee и наследниците му могат да я сменят.

protected отслабва защитата: всеки наследник може да смени заплатата, без да мине през проверката в Raise(). Затова по подразбиране полетата остават private. protected е за случаите, в които наследникът наистина трябва да пипа полето.

По-точно, за любопитните
  • В C# protected е само за класа и наследниците му. Има и protected internal: наследниците плюс всичко от същия проект.

Пробвай

Кое липсва?

Стажантът е служител. Какво трябва да пише на празното място?

class Intern  Employee
{
    public Intern(string name, int salary) : base(name, salary)
    {
    }
}

Идеята

Един родител и общ прародител

Класът има точно един родител. Не може да наследи едновременно Employee и Student. Затова наследяването е единично.

Веригата може да е дълга. Intern наследява Employee. А кого наследява Employee, щом не пише :? Всеки клас без родител наследява object. Това е общият родител на всички класове. Затова и празен клас има методи:

class Ticket
{
}
Ticket a = new Ticket();
Ticket b = new Ticket();
Console.WriteLine(a.Equals(a));
Console.WriteLine(a.Equals(b));
Console.WriteLine(a.ToString());
Изход
True
False
Ticket

Equals() идва от object. Засега той казва само дали две променливи сочат един и същ обект.

Оттам е и ToString(). Ако не го напишеш, връща името на класа, затова горе се печата „Ticket“. В модул „Класове“ писа свой ToString() с override. Сега знаеш, че така заменяш версията на object. Какво точно значи override, е в следващия урок.

По-точно, за любопитните
  • Клас със sealed пред class не може да се наследява. Такъв е string.
  • object е кратко име на System.Object. Променлива от тип object може да сочи какъвто и да е обект: object anything = maria;.

Упражнение

Наследници

Реши задачите в редактора си. Родителските класове са от модул „Класове“. Ако не са ти под ръка, напиши ги наново: те са кратки.

1. Семейна кола. SimpleCar има скорост и метод Accelerate(), който я вдига с 10. Напиши FamilyCar, който наследява SimpleCar, започва с 4 места и има метод AddSeat(). Създай семейна кола, ускори я и добави място:

Изход
Скорост: 10 км/ч
Места: 5

2. Студент. Person има име, възраст и Introduce(). Напиши Student, който наследява Person и добавя среден успех Grade (дробно число). Използвай конструктора на родителя.

Изход
Аз съм Елена, на 19 години.
Успех: 5.5

3. Брейк. DanceMove има име, продължителност в секунди и Perform(). Напиши BreakDanceMove със сложност от 1 до 10 и метод ShowComplexity().

Изход
Изпълнявам moonwalk за 5 секунди
Изпълнявам windmill за 12 секунди
windmill: сложност 8 от 10

4. Електрическа кола. Car има марка, гориво в литри и изминати километри. Refuel(liters) долива, а Drive(distance) харчи 1 литър на 10 км и добавя километрите (ако горивото не стига, казва го и не тръгва). Напиши ElectricCar:

  • без гориво, с батерия, която започва от 100%;
  • с ефективност: колко км изминава с 1% батерия (за „Тесла“ 5);
  • Charge() зарежда батерията до 100%;
  • DriveElectric(distance) харчи батерия вместо гориво и също добавя километрите.
Километрите са private в Car. Как ги променя наследникът? Подсказка: картата за protected.
Изход
Шкода: 150 км, гориво 35 л
Тесла: 200 км, батерия 60%
Батерията не стига за 400 км
Тесла: 600 км, батерия 20%

Още задачи

  • Работник. Worker наследява Person, добавя длъжност и метод Work():
Изход
Аз съм Петър, на 34 години.
Работя като счетоводител
  • Стажант с ментор. Добави в Intern поле с името на ментора и го подай в конструктора, заедно с името и заплатата за родителя.

Тест

Провери се

1. class Manager : Employee. Кое е вярно?
2. Полето name е private в Employee. Може ли Manager да напише name = "Мария"; в свой метод?
3. Какво прави : base(name, salary) в конструктора на Manager?
4. Колко родителя може да има един клас?
5. Кой вижда protected поле?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·