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

Sim*_*sco 6 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;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

    /*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;
    }
Run Code Online (Sandbox Code Playgroud)

在第一种方法中,约束T:TextInput <T>似乎是所有意图和目的,循环.但是,如果我发表评论,我会收到编译错误:

"类型'T'不能在泛型类型或方法'MvcContrib.FluentHtml.Elements.TextInput <T>'中用作类型参数'T'.没有从'T'到'MvcContrib的装箱转换或类型参数转换.FluentHtml.Elements.TextInput <T>"".

在List <T>的情况下,错误是:

"'System.Collections.Generic.List.Add(T)'的最佳重载方法匹配有一些无效的参数参数1:无法从'<null>'转换为'T'"

我可以想象一个更直观的定义是包含2种类型的定义,对泛型类型的引用和对约束类型的引用,例如:

public static TextInput<T> ReadOnly<T,U>(this TextInput<T> element, bool value) 
    where U: TextInput<T>
Run Code Online (Sandbox Code Playgroud)

要么

public static U ReadOnly<T,U>(this U element, bool value) 
    where U: TextInput<T>
Run Code Online (Sandbox Code Playgroud)

但这些都没有编译.

Eri*_*ert 10

更新:这个问题是我2011年2月3日博客文章的基础.谢谢你这个好问题!


这是合法的,它不是循环的,而且相当普遍.我个人不喜欢它.

我不喜欢它的原因是:

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) { ... }
}
Run Code Online (Sandbox Code Playgroud)

这里的想法是"动物猫只能与其他猫交朋友".

问题是实际上没有强制执行所需的规则:

class Tiger: Animal<Cat>
{
    public override void MakeFriends(IEnumerable<Cat> newFriends) { ... }
}
Run Code Online (Sandbox Code Playgroud)

现在老虎可以和猫交朋友,但不能和老虎交朋友.

要在C#中实际完成这项工作,您需要执行以下操作:

abstract class Animal 
{
    public abstract void MakeFriends(IEnumerable<THISTYPE> newFriends);
}
Run Code Online (Sandbox Code Playgroud)

其中"THISTYPE"是一种神奇的新语言特征,意思是"在这里填写自己的类型需要一个重写的类".

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

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

不幸的是,这也不是类型安全的:

Animal animal = new Cat();
animal.MakeFriends(new Animal[] {new Tiger()});
Run Code Online (Sandbox Code Playgroud)

如果规则是"动物可以与任何一种类型交朋友",那么动物可以与动物交朋友.但猫只能与猫交朋友,而不是老虎!在参数位置上的东西一定是有效contravariantly ; 在这个假设的情况下,我们需要协方差,这是行不通的.

我似乎有些偏离.回到这个奇怪的反复出现的模式的主题:我尝试只将这种模式用于常见的,容易理解的情况,如其他答案中提到的情况:

class SortedList<T> where T : IComparable<T>
Run Code Online (Sandbox Code Playgroud)

也就是说,如果我们有希望制作它们的排序列表,我们需要每个T都与其他T相当.

要实际被标记为循环,必须在依赖关系中有一个真正的循环:

class C<T, U> where T : U where U : T
Run Code Online (Sandbox Code Playgroud)

类型理论的一个有趣的领域(目前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


Igo*_*aka 5

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

public abstract class TextInput<T> where T: TextInput<T>{
   //...
}
Run Code Online (Sandbox Code Playgroud)

另请注意,它TextInput<T>是抽象的,并且创建此类的实例的唯一方法是以类似CRTP的方式从它派生:

public class FileUpload : TextInput<FileUpload> {
}
Run Code Online (Sandbox Code Playgroud)

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

首先使用CRTP的原因是为了在类上启用强类型方法启用Fluent接口,所以请考虑这样的示例:

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;
   }
}
Run Code Online (Sandbox Code Playgroud)

所以当你有一个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
Run Code Online (Sandbox Code Playgroud)

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