Поиск

Полиморфизм

Подмена методов с помощью ключевого слова new замечательно работает, если у вас есть ссылка на производный объект. А что будет, если у вас есть ссылка, приведенная к базовому классу, но нужно, чтобы ком- \ пилятор вызывал реализацию метода из производного класса? Это имен- \ но тот момент, когда в дело вступает полиморфизм. Полиморфизм позволяет вам многократно определять методы в иерархии классов так, что в период выполнения в зависимости от того, какой именно объект используется, вызывается соответствующая версия данного метода. \ Обратимся к нашему примеру с сотрудником. Приложение PolylApp работает корректно, поскольку у нас есть два объекта: Employee и Sala-rfedEmployee. В более практичном приложении мы, вероятно, считыва-лИ бы все записи о сотрудниках из БД и заполняли бы ими массив. Хотя некоторые сотрудники работают по контракту, а другим выплачивается зарплата, все они должны быть помещены в массив в виде объектов одного типа — базового класса Employee. Но при циклической обработке массива, при которой происходит получение каждого объекта и вызов его метода CalculatePay, нам бы хотелось, чтобы компилятор вызывал подходящую реализацию метода CalculatePay. \

В следующем примере я добавил новый класс — ContractEmployee. Главный класс приложения теперь содержит массив объектов типа Employee и два дополнительных метода: LoadEmployees, загружающий объекты «сотрудник» в массив, и DoPayroll, обрабатывающий массив, вызывая метод CalculatePay для каждого объекта.

