Полиморфизъм
Проблем
Заплата с if по вид
Във фирмата има служители, мениджъри с бонус и стажанти на половин работен ден. Някой е направил един клас Employee с поле „роля“, а заплатата се смята така:
static int MonthlyPay(Employee e){ if (e.Role == "мениджър") { return e.Salary + e.Bonus; } else if (e.Role == "стажант") { return e.Salary / 2; } return e.Salary;}Работи. Но после идва нов вид служител, на хонорар. Трябва нов else if тук, и в метода за данъците, и в метода за фиша, и навсякъде, където някой е питал за ролята. А всеки служител носи поле за бонус, макар че само мениджърите имат бонус.
Кодът за мениджъра е разпилян из цялата програма, вместо да е в мениджъра.
Идеята
Всеки клас казва сам
С наследяването от миналия урок Manager и Intern са отделни класове. Нека всеки сам каже каква е месечната му заплата. Родителят има обща версия, а наследникът пише своя версия на същия метод:
class Employee{ // ... Name, Salary и конструкторът
public virtual int MonthlyPay() { return Salary; }}
class Manager : Employee{ // ... Bonus и конструкторът
public override int MonthlyPay() { return Salary + Bonus; }}Employee ivan = new Employee("Иван", 2000);Manager maria = new Manager("Мария", 3000, 500);Console.WriteLine(ivan.MonthlyPay());Console.WriteLine(maria.MonthlyPay());20003500Това се казва презаписване на метод (override). Наследникът пише метод със същото име, същите параметри и същия тип на резултата. За обектите от наследника неговата версия заменя тази на родителя.
В C# това се договаря от двете страни. Родителят казва, че методът може да се презаписва, с virtual. Наследникът казва, че го презаписва, с override. Без virtual в родителя override не се компилира.
По-точно, за любопитните
- Ако наследникът напише метод със същото име без
override, компилаторът предупреждава, че методът скрива този на родителя. Ако наистина искаш това, пишешnew. Но скритият метод не е презаписан: през променлива от типа на родителя се вика версията на родителя.
class Report{ public virtual string Title() { return "Отчет"; }}
class SalesReport : Report{ public new string Title() { return "Отчет за продажбите"; }}
SalesReport sales = new SalesReport();Report report = sales;Console.WriteLine(sales.Title());Console.WriteLine(report.Title());Отчет за продажбитеОтчетsealed overrideказва „по-нататъшните наследници не могат да го сменят пак“.- Наследникът не може да направи метода по-скрит: ако е
publicв родителя, оставаpublic.
Пробвай
Какво се смени?
Стажантът презаписва само метода за месечната заплата:
Какво ще отпечата програмата?
class Intern : Employee{ public Intern(string name, int salary) : base(name, salary) { }
public override int MonthlyPay() { return Salary / 2; }}
Intern ana = new Intern("Ана", 1600);Console.WriteLine(ana.MonthlyPay());Console.WriteLine(ana.Salary);MonthlyPay() на стажанта връща половината заплата. Самата заплата не се променя, а Salary е наследен от Employee без промяна.
Идеята
Един масив за всички
Мениджърът е служител. Затова променлива от тип Employee може да сочи обект от тип Manager, а масивът Employee[] може да пази всякакви служители:
Employee[] staff ={ new Employee("Иван", 2000), new Manager("Мария", 3000, 500), new Intern("Ана", 1600)};foreach (Employee e in staff){ Console.WriteLine(e.Name + ": " + e.MonthlyPay() + " €");}Иван: 2000 €Мария: 3500 €Ана: 800 €Цикълът вика един и същ метод, e.MonthlyPay(), а резултатът е различен. Изпълнява се версията на обекта, а не на типа на променливата. Това се казва полиморфизъм: на гръцки „много форми“. Едно извикване, а всеки обект отговаря по своя начин.
Сега новият вид служител е нов клас с негов MonthlyPay(). Цикълът и останалата програма не се пипат. Няма if по роля, няма поле за бонус в служителите без бонус.
Типът на променливата обаче решава какво можеш да извикаш. През e не можеш да стигнеш до бонуса, дори ако обектът е мениджър: компилаторът знае само, че e е някакъв Employee. Как се стига до неща, които имат само някои обекти, ще видим в урока „Какъв е всъщност?“.
Виж го
Кой метод се изпълнява
Employee[] staff ={ new Employee("Иван", 2000), new Manager("Мария", 3000, 500), new Intern("Ана", 1600)};foreach (Employee e in staff){ Console.WriteLine(e.Name + ": " + e.MonthlyPay() + " €");}Стъпка по стъпка, с думи
- Масивът е от тип Employee, но обектите са от три класа. Всяко място пази стрелка.
- e сочи Иван, обикновен Employee: изпълнява се MonthlyPay на Employee.
- e сочи Мария, Manager: изпълнява се нейният метод, заплата плюс бонус.
- e сочи Ана, Intern: стажантът взима половината от заплатата.
- Един и същ ред, три различни метода. Решава обектът, не типът на променливата.
Масивът пази три стрелки от тип Employee. Всяка сочи обект от различен клас. При всеки кръг e сочи следващия обект, а извикването отива в метода на този обект: първо в Employee, после в Manager, после в Intern.
Изборът става, докато програмата работи, а не при компилирането. Компилаторът не знае какво ще има в масива. Той проверява само, че всеки Employee има такъв метод.
Пробвай
Намери грешката
Програмата е същата, но стажантът получава цялата заплата:
Иван: 2000 €Мария: 3500 €Ана: 1600 €Ето класа Intern. На кой ред е грешката?
Избери реда с грешката.
override. Без него методът на Intern само скрива този на Employee, не го презаписва. Затова през променлива от тип Employee се вика версията на родителя. Компилаторът предупреждава: „hides inherited member … To make the current member override that implementation, add the override keyword“. Поправката е public override int MonthlyPay().Идеята
Допълни родителя, не го преписвай
Понякога наследникът не иска да замени метода изцяло, а да добави нещо към него. Мениджърът се описва като служител, плюс бонуса. Версията на родителя се вика с base.ToString():
public override string ToString(){ return base.ToString() + ", бонус " + Bonus + " €";}Console.WriteLine(new Employee("Иван", 2000));Console.WriteLine(new Manager("Мария", 3000, 500));Иван (2000 €)Мария (3000 €), бонус 500 €Ако Employee смени как се описва, мениджърът ще го последва сам.
Това е и ToString() от модул „Класове“. Там вече презаписваше метод: версията на object, която е virtual. Затова пишеше override.
По-точно, за любопитните
base.Method()може да се извика от всеки метод на наследника, не само от презаписания.
- Презаписването не е претоварване от модул „Методи“. При претоварването името е същото, а параметрите са различни, и компилаторът избира по аргументите. При презаписването всичко е същото, а избира обектът, докато програмата работи.
Пробвай
Кое липсва?
Стажантът се описва като служител и думата „стажант“. Какво трябва да пише на празното място?
class Intern : Employee
{
public Intern(string name, int salary) : base(name, salary)
{
}
public override string ToString()
{
return .ToString() + ", стажант";
}
}base.ToString() дава „Ана (1600 €)“, а наследникът добавя „, стажант“. Без base. методът щеше да вика сам себе си, без край.
Упражнение
Всеки по своему
Реши задачите в редактора си. Във всяка задача има масив от родителския тип и един цикъл.
1. Приют. Клас Animal с име и метод MakeSound(), който печата „някакъв звук“. Dog и Cat го презаписват. Сложи по едно от трите в масив Animal[]:
Гост: някакъв звукРекс: Бау!Мици: Мяу, мяу!2. Студентът се представя. Student от миналия урок вече има успех. Презапиши Introduce(), така че да казва и успеха. Използвай base.Introduce(), не преписвай реда на Person.
Аз съм Георги, на 45 години.Аз съм Елена, на 19 години.Среден успех: 5.53. Купон. Клас Performance със заглавие и метод Start(). DanceMove, BreakDanceMove (наследява DanceMove, добавя сложност) и Song го презаписват. Клас Party пази масив Performance[] и има метод StartParty(), който стартира всичко поред и накрая казва, че купонът е започнал.
Танцуваме moonwalkТанцуваме windmill сложност 8 от 10Пеем 'Bohemian Rhapsody'Започва томболаКупонът започна!Още задачи
- Хонорар. Добави към служителите
Contractorс часове и ставка на час. Месечната му заплата е часове × ставка. Сметни фонда на заплатите за Иван, Мария, Ана и Петър (60 часа по 25 €):
Фонд работна заплата: 7800 €- Фиш. Добави на
EmployeeметодPayslip(), който връща текст „име: заплата €“ с месечната заплата. Напиши го само вEmployee. Защо работи вярно и за мениджъра, и за стажанта?
Тест