Ограничения параметров рефлексивного типа: X <T>, где T: X <T>- есть ли более простые альтернативы? - PullRequest
18 голосов
/ 15 января 2012

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

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. (Ограничение в конечном итоге позаботится об этом.)

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

Ответы [ 2 ]

19 голосов
/ 15 января 2012

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

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

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

6 голосов
/ 15 января 2012

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

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

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

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

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

...