Виртуальные методы и их переопределение.
Как же сделать так, чтобы, обращаясь к объекту родительского класса, вызывать методы его производных классов. Оказывается это возможно и язык С# предлагает для этого использовать понятие виртуального метода. Дело в том, что при переопределении обычного метода, создается новая реализация этого метода в дочернем классе, которая полностью скрывает реализацию одноименного метода, унаследованного от родительского класса. При вызове такого метода вызывается та его версия, которая соответствует типу (классу) того объекта, к которому происходит обращение. Поэтому, если требуется вызвать конкретную версию переопределенного метода, то необходимо обязательно указать тип ссылки на объект. Если же в базовом классе объявить метод виртуальным, то, при его переопределении в классах-потомках, старая ссылка на метод (то есть адрес области памяти в которой размещается реализация виртуального метода) в базовом классе заменяется (подменяется, переписывается) на новое значение. В результате, при обращении к виртуальному методу объекта базового класса будет вызываться самая последняя его реализация в классах-потомках. Это позволяет вызывать у объекта именно те методы, реализация которых зависит не от типа ссылки на этот объект, а от его реального типа. Поскольку конкретная версия вызываемого метода объекта определяется уже не на этапе компиляции, а на этапе выполнения программы, то такой вид переопределения носит название позднего связывания.
Виртуальным называется такой метод, который объявляется как virtual в базовом классе. Так же как и обычный метод, виртуальный метод может быть переопределен в одном или нескольких производных классах. Следовательно, у каждого производного класса может быть свой вариант виртуального метода. Кроме того, виртуальные методы интересны тем, что именно происходит при их вызове по ссылке на базовый класс. В этом случае средствами языка С# определяется именно тот вариант виртуального метода, который следует вызвать, исходя из реального типа объекта, к которому происходит обращение по ссылке, причем это делается во время выполнения. Поэтому при ссылке на разные типы объектов выполняются разные варианты виртуального метода. Иными словами, как уже было сказано, вариант выполняемого виртуального метода выбирается по типу объекта, а не по типу ссылки на этот объект. Так, если базовый объект содержит виртуальный метод и от него получены производные классы, то при обращении к разным типам объектов по ссылке на базовый класс выполняются разные варианты этого виртуального метода.
Метод объявляется как виртуальный в базовом классе с помощью ключевого слова virtual, указываемого перед его именем. Когда же виртуальный метод переопределяется в производном классе, то для этого используется модификатор override. А сам процесс повторного определения виртуального метода в производном классе называется переопределением виртуального метода. При переопределении метода имя, возвращаемый тип и сигнатура переопределяющего метода должны быть точно такими же, как и у того виртуального метода, который переопределяется. Кроме того, виртуальный метод не может быть объявлен как static или abstract.
Рассмотрим это на конкретном примере. Пусть у нас имеется три класса:
class A
{
…
public virtual void Draw(){ /*Рисование окружности*/ }
}
class B : A
{
…
public override virtual void Draw(){ /*Рисование прямоугольника*/ }
}
Class C : B
{
…
public override virtual void Draw(){ /*Рисование прямоугольника c текстом*/ }
}
List<А> figures;
figures.Add(new A);
figures.Add(new B);
figures.Add(new C);
foreach(A figure in figures) figure.Draw();
В результате, получим изображение, представленное на рис.3.
Рис.3. Эффект переопределения виртуальных методов.
Теперь, если полученный результат сравнить с результатами, приведенными в предыдущем разделе при использовании механизма переопределения обычных методов, можно легко оценить все преимущества использования виртуальных методов.
Переопределение виртуального метода служит основанием для воплощения одного из самых эффективных в С# принципов: динамического связывания методов, которая представляет собой механизм разрешения вызова во время выполнения, а не во время компиляции. Значение динамического связывания методов состоит в том, что именно благодаря ему в С# реализуется динамический полиморфизм.
Переопределять виртуальный метод совсем не обязательно. Ведь если в производном классе не предоставляется собственный вариант виртуального метода, то используется его унаследованный вариант из базового класса. То есть, если в классах, находящиеся в отношении наследования, виртуальный метод не переопределяется в производном классе, то выполняется ближайший его вариант, обнаруживаемый при продвижении вверх по иерархической цепочке наследования.
Преимущества, получаемые от переопределения виртуальных методов, особенно очевидны при необходимости обрабатывать объекты различных производных классов, состоящих в отношении наследования. В особенности, когда они размещаются в массивах или контейнерах различного типа.