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