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

В прошлый раз мы уже избавились от экземплярных методов; мы представили их в виде статических методов, которые принимают скрытый параметр “this”. Но виртуальные методы несколько сложнее. Мы реализуем виртуальные методы, в виде полей делегата, содержащего указатели на статические методы.

 abstract class Animal
{
  public Func<Animal, string> Complain;
  public Func<Animal, string> MakeNoise;
  public static string MakeNoise(Animal _this)
  {
    return "";
  }
}

Пока все выглядит хорошо…

 class Giraffe : Animal
{
  public bool SoreThroat { get; set; }
  public static string Complain(Animal _this)
  {
    return _this.SoreThroat ? "Ужасно болит шея!" : "Никаких жалоб сегодня.";
  }
}

У нас проблема. Класс Animal не содержит свойство SoreThroat, но метод Complain не может принимать тип Giraffe, поскольку тогда он не будет совместим с типом делегата, который, в конце концов, ожидает тип Animal в качестве формального параметра “_this”.

Нам нужно реализовать шаблон виртуального метода таким образом, чтобы гарантировать, что вызывающий код никогда не передаст объект класса Cat в «виртуальный» метод, принимающий Giraffe. Давайте предположим, что мы это обеспечили. Тогда мы можем сделать следующее преобразование:

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

Все в порядке? Пока нет. Мы забыли об инициализации полей!

Здесь мы впервые столкнулись с ситуацией, при которой мы не можем что-то сделать в «C# без экземплярных методов». CLR инициализирует «поля» виртуальных методов после выделения памяти, но до вызова конструкторов. (*) В языке C# мы этого добиться не можем. Тогда давайте сделаем глупость; мы уже избавились от экземплярных методов; конструкторы экземпляров, по сути являются экземплярными методами, которые вызываются при создании объектов. Так давайте избавимся и от конструкторов экземпляров. Мы заменим конструкторы с помощью фабрики, статический метод, которой будет создавать и инициализировать объект. (Мы предполагаем, что статический метод может играть роль конструктора; например, он может устанавливать поля только для чтения и т.д.) Теперь вызов конструктора по умолчанию, всего лишь выделяет память.

 abstract class Animal
{
  public Func<Animal, string> Complain;
  public Func<Animal, string> MakeNoise;
  public static string MakeNoise(Animal _this)
  {
    return "";
  }
  // Никакой фабрики; Animal является абстрактным классом.
  public static void InitializeVirtualMethodFields(Animal animal)
  {
    animal.Complain = null; // абстрактный!
    animal.MakeNoise = Animal.MakeNoise;
  }
}
class Giraffe : Animal
{
  public bool SoreThroat { get; set; }
  public static string Complain(Animal _this)
  {
    return (_this as Giraffe).SoreThroat ? "Ужасно болит шея!" : "Никаких жалоб сегодня.";
  }
  public static void InitializeVirtualMethodFields(Giraffe giraffe)
  {
    Animal.InitializeVirtualMethodFields(giraffe);
    giraffe.Complain = Giraffe.Complain;
    // Giraffe не переопределяет MakeNoise, так что, игнорируем это поле.
  }  
  public static Giraffe Create()
  {
    // Никаких конструкторов; следующая строка просто выделяет память.
    Giraffe giraffe = new Giraffe(); 
    // Гарантируем, что поля виртуальных методов проинициализированы до вызова любого другого кода.
    Giraffe.InitializeVirtualMethodFields(giraffe);
    // Теперь выполняем остальную инициализацию, которую делает конструктор.
  }
}
class Cat : Animal
{
  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 void InitializeVirtualMethodFields(Cat cat)
  {
    Animal.InitializeVirtualMethodFields(cat);
    cat.Complain = Cat.Complain;
    cat.MakeNoise = Cat.MakeNoise;
  }  
  public static Cat Create()
  {
    Cat cat = new Cat(); 
    Cat.InitializeVirtualMethodFields(cat);
    // Теперь выполняем остальную инициализацию, которую делает конструктор.
  }
}
class Dog : Animal
{
  public bool Small { get; set; }
  public static string Complain(Animal _this)
  {
    return " Код регрессионного налога нашего штата – ... БЕЛКА!";
  }
  public static string MakeNoise(Dog _this)  // Помните, что мы забыли ключевое слово "override"
  { 
    return _this.Small ? "гав" : "ГАВ-ГАВ"; 
  }
  public static void InitializeVirtualMethodFields(Dog dog)
  {
    Animal.InitializeVirtualMethodFields(dog);
    dog.Complain = Dog.Complain;
    // Не переопределяет метод MakeNoise, так что, игнорируем это поле.
  }  
  public static Dog Create()
  {
    Dog dog = new Dog ();
    Dog.InitializeVirtualMethodFields(dog);
    // Теперь выполняем остальную инициализацию, которую делает конструктор.
  }
}

А как насчет мест вызова этого кода? Мы перепишем нашу программу следующим образом:

 string s;
Animal animal = Giraffe.Create();
// создает новый объект класса Giraffe и инициализирует поле Complain методом Giraffe.Complain,
// а поле MakeNoise инициализирует методом Animal.MakeNoise. Мы продолжаем 
// переопределять вызовы, чтобы «получатель» передавался в качестве “_this” каждому делегату:
s = animal.Complain(animal); 
// Вызываем делегат animal.Complain, который указывает на статический метод Giraffe.Complain

s = animal.MakeNoise(animal); 
// вызываем делегат animal.MakeNoise, который указывает на статический метод Animal.MakeNoise

animal = Cat.Create();
// Создаем объект класса Cat и инициализируем поля делегатов методами Cat.Complain и Cat.MakeNoise.

s = animal.Complain(animal);  // Я ВАС НЕНАВИЖУ
s = animal.MakeNoise(animal); // МЯУ!

Dog dog = Dog.Create();
// Инициализируем поля делегатов методами Dog.Complain и Animal.MakeNoise

animal = dog;
s = animal.Complain(animal); 
s = animal.MakeNoise(animal); 
// Вызывает делегат animal.MakeNoise, который указывает на статический метод Animal.MakeNoise

s = Dog.MakeNoise(dog); // гав!
// Вообще не вызывает никакого делегата; механизм определения перегрузки методов видит, что класс Dog содержит метод 
// MakeNoise, который объявлен в более производном классе, чем поле 
// делегата базового класса, и вызывает статический метод более производного класса.

Вот и все; мы успешно эмулировали виртуальные и экземплярные методы в языке, содержащем только статические методы (и делегаты к статическим методам.)

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

В следующий раз мы решим проблему неэффективного использования памяти.

-----

(*) CLR руководствуется следующими правилами: «слот» для виртуальной функции корректно инициализируется сразу же при создании объекта; в отличие от языка С++, в котором виртуальные функции изменяются в процессе конструирования объекта. До того, как я присоединился к этой команде, мне значительно больше нравился подход языка С++, как можно увидеть из моего сообщения 2005-го года, которое было написано незадолго до того, как я присоединился к команде разработчиков языка C#. У обоих подходов есть свои достоинства и недостатки; сейчас я считаю, что подход, применяемый в CLR немного лучше, но все равно, лучше не играть с огнем: просто не вызывайте виртуальные методы из конструкторов.

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