using System;
class Employee {
public Employee(string name)
{
this.Name = name;
>
protected string Name; public string name {
get
{
return this.Name;
} >
public void CalculatePayO {
Console.WriteLine("Employee.CalculatePay вызван " + "для {0}", name);
} }
class ContractEmployee : Employee {
public ContractEmployee(string name)
: base(name)
{
}
public new void CalculatePayO {
Console.WriteLine("ContractEmployee.CalculatePay вызван "
"для {0}", name); } }
class SalariedEmployee : Employee {
public SalariedEmployee (string name)
: base(name)
{
}
public new void CalculatePayO {
Console.WriteLine("SalariedEmployee.CalculatePay вызван " н
"для {О}", name); } >
class Poly2App {
protected Employee[] employees; public void LoadEmployeesO {
// Имитация загрузки информации из базы данных, employees = new Employee[2];
employees[0] = new ContractEmployee("Kate Dresen");
employees[1] = new SalariedEmployee("Megan Sherman"); }
public void DoPayrollO {
foreach(Employee emp in employees)
I <
\ emp. CalculatePayO;
I public static void Main()
\ < \ Poly2App poly2 = new Poly2App();
I poly2. LoadEmployeesO; \ poly2. DoPayrollO;
' } }

Однако, запустив это приложение, вы получите такую информацию:

c:\>Poly2App
Employee.CalculatePay вызван для Kate Dresen
Employee.CalculatePay вызван для Megan Sherman

Ясно, что это совсем не то, что нам нужно: для каждого объекта Bfy-зывается реализация метода CalculatePay из базового класса. То, что здесь происходит, — пример раннего связывания (early binding). Во время компиляции этого кода компилятор С#, изучив вызов emp.CalculatePay, определил адрес памяти, на который ему нужно перейти во время этого вызова. В этом случае это будет адрес метода Employee.CalculatePay.

Взгляните на следующий MSIL-код, сгенерированный из приложения Ро1у2Арр, и обратите особое внимание на строку IL_0014 и тот факт, что в ней производится явный вызов метода Employ ее.Calculate Pay.

.method public hidebysig instance void DoPayrollO il managed {
// Размер кода 34 (0x22) .maxstack 2
.locals (class Employee V_0, class Employee[] V_1, int32 V_2, int32 V_3) ILJJOOO: ldarg.0
IL_0001: Idfld class Employee[] Poly2App::employees
IL_0006: stloc.1
IL_0007: ldloc.1
IL_0008: Idlen
IL_0009: conv.14
IL.OOOa: stloo.2
ILJJOOb: ldo.14.0
IL_OOOc: stloc.3 I
ILJWOd: br.s IL_001d
IL_OOOf: ldloc.1 /
IL_0010: ldloc.3 /
IL_0011: Idelem.ref /
IL_0012: stloc.O /
IL_0013: ldloc.0 /
IL_0014: call instance void Employee::CalculatePay() /
IL_0019: ldloc.3 /
IL_001a: ldc.14.1 !
IL_001b: add
IL_001c: stloc.3
IL_001d: ldloc.3
IL_001e: ldloc.2
IL_001f: blt.s ILJWOf
IL_0021: ret
} // конец метода Poly2App::DoPayroll

Этот вызов метода Employee.CalculatePay представляет собой проблему. Мы хотим, чтобы вместо раннего связывания происходило динамическое (позднее) связывание (late binding). Динамическое связывание означает, что компилятор не выбирает метод для исполнения до периода выполнения. Чтобы заставить компилятор выбрать правильную версию метода объекта мы используем два новых ключевых слова: virtual и over-rids. С методом базового класса применяется ключевое слово virtual, a override — с реализацией метода в производном классе. Вот еще один пример, который на этот раз работает должным образом!

using System;
class Employee {
public Employee(string name)
{
this.Name = name;
>
protected string Name; public string name {
get
{
return this.Name;
\ virtual public void CalculatePayO
\ <
I Console.WriteLine("Employee.CalculatePay вызван " +
\ "для {О}", name);
} }
class ContractEmployee : Employee {
public ContractEmployee(string name)
: base(name)
{
>
override public void CalculatePayO
{
Console.WriteLine("ContractEmployee.CalculatePay вызван "
*'
"для {0}", name);
} >
class SalariedEmployee : Employee <
public SalariedEmployee (string name)
: base(name)
{
}
override public void CalculatePayO
<
Console.WriteLine("SalariedEmployee.CalculatePay вызван " + "для {0}", name);
} }
class PolySApp
{
protected Employee[] employees; public void LoadEmployeesO
// Имитация загрузки информации из базы данных. /
employees = new Employee[2]; I
employees[0] = new ContractEmployee("Kate Dresen"); /
employees[1] = new SalariedEmployee("Megan Sherman"); /
> /
public void DoPayrolK) /
{ /' foreach(Employee emp in employees) i
{ ' / emp.CalculatePayO;
} }
public static void Main()
{
Poly3App poly3 = new Poly3App();
poly3. LoadEmployeesO;
poly3. DoPayrolK); } }

Перед запуском этого приложения взглянем на сгенерированный для него IL-код. Заметьте, что на этот раз в строке ILJ)014 используется код операции MSIL callvirt, который сообщает компилятору, что до периода выполнения неизвестно, какой именно метод будет вызван, так как он зависит от вида используемого производного объекта.

.method public hidebysig instance void DoPayrolK) il managed
{
// размер кода 34 (0x22) .maxstack 2 .locals (class Employee V_0,
class Employee[] V_1,
int32 V_2,
int32 v_3) ILJJOOO: ldarg.0
IL_0001: Idfld class Employee[] PolySApp::employees Il__0006:
stloc.1 IL_0007: ldloc.1
IL_0008: Idlen
IL_0009: conv.i4
IL_OOOa: stloc.2
ILJWOb: Idc.i4.0
IL_OOOc: stloc.3
IL.OOOd: br.s IL_001d
IL_OOOf: ldloc.1
IL_0010: ldloc.3
IL_0011: Idelem.ref
IL_0012: stloc.O
IL_0013: ldloc.0
IL_0014: callvirt instance void Employee::CalculatePay()
IL.0019: ldloc.3
IL_001a: Idc.i4.1
IL_001b: add
IL_001c: stloc.3
IL_001d: ldloc.3
IL_001e: ldloc.2
IL_001f: blt.S IL_OOOf
IL.0021: ret
> // конец метода PolySApp;:DoPayroll

Выполнение такого коды приведет к следующим результатам:

с:\>Ро!уЗАрр
ContractEmployee.CalculatePay вызван для Kate Dresen
SalariedEmployee.CalculatePay вызван для Megan Sherman

ПРИМЕЧАНИЕ Виртуальные методы нельзя объявлять как закрытые, поскольку они по определению не будут видимы в производных классах.