В прошлой статье мы увидели что для одинарной диспетчеризации, то есть выбора метода по типу времени выполнения одного аргумента, мы можем использовать виртуальные методы. Для двойной диспетчеризации мы можем использовать шаблон проектирования с неподходящим названием "Посетитель". Он работает, но имеет недостатки. Он тяжеловесный, сложный для понимания и плохо расширяется для множественной диспетчеризации.

Прошлый раз я сказал, что C# не поддерживает двойную диспетчеризацию. Это была наглая ложь! На самом деле C# поддерживает множественную диспетчеризацию. Вы можете выбрать метод на основании типа времен выполнения любого количества аргументов. Для примера диспетчеризация по двум аргументам:

abstract class Player 
{ 
  // not virtual!
  public void Attack(Monster monster)
  {
    dynamic p = this;
    dynamic m = monster;
    p.ResolveAttack(m);
  }

  public void ResolveAttack(Monster monster)
  {
    // basic case code goes here
  }
}
sealed class Warrior : Player
{
  // not virtual!
  public void ResolveAttack(Werewolf monster)
  {
    // Warrior vs Werewolf code goes here
  }
}

Это все. Теперь схватка Warrior против Werewolf вызовет нужный метод, без оверхеда визиторов.

Как это работает? Запомните базовое правило dynamic: "делай то, что сделал бы компилятор зная тип времени выполнения выражения типа dynamic". В том числе диспетчеризация вызовов. Если у нас есть код:

Player player = new Warrior();
Monster monster = new Werewolf();
player.Attack(monster);

То динамический вызов внутри player.Attack, узнает тип времени выполнения динамических переменных p и m. Далее он просто сделает то, что сделал бы компилятор зная типы:

((Warrior)p).ResolveAttack((Werewolf)m);

Очевидно это вызовет метод Warrior.ResolveAttack(Werewolf).

Аналогично, если есть код:

Player player = new Wizard();
Monster monster = new Vampire();
player.Attack(monster);

То динамический вызов будет развернут в:

((Wizard)p).ResolveAttack((Vampire)m);

он, очевидно, вызовет Player.ResolveAttack(Monster) , так как нет метода Wizard.ResolveAttack(Vampire) .

Это работает для любого количества аргументов. Если вы хотите чтобы Attack принимал Weapon или Fruit или что-то еще, просто приведите аргумент к типу dynamic и сделайте вызов. Вызов найдет нужный метод на основании типа времени выполнения.

Еще раз: для диспетчеризации по типу аргумента, аргумент должен быть приведен к типу dynamic. Если мы напишем:

  public void Attack(Monster monster)
  {
    dynamic p = this;
    p.ResolveAttack(monster); // argument is not dynamic
  }

это будет воспринято как:

((Warrior)p).ResolveAttack(monster);

и, очевидно, не будет выбран метод Warrior.ResolveAttack(Werewolf) .

Это панацея? Не совсем, такое решение мне тоже не нравится.

Во-первых не забываем, что делая динамический вызов, мы вызываем компилятор еще раз. Это очень умный компилятор, он кэширует результаты предыдущего вызова, поэтому вызов с теми же типами не вызовет компилятор еще раз. Конечно кэш без политики истечения срока действие это всего лишь другое название для утечки памяти, и неважно насколько это оптимизировано, все равно простой виртуальный вызов или визитор дешевле с точки зрения расхода памяти и времени выполнения.

Во-вторых вспомним с чего мы начали. Мы хотим представить бизнес-правила в системе типов C#, чтобы компилятор помогал нам обнаружить проблемы как можно раньше. dynamic полностью убирает такую возможность. Если будет проблема с выбором метода, то она обнаружится только во время выполнения.

В-третьих, давайте поговорим о проблемах dynamic, которые я ранее не упоминал. Алгоритм разрешения перегрузки C# имеет много интересных особенностей. Предположим, что нам нужно решить немного более сложную задачу:

class Warrior : Player
{
  public void ResolveAttack(Werewolf monster, HolyGround location)
  {
    // Warrior vs Werewolf on Holy Ground code goes here
  }
}

sealed class Paladin : Warrior
{
  public void ResolveAttack(Monster monster, HolyGround location)
  {
    // Paladin vs any Monster on Holy Ground code goes here
  }
}

Что произойдет, если Paladin нападет на Werewolf на HolyGround? С одной стороны, у нас есть правило, что более конкретный тип аргумента лучше ("лучше" или "better" - это термин из спецификации C#, где описано разрешение перегрузок - прим. пер.) . Werewolf более конкретен, чем Monster, но Paladin более конкретен, чем Warrior, так кто победит? C# говорит, что получатель - объект метод которого вызывается - особенный. Метод в более производном классе всегда выигрывает. Это может быть неожиданно.

Вы можете оказаться в ситуации когда компилятор выдал бы вам ошибку, следовательно dynamic выкинет исключение во время выполнения:

sealed class Paladin : Warrior
{
  public void ResolveAttack(Monster monster, HolyGround location)
  {
    // Paladin vs any Monster on Holy Ground code goes here
  }
  public void ResolveAttack(Werewolf monster, Location location) 
  {
    // Paladin vs Werewolf in any location code goes here
  }
}

Теперь если Paladin нападет на Werewolf на HolyGround что случится? Ни один метод не лучше другого, поэтому динамический вызов выкинет исключение.

В-четвертых, это немного злоупотребление dynamic. Мы не добавляли dynamic в C# 4 для множественно диспетчеризации, мы добавляли его для облегчения взаимодействия между C# и объектными моделями созданными для динамически типизированных языков, таких как HTML DOM (создавался для JS), Word и Excel (создавался для VB 6).

Мы начали серию с попытки представить правило "У Игрока есть Оружие, но Волшебник может использовать только Посох" и мы не смогли найти хороший способ сделать это. Выразить ограничения с помощью наследования классов - это сложно. Далее мы пытались выразить представить логику "вызывать специальные методы для специальных случаев и обычные методы для обычных случаев" и тоже не смогли продвинуться. Кажется что наши инструменты ограничивают нас. Может быть мы используем неправильные инструменты? Или мы неправильно пытаемся решать задачи? Самое время сделать шаг назад и посмотреть, не совершили ли бы фундаментальную ошибку.

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

Комментарии (0)