Подтвердить что ты не робот

Ограничения параметра рефлексивного типа: 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) теперь может ссылаться на себя, а не на его базовый тип, что уменьшает необходимость в литье типов (как показано в последней строке кода).

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

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


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

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

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

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

4b9b3361

Ответ 1

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

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

http://blogs.msdn.com/b/ericlippert/archive/2011/02/03/curiouser-and-curiouser.aspx

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

Угу. Я стараюсь избегать этой картины. Трудно рассуждать.

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

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

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

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

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

http://blogs.msdn.com/b/ericlippert/archive/2008/05/07/covariance-and-contravariance-part-twelve-to-infinity-but-not-beyond.aspx

Ответ 2

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

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

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

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

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