2010-09-23 79 views
6

我已经写了一个MVCContrib Html助手csharp的扩展方法,并且对通用约束的形式感到惊讶,它的表面似乎是循环的通过类型参数引用它自己。为什么这个泛型约束似乎有一个循环引用编译

这就是说,该方法编译和按需要工作。

我很想有人解释为什么这个工作,如果存在更直观的直观语法,如果没有人知道为什么?

这里是编译和功能代码,但我已经删除了T列表的例子,因为它阴云密布的问题。 以及使用列表<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 <Ť>似乎所有的意图和目的,是圆形的。但是如果我把它注释掉我得到一个编译错误:

“类型‘T’不能在泛型类型或方法作为类型参数‘T’‘MvcContrib.FluentHtml.Elements.TextInput <牛逼>’ 。 没有从'T'到'MvcContrib.FluentHtml.Elements.TextInput <T>'的装箱转换或类型参数转换。“

和在列表<Ť>情况下的误差(或多个):

“的最好重载方法匹配 '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> 

但既不这些编译的。

+0

如已回答已经这不是圆形的,但作为一个侧面说明它可以创建循环继承,有时可以编译,有时不会(例如添加,删除或重命名文件和文件夹可能导致编译随机成功或失败)。所以循环继承的错误确实存在。 (VS2010) – AnorZaken 2015-02-24 02:32:46

回答

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编译器中,对于可能假设的编译器未来版本并不是高优先级。如果你有兴趣infinitary类型的一些例子,或者其中的C#循环检测弄乱了一些示例,请参阅我的文章:

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

+0

感谢埃里克,这是一个有趣的领域。我还有其他一些关于约束的问题,但我想我应该将它们作为另一个问题发布:-)。 – 2010-09-26 21:48:10

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成为可以为空的引用类型。但你也让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),设置出于这样的目的,你甚至可以删除该约束,这意味着nullT是引用类型和任何类型的值都为零。

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

我怀疑你的第一种方法也需要像T : class约束,但因为我不知道你在使用的所有类我不能肯定地说。

+0

我同意没有道理,但它编译并做我想要的。你的例子太简单了,无法捕获用例。 – 2010-09-24 00:17:57

+0

对不起,我打开输入,我的意思是继续......我在想一个列表<T>其中我可以有一个香蕉列表,并有一个扩展方法,如list.AddNullItem(),所以泛型参数,本身是通用的。 – 2010-09-24 00:25:55

+0

我希望我添加的附加信息将帮助您了解为什么它没有约束没有编译,但约束不一定是正确的。 – 2010-09-24 00:28:28

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是我应该有点提示你这是打算的代码(以启用方法调用的链接)。

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++中一个众所周知的模式,叫做奇怪的循环模板模式。阅读它here。直到今天,我不知道它可能在C#中。我怀疑这个约束与在C#中使用这种模式有关。

+0

Igor我认为你关于从约束类型继承的类型的评论正在成为问题的一部分,它不会在没有约束的情况下编译。但是我真正的问题是关于语法,尽管T似乎指的是类型和约束,即T和T本身的TextInput似乎都是模糊的。 – 2010-09-24 00:56:39

+0

谢谢刚刚读过CRTP。笑,我的例子有一个'这是设计'类型的感觉,从这个意义上说,你可以将循环定义为无意的递归,或者在这种情况下看起来像循环是真正的递归。它也显示了如何在给定的编程语言中变得流利:) – 2010-09-24 01:35:44

+0

哈哈,我在使用术语“流利”(fluent)这个术语时是在夹紧你。这是非常合适的,因为这种模式被称为Fluent Interface(方法链接)。 – 2010-09-24 01:46:15

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>是对T的约束从ReadOnly<T>,使得T可以在通用TextInput<TSource>中使用。 (T是TSource!)

我不认为它是循环的。

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

+0

嗨,杰夫,除了我们得出不同的结论之外,你和我的假设都一样。这对我来说是圆形的,因为使用数学或逻辑替换的约束意味着我们应该能够做到这样的事情:只读其中T:TextInput =>只读>其中T:TextInput =>只读 >> ....冲洗和无限重复无限:)。 – 2010-09-24 01:21:31

+0

嗨西蒙,如果你想'在哪里T:TextInput '就像说,“其中T是TextInput的通用部分”,它应该点击。希望。 :) – 2010-09-24 01:46:44

+0

换句话说,不要将where子句作为逻辑替代来读取。不是。它的意思是定义T如何与另一个对象相关,这更像是组合而不是替换。 – 2010-09-24 01:49:45

相关问题