Почему это общее ограничение компилируется, когда оно имеет циклическую ссылку - PullRequest
6 голосов
/ 24 сентября 2010

Я написал метод расширения в csharp для помощника MVCContrib Html и был удивлен формой общего ограничения, которое, по-видимому, циклически ссылается на себя через параметр типа.

При этом метод компилируется и работает как нужно.

Я бы хотел, чтобы кто-то объяснил, почему это работает, и если существует более интуитивный, интуитивно понятный синтаксис, и если нет, если кто-нибудь знает почему?

Вот код компиляции и функции, но я удалил пример списка T, так как он затуманил проблему. , а также аналогичный метод с использованием List .

namespace MvcContrib.FluentHtml 
{
  public static class FluentHtmlElementExtensions
  {
    public static TextInput<T> ReadOnly<T>(this TextInput<T> element, bool value)
        where T: TextInput<T>
    {
        if (value)
            element.Attr("readonly", "readonly");
        else
            ((IElement)element).RemoveAttr("readonly");
        return element;
    }
  }
}

    /*analogous method for comparison*/
    public static List<T> AddNullItem<T>(this List<T> list, bool value) 
        where T : List<T>
    {
        list.Add(null);
        return list;
    }

В первом методе ограничение T: TextInput кажется практически круглым. Однако, если я закомментирую это, я получу ошибку компилятора:

"Тип 'T' нельзя использовать в качестве параметра типа 'T' в универсальном типе или методе 'MvcContrib.FluentHtml.Elements.TextInput '. Преобразование в бокс или преобразование параметров типа из 'T' в 'MvcContrib.FluentHtml.Elements.TextInput '. "

отсутствует.

и в случае списка ошибка (и):

"Наилучшее перегруженное совпадение методов для System.Collections.Generic.List.Add (T) 'содержит недопустимые аргументы Аргумент 1: невозможно преобразовать '' в 'T' "

Я мог бы представить, что более интуитивное определение будет таким, которое включает 2 типа, ссылку на общий тип и ссылку на тип ограничения, например:

public static TextInput<T> ReadOnly<T,U>(this TextInput<T> element, bool value) 
    where U: TextInput<T>

или

public static U ReadOnly<T,U>(this U element, bool value) 
    where U: TextInput<T>

но ни один из них не компилируется.

Ответы [ 5 ]

10 голосов
/ 24 сентября 2010

ОБНОВЛЕНИЕ: Этот вопрос лег в основу моей статьи в блоге от 3 февраля 2011 года .Спасибо за отличный вопрос!


Это законно, оно не круглое и довольно распространенное.Мне лично это не нравится.

Причины, по которым мне это не нравится:

1) Это слишком умно;как вы обнаружили, умный код труден для людей, незнакомых со сложностями системы типов, чтобы интуитивно понять.

2) Это не соответствует моей интуиции того, что «представляет» универсальный тип.Мне нравятся классы для представления категорий вещей и общие классы для представления параметризованных категорий.Для меня ясно, что «список строк» ​​и «список чисел» - это оба вида списков, различающиеся только типом объекта в списке.Мне гораздо менее понятно, что такое «TextInput из T, где T - это TextInput из T».Не заставляйте меня думать.

3) Этот шаблон часто используется в попытке навязать ограничение в системе типов, которое на самом деле не применяется в C #.А именно этот:

abstract class Animal<T> where T : Animal<T>
{
    public abstract void MakeFriends(IEnumerable<T> newFriends);
}
class Cat : Animal<Cat>
{
    public override void MakeFriends(IEnumerable<Cat> newFriends) { ... }
}

Идея заключается в том, что «подкласс Cat of Animal может дружить только с другими кошками».

Проблема в том, что требуемое правило на самом деле не применяется:

class Tiger: Animal<Cat>
{
    public override void MakeFriends(IEnumerable<Cat> newFriends) { ... }
}

Теперь Тигр может дружить с Кошками, но не с Тиграми.

Чтобы действительно выполнить эту работу в C #, вам нужно сделать что-то вроде:

abstract class Animal 
{
    public abstract void MakeFriends(IEnumerable<THISTYPE> newFriends);
}

, где "THISTYPE" - это волшебная новая языковая функция, которая означает, что "переопределяющий класс требуется длявведите свой собственный тип здесь ".

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

class Tiger: Animal
{
    // illegal!
    public override void MakeFriends(IEnumerable<Cat> newFriends) { ... }
}

К сожалению, это также небезопасно:

Animal animal = new Cat();
animal.MakeFriends(new Animal[] {new Tiger()});

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

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

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

То есть нам нужно, чтобы каждый T был сопоставимымдля всех остальных T, если у нас есть надежда составить их отсортированный список.

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

class C<T, U> where T : U where U : T

