为什么这个泛型约束在它似乎有一个循环引用时编译

时间:2010-09-23 23:59:45

标签: c# generics constraints

我在csharp中为MVCContrib Html助手编写了一个扩展方法,并对通用约束的形式感到惊讶,从表面看它通过类型参数循环引用自身。

据说这种方法可以根据需要进行编译和工作。

我希望有人解释为什么这样有效,如果存在更直观的直观语法,如果有人知道为什么会这样做?

这是编译和功能代码,但我删除了T的例子,因为它使问题蒙上阴影。 以及使用List< T>。

的类似方法
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&lt; T&gt;似乎所有意图和目的,循环。但是,如果我发表评论,我会收到编译错误:

“类型'T'不能用作泛型类型或方法'MvcContrib.FluentHtml.Elements.TextInput&lt; T&gt;'中的类型参数'T'。 从'T'到'MvcContrib.FluentHtml.Elements.TextInput&lt; T&gt;'没有装箱转换或类型参数转换。“

<击> 并且在列表&lt; T&gt;中如果错误是:

“'System.Collections.Generic.List.Add(T)'的最佳重载方法匹配'有一些无效的参数 参数1:无法从'&lt; null&gt;'转换到'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 个答案:

答案 0 :(得分:10)

更新:这个问题是我blog article on the 3rd of February 2011的基础。谢谢你提出的好问题!


这是合法的,不是循环的,而且很常见。我个人不喜欢它。

我不喜欢它的原因是:

1)过于聪明;正如您所发现的那样,对于不熟悉类型系统的复杂性的人来说,聪明的代码很难直观地理解。

2)它没有很好地映射到我对通用类型“代表”的直觉。我喜欢用于表示事物类别的类,以及用于表示参数化类别的泛型类。我很清楚,“字符串列表”和“数字列表”都是各种列表,仅在列表中的事物类型上有所不同。我不太清楚“T的TextInput,其中T是T的TextInput”是什么。不要让我思考。

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) { ... }
}

这里的想法是“动物猫猫只能与其他猫交朋友。”

问题是实际上没有实施所需的规则:

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#编译器处理不当)是非循环但 infinitary 泛型类型的领域。我编写了一个无限型检测器,但它没有进入C#4编译器,并且对于可能的假设未来版本的编译器而言并不是高优先级。如果您对某些无限类型的示例感兴趣,或者对C#循环检测器搞砸的一些示例感兴趣,请参阅我的文章:

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

答案 1 :(得分:5)

原因限制是因为TextInput类型本身就有这样的约束。

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

另请注意TextInput<T>是抽象的,制作此类实例的唯一方法是以类似CRTP的方式从中派生出来:

public class FileUpload : TextInput<FileUpload> {
}

如果没有该约束,扩展方法将无法编译,这就是它存在的原因。

首先使用CRTP的原因是在基础类上启用强类型方法启用Fluent Interface,因此请考虑以下示例:

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

编辑解决OP关于递归类继承的评论。这是一个众所周知的C ++模式,叫做Curiously Recurring Template Pattern。读一读here。到目前为止,我不知道它在C#中是否可行。我怀疑约束与在C#中使用这种模式有关。

答案 2 :(得分:1)

你使用它的方式毫无意义。但是在相同参数的约束中使用泛型参数是很正常的,这是一个更明显的例子:

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;
}

没有约束就不能编译的原因是值类型不能是nullList<T>是一种引用类型,因此强制where T : List<T>强制T成为可以为null的引用类型。但是你也使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而对于任何引用都是0价值类型。

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

我怀疑你的第一个方法也需要像T : class那样的约束,但由于我没有你正在使用的所有类,我无法肯定地说。

答案 3 :(得分:0)

我只能猜测你发布的代码是什么。也就是说,我可以看到像这样的泛型类型约束的优点。在任何你想要某种类型的参数可以对相同类型的参数执行某些操作的任何情况下(对我而言)都是有意义的。

这是一个不相关的例子:

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应该让你知道这是代码的意图(启用方法调用的链接)。

答案 4 :(得分:0)

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>ReadOnly<T>对T的约束,因此T可用于通用TextInput<TSource>。 (T是TSource!)

我不认为这是循环的。

如果你取出约束,我希望element试图被转换为泛型类型(不是泛型类型的TextInput),这显然不会起作用。