При раннем связывании программа, готовая для выполнения, представляет собой структуру, логика выполнения которой жестко определена. Если же требуется, чтобы решение о том, какой из одноименных методов разных объектов иерархии использовать, принималось в зависимости от конкретного объекта, для которого выполняется вызов, то заранее жестко связывать эти методы с остальной частью кода нельзя.
Следовательно, надо каким-то образом дать знать компилятору, что эти методы будут обрабатываться по-другому. Для этого в С# существует ключевое слово virtual. Оно записывается в заголовке метода базового класса, например:
virtual public void Passport()...
Слово virtual в переводе с английского значит «фактический». Объявление метода виртуальным означает, что все ссылки на этот метод будут разрешаться по факту его вызова, то есть не на стадии компиляции, а во время выполнения программы. Этот механизм называется поздним связыванием.
Для его реализации необходимо, чтобы адреса виртуальных методов хранились там, где ими можно будет в любой момент воспользоваться, поэтому компилятор формирует для этих методов таблицу виртуальных методов (Virtual Method Table, VMT). В нее записываются адреса виртуальных методов (в том числе унаследованных) в порядке описания в классе. Для каждого класса создается одна таблица. Каждый объект во время выполнения должен иметь доступ к VMT. Обеспечение этой связи нельзя поручить компилятору, так как она должна устанавливаться во время выполнения программы при создании объекта. Поэтому связь экземпляра объекта с VMT устанавливается с помощью специального кода, автоматически помещаемого компилятором в конструктор объекта. Если в производном классе требуется переопределить виртуальный метод, используется ключевое слово override, например:
|
|
override public void Passport()...
Переопределенный виртуальный метод должен обладать таким же набором параметров, как и одноименный метод базового класса. Это требование вполне естественно, если учесть, что одноименные методы, относящиеся к разным классам, могут вызываться из одной и той же точки программы.
Добавим в листинг 8.2 два волшебных слова — virtual и override — в описания методов Passport, соответственно, базового и производного классов (листинг 8.3).
Листинг 8.3. Виртуальные методы
using System;
namespace ConsoleApplication1
{
class Monster
{
virtual public void Passport()
{
Console.WriteLine("Monster {0} \t health = {1} ammo = {2}",
name, health, ammo);
}
}
class Daemon: Monster
{
override public void Passport()
{
Console.WriteLine(
"Daemon {0} \t health = {1} ammo = {2} brain = {3}", Name.Health, Ammo, brain);
}
}
class Class1
{
static void Main()
{
const int n = 3;
Monster[] stado = new Monster[n];
stado[0] = new Monster("Monia");
stado[l] = new Monster("Monk");
|
|
stado[2] = new Daemon("Dimon", 3);
foreach (Monster elem in stado)
elem.Passport();
for (int i = 0; i < n; ++i)
stado[i].Ammo = 0;
Console.WriteLine();
foreach (Monster elem in stado)
elem.Passport();
}
}
}
Результат работы программы:
Monster Monia health = 100 ammo = 100 Monster Monk health = 100 ammo - 100 Daemon Dimon health - 100 ammo = 100 brain = 3
Monster Monia health = 100 ammo = 0 Monster Monk health = 100 ammo ■ 0 Daemon Dimon health = 100 ammo = 0 brain = 3
Как видите, теперь в циклах 1 и 3 вызывается метод Passport, соответствующий типу объекта, помещенного в массив.
Виртуальные методы базового класса определяют интерфейс всей иерархии. Этот интерфейс может расширяться в потомках за счет добавления новых виртуальных методов. Переопределять виртуальный метод в каждом из потомков не обязательно: если он выполняет устраивающие потомка действия, метод наследуется.
Вызов виртуального метода выполняется так: из объекта берется адрес его таблицы VMT, из VMT выбирается адрес метода, а затем управление передается этому методу. Таким образом, при использовании виртуальных методов из всех одноименных методов иерархии всегда выбирается тот, который соответствует фактическому типу вызвавшего его объекта.