Наследяване
Проблем
Две копия на един и същ код
Програмата за заплати има клас Employee с име, заплата и метод за повишение. После трябваше и мениджър, който има и бонус. Най-бързо беше да копираш Employee в нов клас Manager и да добавиш бонуса.
Месец по-късно някой въвежда повишение -10% вместо 10%. Затова в Employee добавяш проверка. В Manager я забравяш:
// в Employeepublic 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);Raise() и ToString() са наследени от Employee. 5% от 1200 са 60, а 1200 + 60 = 1260.
Идеята
Първо родителят
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 + " €");Стъпка по стъпка, с думи
- new заделя един обект. В него има частта на Employee и частта на Manager.
- Започва конструкторът на Manager с трите аргумента.
- Първо родителят: base(name, salary) вика конструктора на Employee, преди тялото.
- Конструкторът на Employee подрежда своята част: име и заплата.
- Чак после тялото на Manager подрежда бонуса.
- Обектът е един, а всяка част е подредена от своя конструктор.
По-точно, за любопитните
- Конструкторите не се наследяват.
Managerняма конструкторManager(string name, int salary), освен ако не го напишеш. - Ако родителят има само конструктор с параметри, а наследникът не подаде нищо, програмата не се компилира. Компилаторът не знае какво име и каква заплата да даде.
Пробвай
Кой конструктор е пръв?
Тук наследникът не вика родителя изрично:
Какво ще отпечата програмата?
class Account{ public Account() { Console.WriteLine("Сметката е открита"); }}
class SavingsAccount : Account{ public SavingsAccount() { Console.WriteLine("Лихва: 3%"); }}
SavingsAccount savings = new SavingsAccount();: base(...), компилаторът сам добавя извикване на конструктора на родителя без параметри. Родителят винаги е пръв. Затова първо се печата „Сметката е открита“.Пробвай
Намери грешката
Мария трябва да има заплата 3000 € и бонус 500 €. Програмата обаче печата:
Мария: 500 €На кой ред е грешката?
Избери реда с грешката.
На родителя е подаден бонусът, а не заплатата. Аргументите се свързват с параметрите по място, затова Employee получава 500 като заплата. Поправката е : base(name, salary).
Идеята
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)
{
}
}class Intern : Employee. Същото двоеточие стои и пред base(...).Идеята
Един родител и общ прародител
Класът има точно един родител. Не може да наследи едновременно 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());TrueFalseTicketEquals() идва от 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 км/чМеста: 52. Студент. Person има име, възраст и Introduce(). Напиши Student, който наследява Person и добавя среден успех Grade (дробно число). Използвай конструктора на родителя.
Аз съм Елена, на 19 години.Успех: 5.53. Брейк. DanceMove има име, продължителност в секунди и Perform(). Напиши BreakDanceMove със сложност от 1 до 10 и метод ShowComplexity().
Изпълнявам moonwalk за 5 секундиИзпълнявам windmill за 12 секундиwindmill: сложност 8 от 104. Електрическа кола. 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поле с името на ментора и го подай в конструктора, заедно с името и заплатата за родителя.
Тест