Как вызывают виртуальные методы
Методы представляют код, выполняющий некоторые действия над типом (статические методы) или экземпляром типа (нестатические). У каждого метода есть имя, сигнатура и возвращаемое значение. У типа может быть несколько методов с одним именем, но с разным числом параметров или разными возвращаемыми значениями. Можно определить и два метода с одним и тем же именем и параметрами, но с разным типом возвращаемого значения. Пожалуй, не существует ни одного языка (кроме IL), который бы использовал эту «возможность". Большинство языков требует, чтобы параметры методов отличались, игнорируя тип возвращаемого значения при определении уникальности метода.
CLR определяет, является ли нестатический метод виртуальным, путем изучения метаданных. Однако CLR не использует эту информацию при вызове метода — вместо этого она поддерживает две команды IL для вызова методов: call и callvirt. Метод, вызываемый командой IL call, зависит от типа переданной ссылки, а вызываемый командой callvirt — от типа объекта, на который указывает переданная ссылка. При компиляции исходного текста компилятор определяет, какой метод вызывается — виртуальный или нет. и генерирует соответствующую команду IL - call или callvirt. Это значит, что виртуальный метод можно вызвать как невиртуальный. Такой подход часто применяют, когда код вызывает виртуальный метод, определенный в базовом классе типа, как показано ниже:
class SomeClass
{
// ToString - это виртуальный метод, определенный в базовом классе Object.
public override String ToString()
{
// Компилятор использует команду IL 'call' для вызова
// виртуального метода ToString класса Object как невиртуального.
// Если бы компилятор использовал команду ' callvirt ' вместо 'call',
// то этот метод рекурсивно вызывал бы сам себя до переполнения стека.
return base.ToString();
}
}
Кроме того, компиляторы обычно генерируют команду call при вызове виртуального метода по ссылке на изолированный тип. Здесь применение call вместо callvirt помогает повысить скорость, так как в этом случае CLR может не проверять реальный тип объекта, на который передана ссылка. Кроме того, команда call в случае размерных типов (которые всегда являются изолированными) предотвращает их упаковку, что снижает утилизацию памяти и процессора.
Независимо от того, какая команда используется для вызова экземплярного метода — call или callvirt, все методы экземпляра всегда получают в качестве первого параметра скрытый указатель this, ссылающийся на объект, которым манипулирует метод.
Версии виртуальных методов.
В старые добрые времена за весь код приложения отвечала единственная компания. В наше время приложения чаще всего состоят из частей, созданных множеством разных компаний. Причем, сами технологии СОМ(+) и .NET стимулируют такой подход. Когда приложение состоит из множества частей, производимых и распространяемых разными производителями, то возникает масса проблем по управлению версиями.
Но при управлении версиями возникают трудности, нарушающие совместимость между ними на уровне исходного текста. Например, при добавлении и модификации членов базового типа. Рассмотрим несколько примеров, когда разработчиками компании CompA создан тип Phone:
namespace CompA
{
class Phone
{
public void Dial()
{
Console. WriteLine( "Phone. Dial");
// Выполнить действия для набора телефонного номера.
…
}
}
}
А теперь представим, что в компании CompB определили другой тип, BetterPhone, базовым типом которого является тип Phone, созданный CompA:
namespace CompanyB
{
class BetterPhone : CompA. Phone
{
public void Dial()
{
Console. WriteLine( "BetterPhone. Dial");
EstablishConnection( ) ;
base.Dial();
}
protected virtual void EstablishConnection()
{
Con sole. WriteLineC "BetterPhone. EstablishConnection");
// Выполнить действия для установки соединения.
…
}
}
}
Когда разработчики CompB пытаются скомпилировать свой код, компилятор С# выдает им предупреждение уведомляющее о том. что метод Dial, определяемый типом BetterPhone, скроет одноименный метод, определенный в Phone. В новой версии метода Dial его семантика может измениться (т. е. стать совсем иной, нежели та, которая была определена программистами CompA в исходной версии их метода Dial).
Предупреждение о таких потенциальных семантических несоответствиях – это очень полезная функция компилятора. Компилятор также подсказывает, как избавиться от этого предупреждения: нужно поставить ключевое слово new перед определением метода Dial в классе BetterPhone. Вот как выглядит исправленный класс BetterPhone:
namespace CompB
{
class BetterPhone : CompA. Phone
{
// Этот метод Dial не имеет ничего общего с одноименным методом класса Phone.
new public void Dial()
{
Console. WriteLine("BetterPhone. Dial");
EstablishConnection();
base.Dial();
}
protected virtual void EstablishConnection()
{
Console. WriteLine( "BetterPhone. EstablishConnection");
// Выполнить действия для установки соединения.
}
}
}
Теперь CompB может использовать в своем приложении тип BetterPhone. Вот примерный фрагмент кода, который могут написать разработчики CompB:
class App
{
static void Main()
{
CompB. BetterPhone phone = new CompB. BetterPhone();
phone. Dial();
}
}
При исполнении этого кода выводится такая информация:
BetterPhone.Dial
BetterPhone.EstablishConnection
Phone.Dial
Результат исполнения свидетельствует о том, что код выполняет именно те действия, которые нужны CompB. При вызове метода Dial вызывается новая версия этого метода, определенная в типе BetterPhone. Она сначала вызывает виртуальный метод EstablishConnection, а затем исходную версию метода Dial из базового типа Phone.
А теперь представим, что несколько компаний решили использовать тип Phone, созданный в CompA. Представим также, что все они сочли функцию метода Dial, устанавливающую соединение, действительно полезной. В CompA поступил ряд отзывов о работе ее типа, и теперь разработчики компании собираются усовершенствовать свой класс Phone:
namespace CompA
{
class Phone
{
public void Dial()
{
Console. WriteLine( "Phone. Dial");
EstablishConnection( ) ;
// Выполнить действия для набора телефонного номера.
}
protected virtual void EstablishConnectionf)
{
Console. WriteLine( "Phone. EstablishConnection");
// Выполнить действия для установки соединения.
}
}
}
Но теперь разработчики CompB при компиляции своего типа BetterPhone (производного от Phone, созданного в CompA), получают предупреждение: "warning CS01 14: 'BetterPhone.EstablishConnection()' hides inherited member 'Phone.EstablishCormection(). To make the current member override that implementation, add the override keyword. Otherwise, add the new keyword".
Компилятор предупреждает о том, что как Phone, так и BetterPhone предлагают метод EstablishConnection и семантика этого метода может отличаться в разных классах. В этом случае простая перекомпиляция BetterPhone больше не может гарантировать, что новая версия метода будет работать так же, как прежняя, определенная в типе Phone.
Если в CompB решат, что семантика метода EstablishConnection в этих двух типах отличается, компилятору будет указано, что «правильными» являются методы Dial и EstablishConnection, определенные в BetterPhone, и они не связаны с одноименными методами из базового типа Phone. Разработчики CompB смогут заставить компилятор выполнить нужные действия, оставив в определении метода Dial ключевое слово new и добавив его же в определение EstablishConnection:
namespace CompB
{
class BetterPhone : CompA. Phone
{
// Ключевое слово 'new' оставлено, чтобы указать,
// что этот метод не связан с методом Dial из базового типа.
new public void Dial()
{
Console. WriteLinet "BetterPhone. Dial");
EstablishConnection();
base.Dial();
}
// Здесь добавлено ключевое слово 'new', чтобы указать, что этот
// метод не связан с методом EstablishConnection из базового типа.
new protected virtual void EstablishConnection ()
{
Console. WriteLine("BetterPhone. EstablishConnection");
// Выполнить действия для установки соединения.
}
}
}
Здесь ключевое слово new приказывает компилятору генерировать соответствующие метаданные, разъясняющие CLR, что определенные в BetterPhone методы Dial и EstablishConnection следует рассматривать как новые функции, введенные в использование этим типом. При этом CLR будет знать, что одноименные методы типов Phone и BetterPhone никак не связаны.
Примечание. Без ключевого слова new разработчики типа BetterPhone не смогут использовать в нем имена методов Dial и EstablishConnection. Если изменить имена этих методов, то негативный эффект этих изменений скорее всего затронет всю программную основу, нарушая совместимость на уровне исходного текста и двоичного кода. Обычно такого рода изменения с далеко идущими последствиями нежелательны, особенно в средних и крупных проектах. Но если изменение имени метода приведет лишь к ограниченному обновлению исходного текста, то следует пойти на это, чтобы одинаковые имена методов Dial и EstablishConnection, обладающих разной семантикой в разных типах, не вводили в заблуждение других разработчиков.
При исполнении того же приложения (метода Main) выводится информация:
BetterPhone.Dial
BetterPhone.EstablishConnection
Phone.Dial
Phone.EstablishConnection
Из выходной информации видно, что когда Main вызывает новый метод Dial, вызывается версия Dial, определенная в BetterPhone. Далее Dial вызывает виртуальный метод EstablishConnection, также определенный в BetterPhone. Когда метод EstablishConnection из типа BetterPhone возвращает управление, вызывается метод Dial из Phone, вызывающий EstablishConnection из этого типа. Но поскольку метод EstablishConnection в типе BetterPhone помечен ключевым словом new, вызов этого метода не считается переопределением виртуального метода EstablishConnection, исходно определенного в типе Phone. В результате метод Dial из типа Phone вызывает метод EstablishConnection, определенный в типе Phone, что и требовалось от программы.
Альтернативный вариант таков: CompB, получив от CompA новую версию типа Phone, может решить, что текущая семантика методов Dial и EstablishConnection из типа Phone — это именно то, что они искали. В этом случае в CompB полностью удаляют метод Dial из своего типа BetterPhone. Кроме того, поскольку теперь разработчикам CompB нужно указать компилятору, что метод EstablishConnection из типа BetterPhone связан с одноименным методом из типа Phone, то нужно уалить из его определения ключевое слово new. Но простого удаления ключевого слова здесь будет недостаточно, так как компилятору неизвестно предназначение метода EstablishConnection из BetterPhone. Чтобы выразить свои намерения явно, разработчик из CompB должен, помимо прочего, изменить модификатор метода EstablishConnection, определенного в типе BetterPhone, с virtual на override.
Вот код новой версии BetterPhone:
namespace CompB
{
class BetterPhone : CompA.Phone
{
// Метод Dial удален (так как он наследуется от базового типа).
// Здесь ключевое слово 'new' удалено, а модификатор 'virtual' изменен
// на 'override', чтобы указать, что этот метод связан с методом
// EstablishConnection из базового метода.
protected override void EstablishConnection()
{
Console.WriteLine("BetterPhone.EstablishConnection");
// Выполнить действия для установки соединения.
}
}
}
Теперь при исполнении того же приложения (метода Main) выводится:
Phone.Dial
BetterPhone.EstablishConnection
Видно, что когда Main вызывает метод Dial, вызывается версия этого метода, определенная в типе Phone и унаследованная от него типом BetterPhone. Далее, когда метод Dial, определенный в типе Phone, вызывает виртуальный метод EstablishConnection, вызывается одноименный метод из типа BetterPhone, так как он переопределяет виртуальный метод EstablishConnection, определяемый типом Phone.