Реализация шаблона «виртуальный метод». Часть 1
Если вы занимаетесь программированием достаточно долгое время, то наверняка встречали много литературы по «шаблонам проектирования», – вы знаете, что это стандартные решения распространенных задач, такие как «фабрика», «наблюдатель», «одноэлементное множество», «итератор», «композит», «адаптер», «декоратор» и т. п. Зачастую полезно воспользоваться навыками анализа и проектирования других людей, которые потратили значительные усилия, раздумывая над записью шаблонов, предназначенных для решения распространенных задач. Однако я думаю очень полезно понимать, что абсолютно всё в высокоуровневом программирования является шаблоном проектирования. Некоторые из этих шаблонов настолько хороши, что мы встроили их напрямую в язык программирования настолько тщательно, что уже не рассматриваем их как шаблоны; к ним относятся шаблоны «тип», «функция», «локальная переменная», «стек вызовов» и «наследование».
У меня недавно спросили о том, как «внутри» работают виртуальные методы: как CLR определяет во время выполнения, метод какого класса наследника вызвать при вызове метода на переменной базового класса? Очевидно, что должно быть что-то, принимающее такое решение, но как это делается эффективно? Я подумал, что смогу ответить на этот вопрос, размышляя о том, как бы вы реализовали шаблон «виртуальный и экземплярный метод» в языке, в котором нет виртуальных и экземплярных методов. Итак, до конца этого цикла статей, я выбрасываю экземплярные и виртуальные методы из языка C#. Я оставляю делегаты, но они могут содержать только статические методы. Наша цель, взять программу на обычном языке C# и посмотреть на то, как она может быть преобразована в язык C# без виртуальных методов. По ходу дела мы также увидим, как на самом деле работают виртуальные методы.
Давайте начнем с набора классов с различными поведениями:
abstract class Animal
{
public abstract string Complain();
public virtual string MakeNoise()
{
return "";
}
}
class Giraffe : Animal
{
public bool SoreThroat { get; set; }
public override string Complain()
{
return SoreThroat ? "Ужасно болит шея!" : "Никаких жалоб сегодня.";
}
}
class Cat : Animal
{
public bool Hungry { get; set; }
public override string Complain()
{
return Hungry ? "ДАЙТЕ ТУНЦА!" : "Я ВАС ВСЕХ НЕНАВИЖУ!";
}
public override string MakeNoise()
{
return "МЯУ МЯУ МЯУ МЯУ МЯУ МЯУ МЯУ МЯУ МЯУ";
}
}
class Dog : Animal
{
public bool Small { get; set; }
public override string Complain()
{
return "Код регрессионного налога нашего штата – ... БЕЛКА!";
}
public string MakeNoise() // мы забыли добавить "override"!
{
return Small ? "гав" : "ГАВ-ГАВ";
}
}
Каждый, кто провел хотя бы пять минут в одной комнате с моим котом и банкой с тунцом, поймет, чем навеяна предыдущая программа.
Хорошо, у нас есть абстрактные методы, виртуальные методы в базовом классе, классы, которые переопределяют и не переопределяют множество виртуальных методов, и один (случайный) экземплярный метод. У нас может быть следующий фрагмент кода:
string s;
Animal animal = new Giraffe();
s = animal.Complain(); // никаких жалоб
s = animal.MakeNoise(); // ни звука
animal = new Cat();
s = animal.Complain(); // Я вас ненавижу
s = animal.MakeNoise(); // Мяу!
Dog dog = new Dog();
animal = dog;
s = animal.Complain(); // белка!
s = animal.MakeNoise(); // ни звука
s = dog.MakeNoise(); // гав!
Что здесь должно произойти? Два интересных момента. Во-первых, при вызове методов Complain или MakeNoise на переменной типа Animal, должен быть вызван метод на основе типа получателя времени выполнения. Во-вторых, при вызове метода MakeNoise для собаки, мы каким-то образом должны выполнить одно, если типом времени компиляции является Dog и другое, если типом времени компиляции является Animal , а типом времени выполнения является Dog .
Как мы этого добьемся с языком без виртуальных или экземплярных методов? Помните, что все методы должны быть статическими.
Давайте вначале рассмотрим невиртуальный экземплярный метод. Все очень просто. Вызываемый код должен быть таким:
public static string MakeNoise(Dog _this)
{
return _this.Small ? "гав" : "ГАВ-ГАВ";
}
А вызывающий код таким:
s = Dog.MakeNoise(dog); // гав!
Шаблон «экземплярный метод» очень прост: экземплярный метод – это всего лишь статический метод, принимающий невидимый параметр “this”. Достаточно всегда следовать шаблону и передавать первый параметр с именем “_this” текущего типа и все.
Виртуальные методы несколько сложнее. Нужно каким-то образом определить во время выполнения, какой метод вызывать.
Предполагая, что у нас нет виртуальных методов (мы также можем предположить, что у нас нет и виртуальных свойств; поскольку это всего лишь виртуальные методы в хитрой упаковке.)
Однако у нас есть поля с типами делегатов. А что если мы сделаем следующее:
(1) преобразуем все виртуальные и переопределенные методы в статические методы, которые принимают “this” типа Animal и
(2) создадим поля делегатов для этих методов и тогда
(3) преобразуем вызов методов в вызов делегатов
? Если мы это сделаем, тогда мы сможем выбирать, какие делегаты поместить в поля какого экземпляра и, таким образом, контролировать, какой метод вызывать.
В следующий раз мы попробуем это сделать.