Интересная область теории типов (которая в настоящее время плохо обрабатывается компилятором C #) - это область некруглых, но бесконечных универсальных типов.Я написал детектор бесконечного типа, но он не попал в компилятор C # 4 и не является приоритетным для возможных гипотетических будущих версий компилятора.Если вы заинтересованы в некоторых примерах бесконечных типов или в некоторых случаях, когда детектор цикла C # портится, см. Мою статью на эту тему:

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

5 голосов
/ 24 сентября 2010

Причина заключается в том, что тип TextInput сам имеет такое ограничение, как вот.

public abstract class TextInput<T> where T: TextInput<T>{
   //...
}

Также обратите внимание, что TextInput<T> является абстрактным и единственным способом создать экземпляр такого классаявляется производным от него по типу CRTP:

public class FileUpload : TextInput<FileUpload> {
}

Метод расширения не будет компилироваться без этого ограничения, поэтому он существует.

Причина наличия CRTP в первомнеобходимо включить строго типизированные методы, включающие Fluent Interface в классе base , поэтому рассмотрим такой пример:

public abstract class TextInput<T> where T: TextInput<T>{
   public T Length(int length) {
      Attr(length); 
      return (T)this;
   }
}
public class FileUpload : TextInput<FileUpload> {
   FileUpload FileName(string fileName) {
      Attr(fileName);
      return this;
   }
}

Поэтому, когда у вас есть экземпляр FileUpload, Length возвращает экземпляр FileUpload, даже если он определен в базовом классе.Это делает возможным следующий синтаксис:

FileUpload upload = new FileUpload();
upload                      //FileUpload instance
 .Length(5)                 //FileUpload instance, defined on TextInput<T>
 .FileName("filename.txt"); //FileUpload instance, defined on FileUpload

EDIT Чтобы ответить на комментарии OP о рекурсивном наследовании классов.Это хорошо известный в C ++ шаблон, который называется Curiously Recurring Template Pattern.Прочитайте это здесь .До сегодняшнего дня я не знал, что это возможно в C #.Я подозреваю, что ограничение связано с разрешением использования этого шаблона в C #.

1 голос
/ 24 сентября 2010

То, как вы его используете, не имеет никакого смысла. Но использование универсального параметра в ограничении на этот же параметр вполне нормально, вот более очевидный пример:

class MySortedList<T> where T : IComparable<T>

Ограничение выражает тот факт, что между объектами типа T должно быть упорядочение, чтобы упорядочить их в отсортированном порядке.

РЕДАКТИРОВАТЬ: я собираюсь деконструировать ваш второй пример, где ограничение на самом деле неправильно, но помогает компилировать.

Код, о котором идет речь:

/*analogous method for comparison*/
public static List<T> AddNullItem<T>(this List<T> list, bool value) 
    where T : List<T>
{
    list.Add(null);
    return list;
}

Причина, по которой он не будет компилироваться без ограничения, заключается в том, что типы значений не могут быть null. List<T> является ссылочным типом, поэтому, принудительно where T : List<T> вы заставляете T быть ссылочным типом, который может быть нулевым. Но вы также делаете AddNullItem почти бесполезным, поскольку вы больше не можете вызывать его на List<string> и т. Д. Правильное ограничение:

/* corrected constraint so the compiler won't complain about null */
public static List<T> AddNullItem<T>(this List<T> list) 
    where T : class
{
    list.Add(null);
    return list;
}

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

Но вы даже можете удалить это ограничение, если используете default(T), который предусмотрен именно для этой цели, это означает null, когда T является ссылочным типом и все ноль для любого типа значения.

/* most generic form */
public static List<T> AddNullItem<T>(this List<T> list) 
{
    list.Add(default(T));
    return list;
}

Я подозреваю, что вашему первому методу также нужно ограничение типа T : class, но, поскольку у меня нет всех используемых вами классов, я не могу сказать наверняка.

0 голосов
/ 24 сентября 2010
public static TextInput<T> ReadOnly<T>(this TextInput<T> element, bool value)
    where T: TextInput<T>

Давайте разберемся:

TextInput<T> - это тип возвращаемого значения.

TextInput<T> - это расширяемый тип (тип первого параметра для статического метода)

ReadOnly<T> - это имя функции, которая расширяет тип, в определение которого входит T, т. Е. TextInput<T>.

where T: TextInput<T> - это ограничение на T из ReadOnly<T>, напримерчто T может использоваться в общем TextInput<TSource>.(T - это TSource!)

Я не думаю, что он круговой.

Если вы снимите ограничение, я ожидаю, что element пытается привести к обобщенному типу (не TextInput общего типа), который, очевидно, не будет работать.

0 голосов
/ 24 сентября 2010

Я могу только догадываться, что делает код, который вы опубликовали. Тем не менее, я вижу преимущество в ограничении универсального типа, таком как это. Это имело бы смысл (для меня) в любом сценарии, где вам нужен аргумент некоторого типа, который может выполнять определенные операции над аргументами того же типа.

Вот несвязанный пример:

public static IComparable<T> Max<T>(this IComparable<T> value, T other)
    where T : IComparable<T>
{
    return value.CompareTo(other) > 0 ? value : other;
}

Код, подобный этому, позволит вам написать что-то вроде:

int start = 5;
var max = start.Max(6).Max(3).Max(10).Max(8); // result: 10

Пространство имен FluentHtml - это то, что должно напомнить вам, что это является намерением кода (чтобы включить цепочку вызовов методов).

...