Реализация шаблона «виртуальный метод». Часть 3

В прошлый раз мы увидели, как можно эмулировать виртуальные методы в языке, содержащем только статические методы. Это достигается путем создания полей с делегатами и последующим выбором, какие именно указатели на методы будут содержаться в этих полях. Однако то решение не было эффективным с точки зрения использования памяти. Предположим, что класс Animal содержит сотню виртуальных методов, а не два. Это значит, что каждый наследник класса Animal содержит сотню полей, большая часть из которых всегда остаются неизменными. Вы создаете три сотни объектов класса Giraffe, и каждый из них будет содержать абсолютно одинаковые делегаты в сотне этих полей. Это кажется избыточным и расточительным.

На самом деле CLR объединяет такие виртуальные функции в таблицу диспетчеризации (dispatch table) или, для краткости, «vtable». vtable представляет собой коллекцию делегатов; цель этой таблицы ответить на следующий вопрос: «если виртуальный метод вызван для объекта определенного типа времени выполнения, какой метод должен быть вызван?»

 sealed class VTable
{
  public readonly Func<Animal, string> Complain;
  public readonly Func<Animal, string> MakeNoise;
  public VTable(Func<Animal, string> complain, Func<Animal, string> makeNoise)
  {
    this.Complain = complain;
    this.MakeNoise = makeNoise;
  }
}

Предположим, мы хотим создать таблицу виртуальных функций для классов Giraffe, Cat и Dog для ответа на вопрос: «если эти методы будут вызваны для объекта с типом времени компиляции Animal, но тип времени выполнения будет Giraffe/Cat/Dog, какие методы должны вызваться?» Все очень просто:

 static VTable GiraffeVTable = new VTable(Giraffe.Complain, Animal.MakeNoise); 
static VTable CatVTable = new VTable(Cat.Complain, Cat.MakeNoise); 
static VTable DogVTable = new VTable(Dog.Complain, Animal.MakeNoise);

И теперь нам достаточно, чтобы каждый класс содержал ссылку на соответствующую таблицу виртуальных функций. Мы снова перепишем наши классы следующим образом:

 abstract class Animal
{
  public VTable VTable; 
  public static string MakeNoise(Animal _this)
  {
    return "";
  }
  // Никакой фабрики, класс Animal абстрактный.
}
class Giraffe : Animal
{
  private static VTable GiraffeVTable = new VTable(Giraffe.Complain, Animal.MakeNoise); 
  public bool SoreThroat { get; set; }
  public static string Complain(Animal _this)
  {
    return (_this as Giraffe).SoreThroat ? "Ужасно болит шея!" : "Никаких жалоб сегодня.";
  }
  public static Giraffe Construct()
  {
    // Больше нет никаких экземплярных конструкторов; они предназначены только для выделения памяти.
    Giraffe giraffe = new Giraffe(); 
    // Гарантирует, что поля виртуальных методов инициализируются до выполнения любого другого кода.
    giraffe.VTable = Giraffe.VTable;
    // Теперь выполняем остальную инициализацию, которую должен выполнить конструктор.
  }
}
class Cat : Animal
{
  private static VTable CatVTable = new VTable(Cat.Complain, Cat.MakeNoise); 
  public bool Hungry { get; set; }
  public static string Complain(Animal _this)
  {
    return (_this as Cat).Hungry ? "ДАЙТЕ ТУНЦА!" : "Я ВАС ВСЕХ НЕНАВИЖУ!";
  }
  public static string MakeNoise(Animal _this)
  { 
    return " МЯУ МЯУ МЯУ МЯУ МЯУ МЯУ МЯУ МЯУ МЯУ";
  }
  public static Cat Construct()
  {
    Cat cat = new Cat(); 
    cat.VTable = Cat.VTable;
    // Теперь выполняем остальную инициализацию, которую должен выполнить конструктор.
  }
}
class Dog : Animal
{
  private static VTable DogVTable = new VTable(Dog.Complain, Animal.MakeNoise); 
  public bool Small { get; set; }
  public static string Complain(Animal _this)
  {
    return " Код регрессионного налога нашего штата – ... БЕЛКА!";
  }
  public static string MakeNoise(Dog _this)  // Помните, что мы забыли ключевое слово "override"
  { 
    return _this.Small ? "гав" : "ГАВ-ГАВ"; 
  }
  public static Dog Construct()
  {
    Dog dog = new Dog();
    dog.VTable = Dog.VTable;
    // Теперь выполняем остальную инициализацию, которую должен выполнить конструктор.
  }
}

И мы снова должны изменить способ вызовов:

 string s;
Animal animal = Giraffe.Create();
s = animal.VTable.Complain(animal); 
s = animal.VTable.MakeNoise(animal); 
animal = Cat.Create();
s = animal.VTable.Complain(animal); 
s = animal.VTable.MakeNoise(animal);
Dog dog = Dog.Create();
animal = dog;
s = animal.VTable.Complain(); 
s = animal.VTable.MakeNoise(); 
s = Dog.MakeNoise(dog);

Вот основы того, как реализован шаблон «виртуальный метод» в языке C#. Компилятор языка C# анализирует каждый абстрактный, виртуальный или переопределенный (override) метод, для определения какому «слоту таблицы виртуальных функций» («vtable slot») он относится; затем он генерирует метаданные, в которых содержится достаточно информации для правильного определения метода во время выполнения. Для каждого вызова, компилятор C# определяет, нужно ли вызывать экземплярный метод напрямую или выполнить косвенный вызов через таблицу виртуальных функций путем генерации соответствующего кода. После создания объекта во время выполнения CLR получает информацию из метаданных для определения того, к какой таблице виртуальных функций необходимо обратиться для каждого объекта. На самом деле vtable, конечно же, не является классом, а ее поля не являются делегатами; в CLR существуют более изящные внутренние способы представления указателя на метод. Но идея остается именно такой.

Конечно же, здесь я опустил множество деталей. Например, нельзя вызывать виртуальный или экземплярный метод по нулевой ссылке, но можно вызвать статический метод с первым параметром равным null. По-хорошему, при переходе от экземплярных или виртуальных методов к статическим, я должен был еще добавить проверки на null. Кроме того, я не рассказал о том, как работают вызовы методов базового класса (хотя вы, скорее всего, по аналогии сами сможете с этим разобраться). И я не рассказал о том, как работает этот механизм для упакованных значимых типов, которые переопределяют виртуальные методы класса object, хотя, опять-таки, думаю, вы можете с этим разобраться сами. Я не обратил внимания на проблемы безопасности и доступности. Я не рассказал о том, что происходит, когда класс создает «новый виртуальный» метод. И я не рассказал о том, как работают интерфейсы; CLR использует специальные подходы для решения проблемы переопределения методов интерфейса, о которых я, возможно, расскажу позднее.

Реальное преимущество от реализации шаблона «виртуальных и экземплярных методов» в языке программирования заключается в том, что вы избавляетесь от ненужных проблем объявления всех этих полей и самостоятельной инициализации таблицы виртуальных функций. Команды разработчиков компилятора и CLR решили эти проблемы за вас.

Кроме того, вы получаете безопасность! Реализованный здесь шаблон подразумевает, что все тщательно ему следуют. Например, что мешает вам вызвать Giraffe.VTable.Complain(dog)? Ничего. Компилятор C# гарантирует, что вы не сможете вызвать виртуальный метод, в котором “this” не будет соответствовать данной функции, однако в нашей реализации виртуальных методов эта ошибка не будет отловлена до момента выполнения, если вообще будет отловлена.

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