Ограничения параметров рефлексивного типа: X‹T›, где T : X‹T› ‒ любые более простые альтернативы?

Время от времени я усложняю простой интерфейс, добавляя к нему ограничение параметра самоссылающегося («рефлексивного») типа. Например, я мог бы превратить это:

interface ICloneable
{
    ICloneable Clone();
}

class Sheep : ICloneable
{
    ICloneable Clone() { … }
} //^^^^^^^^^^

Sheep dolly = new Sheep().Clone() as Sheep;
                                //^^^^^^^^

в:

interface ICloneable<TImpl> where TImpl : ICloneable<TImpl>
{
    TImpl Clone();
}

class Sheep : ICloneable<Sheep>
{
    Sheep Clone() { … }
} //^^^^^

Sheep dolly = new Sheep().Clone();

Основное преимущество: тип реализации (такой как Sheep) теперь может ссылаться на себя, а не на свой базовый тип, что снижает потребность в приведении типов (как показано в последней строке кода).

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

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


*) Этот шаблон кода может быть неинтуитивным и трудным для понимания, например. следующими способами:

  • Объявление X<T> where T : X<T> выглядит рекурсивным, и можно задаться вопросом, почему компилятор не застревает в бесконечном цикле, рассуждая "Если T является X<T>, то X<T> на самом деле является X<X<…<T>…>>". (Но ограничения, очевидно, не разрешаются таким образом.)

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

  • Как только вы добавите в смесь больше параметров типа и отношений подтипов между различными универсальными интерфейсами, ситуация довольно быстро станет неуправляемой.


person stakx - no longer contributing    schedule 14.01.2012    source источник
comment
Вы будете рады узнать, что это достаточно распространено, чтобы иметь название: он называется «Любопытно повторяющийся шаблон шаблона» (сокращенно CRTP).   -  person Cameron    schedule 15.01.2012
comment
... и это не имеет ничего общего с ограничениями (поскольку стандартные шаблоны C++ их вообще не имеют).   -  person Krizz    schedule 15.01.2012
comment
Есть ли причина, по которой это не просто interface ICloneable<T> { T Clone(); }?   -  person    schedule 15.01.2012
comment
@pst, да, потому что без ограничения можно было бы реализовать Sheep как class Sheep : ICloneable<Dog> { public Dog Clone() {…} }, что может быть не тем, что изначально имелось в виду с интерфейсом ICloneable.   -  person stakx - no longer contributing    schedule 15.01.2012
comment
@pst да, иначе это позволит class Cow : ICloneable<Sheep>.   -  person Krizz    schedule 15.01.2012
comment
@stakx В тот момент я просто бегал, дико размахивая руками в воздухе :)   -  person    schedule 15.01.2012
comment
связанный вопрос на прошлой неделе «почему значение спецификации базового класса не может рекурсивно зависеть от самого себя»> stackoverflow.com/questions/8766365/   -  person Tim Schmelter    schedule 15.01.2012
comment
@pst, оказывается, ты был прав! ICloneable<T> достаточно. Как показывают ответы ниже, дополнительное ограничение гарантирует, что аргумент типа реализует ICloneable<> только для некоторого типа, но не обязательно для того же самого типа.   -  person stakx - no longer contributing    schedule 15.01.2012


Ответы (2)


Основное преимущество: тип реализации теперь может ссылаться на себя, а не на свой базовый тип, что снижает потребность в приведении типов.

Хотя может показаться, что ограничение типа, ссылающееся на себя, заставляет реализующий тип делать то же самое, на самом деле это не то, что он делает. Люди используют этот шаблон, чтобы попытаться выразить шаблоны формы, когда переопределение этого метода должно возвращать тип переопределяющего класса, но на самом деле это не ограничение, выраженное или навязанное системой типов. Я привожу пример здесь:

https://ericlippert.com/2011/02/02/curiouser-and-curiouser/

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

Ага. Я стараюсь избегать этого шаблона. Трудно рассуждать.

Кто-нибудь знает другой шаблон кода C#, который достигает того же эффекта или чего-то подобного, но более простым для понимания способом?

Не в С#, нет. Вы можете подумать о системе типов Haskell, если вас это интересует; Высшие типы Haskell могут представлять такие шаблоны типов.

Объявление X<T> where T : X<T> кажется рекурсивным, и можно задаться вопросом, почему компилятор не застревает в бесконечном цикле, рассуждая так: если T является X<T>, то X<T> на самом деле является X<X<…<T>…>>.

Компилятор никогда не зацикливается, рассуждая о таких простых отношениях. Однако номинальное подтипирование универсальных типов с контравариантностью, как правило, неразрешимо. Существуют способы заставить компилятор совершать бесконечные регрессии, и компилятор C# не обнаруживает их и не предотвращает, прежде чем отправиться в бесконечное путешествие. (Пока. Я надеюсь добавить обнаружение этого в компилятор Roslyn, но посмотрим.)

См. мою статью на эту тему, если это вас интересует. Вы также захотите прочитать статью, на которую есть ссылка.

https://ericlippert.com/2008/05/07/covariance-and-contravariance-part-11-to-infinity-but-not-beyond/

person Eric Lippert    schedule 15.01.2012
comment
Спасибо за подробный ответ. Ваше сообщение в блоге попадает в самую точку. +1 также за указание на то, что компилятор С# умнее, чем коллектив Борга, когда дело доходит до явно бесконечных вещей. :) - person stakx - no longer contributing; 15.01.2012

К сожалению, нет способа полностью предотвратить это, и достаточно универсального ICloneable<T> без ограничений типа. Ваше ограничение ограничивает возможные параметры только классами, которые сами его реализуют, что не означает, что они реализуются в настоящее время.

Другими словами, если Cow реализует ICloneable<Cow>, вы все равно легко заставите Sheep реализовать ICloneable<Cow>.

Я бы просто использовал ICloneable<T> без ограничений по двум причинам:

  1. Я серьезно сомневаюсь, что вы когда-нибудь совершите ошибку, используя неправильный параметр типа.

  2. Интерфейсы должны быть контрактами для других частей кода, а не использоваться для кодирования на автопилоте. Если часть кода ожидает ICloneable<Cow>, а вы передаете Sheep, который может это сделать, с этой точки зрения это кажется совершенно правильным.

person Groo    schedule 15.01.2012
comment
+1 [interface X<T> where T:X<T>{}] ограничивает возможные параметры только классами, которые сами его реализуют, что не означает, что они реализуются в настоящее время. Очень лаконично, замечательно! - person Glenn Slayden; 27.07.2012