Реализация шаблона «виртуальный метод». Часть 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) преобразуем вызов методов в вызов делегатов

? Если мы это сделаем, тогда мы сможем выбирать, какие делегаты поместить в поля какого экземпляра и, таким образом, контролировать, какой метод вызывать.

В следующий раз мы попробуем это сделать.

Оригинал статьи