2012-01-14 4 views
18

Каждый раз, когда я делаю простой интерфейс более сложным, добавляя к нему привязку к параметру self-referencing («reflexive»). Например, я мог бы превратить это:Ограничения параметра рефлексивного типа: 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# код шаблона, который достигает того же эффекта или что-то подобное, но в более легком для хватки моды?


*) Этот шаблон кода может быть неинтуитивными и трудно понять, например, в этих отношениях: «Если T является X<T>, то X<T> действительно является X<X<…<T>…>>»

  • Объявление X<T> where T : X<T> появляется рекурсивным, и можно было бы задаться вопросом, почему компилятор doesn't get stuck in an infinite loop, рассуждения, (Но ограничения явно не разрешаются, как это.)

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

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

+3

Вы будете рады узнать, что это достаточно распространено, чтобы иметь имя: оно называется «Любопытно повторяющийся шаблон шаблона» (CRTP для краткости). – Cameron

+1

... и это не имеет ничего общего с ограничениями (в качестве стандартных шаблонов на C++ их вообще нет). – Krizz

+0

Есть ли причина, по которой это не просто «интерфейс ICloneable {T Clone(); } '? –

ответ

19

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

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

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

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

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

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

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

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

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

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

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

+0

Спасибо за подробный ответ. Ваше сообщение в блоге попадает в гвоздь на голове. +1 также для указания, что компилятор C# умнее, чем коллектив Borg, когда дело доходит до, по-видимому, бесконечных штучек. :) – stakx

6

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

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

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

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

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

+1

+1 "[' interface X где T: X {} '] ограничивает только возможные параметры для классов, которые сами реализуют его, что не означает, что они в настоящее время реализуются». Очень лаконично, выдающийся! –