Всё интереснее и интереснее

Следующий образец вы постоянно видите в программах на языке C#:

 class Frob : IComparable<Frob>

С первого взгляда у вас может возникнуть вопрос, почему в этом коде нет «циклической» зависимости; в конце концов, следующая запись class Frob : Frob является некорректной (*). Однако, при более детальном рассмотрении, она выглядит разумно; Frob – это нечто, что можно сравнивать с Frob. На самом деле, никакой циклической зависимости здесь нет.

Это шаблон может быть обобщен следующим образом:

 class SortedList<T> where T : IComparable<T>

Опять-таки, задание ограничения для типа T в терминах T кажется немного цикличным, однако на самом деле этот код аналогичен предыдущему. Тип T ограничен типами, которые можно сравнивать с T. Тип Frob является корректным аргументом типа SortedList, поскольку один экземпляр класса Frob может быть сравнен с другим экземпляром класса Frob.

Но вот этот код действительно взрывает мой мозг:

 class Blah<T> where T : Blah<T>

Это объявление кажется цикличным (как минимум) в двух моментах. А вообще, корректен ли этот код?

Да, он корректен, и у него есть несколько законных применений. Я вижу этот пример довольно часто (**). Однако, лично мне он не нравится, и я не советую его использовать.

Это реализация на языке C# так называемого «параметризованного наследования» (Curiously Recurring Template Pattern, CRTP) применяемого в С++ и я оставляю возможность рассказать об использовании этого шаблона в языке С++ всем желающим. По сути, этот шаблон в языке C# является попыткой заставить использовать CRTP.

Так зачем вам это может понадобиться и почему я против этого?

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

 abstract class Animal 
{ 
    public virtual void MakeFriends(Animal animal); 
}

Но это значит, что класс Cat (кот) может дружить с классом Dog (собакой), а это будет проблемой вселенского масштаба! (***) Мы же хотим определить следующее:

 abstract class Animal 
{ 
    public virtual void MakeFriends(THISTYPE animal); 
}

Так, чтобы если класс Cat переопределял метод MakeFriend, он мог переопределить его только с параметром типа Cat.

Но теперь у нас появляется новая проблема, поскольку мы только что нарушили принцип подстановки Лисков. Мы теперь не можем больше вызывать метод на переменной абстрактного базового класса с уверенностью сохранения безопасности типов. Для обеспечения безопасности типов вариантность параметров должна быть контравариантной, а не ковариантной. И, более того, такой возможности нет в системе типов CLR.

Но вы можете практически добиться этого с помощью CRTP:

 abstract class Animal<T> where T : Animal<T> 
{ 
    public virtual void MakeFriends(T animal); 
}

class Cat : Animal<Cat> 
{ 
  public override void MakeFriends(Cat cat) {} 
}

Теперь мы не нарушаем принцип подстановки Лисков и гарантируем, что коты могут дружить только с котами.

Погодите-ка минутку… ,а мы правда это гарантируем?

 class EvilDog : Animal<Cat> 
{ 
  public override void MakeFriends(Cat cat) { } 
}

Мы не гарантируем того, чтобы только коты дружили с котами; злые собаки (EvilDog) тоже могут дружить с котами. Ограничение только лишь заставляет использовать аргумент класса Animal, а как вы будете использовать корректный результирующий тип, зависит только от вас. Если хотите, вы можете использовать его в качестве базового типа.

Так что, это первая причина избегать использования этого шаблона: он не гарантирует тех ограничений, о которых вы думаете. Все должны действовать согласованно и согласиться использовать CRTP так, как нужно, а не так, как его можно использовать (как в примере с классом EvilDog).

Вторая причина, по которой стоит избегать использования этого шаблона, заключается в том, что этот код вывихнет мозг любому, кто будет его читать. Когда я вижу List<Giraffe> у меня есть четкое представление о взаимоотношении части List – что означает наличие операций добавления и удаления чего-то, и части Giraffe – что означает, что эти операции будут производиться с типом Giraffe. Когда я вижу FuturesContract<T> where T : LegalPolicy, я понимаю, что этот код моделирует контракт о будущей сделке, который будет контролироваться каким-то правовым органом. Но когда я читаю: Blah<T> where T : Blah<T>, у меня нет интуитивного понимания, какие взаимоотношения предполагаются между типом Blah и конкретным типом T. Это скорее похоже на злоупотребление возможностью языка, чем моделированием концепций «бизнес-области».

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

Мой совет заключается в том, чтобы вы очень хорошо подумали, прежде чем использовали этот шаблон в языке C #; перевешивают ли преимущества для пользователя, тот умственный груз, который вы возлагаете на людей, поддерживающих этот код?


(*) Из-за досадного упущения, некоторые последние редакции спецификации языка C# не объявили, что этот код некорректен! Однако компилятор всегда считает этот код некорректным. На самом деле, компилятор иногда считает некорректным код, без циклических зависимостей.

(**) В основном в сообщениях электронной почты с вопросом: «А так правда можно?»

(***) Дикий смех!

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