为什么这个generics约束在它似乎有一个循环引用时编译
我在csharp中为MVCContrib Html帮助器编写了一个扩展方法,并对通用约束的forms感到惊讶,从表面看它通过类型参数循环引用自身。
据说这种方法可以根据需要进行编译和工作。
我希望有人解释为什么这有效,如果存在更直观的直观语法,如果没有人知道为什么?
这是编译和function代码,但我删除了T的例子,因为它使问题蒙上阴影。 以及使用List 的类似方法。
namespace MvcContrib.FluentHtml { public static class FluentHtmlElementExtensions { public static TextInput ReadOnly(this TextInput element, bool value) where T: TextInput { if (value) element.Attr("readonly", "readonly"); else ((IElement)element).RemoveAttr("readonly"); return element; } } }
/*analogous method for comparison*/ public static List AddNullItem(this List list, bool value) where T : List { list.Add(null); return list; }
在第一种方法中,约束T:TextInput 似乎是所有意图和目的,循环。 但是,如果我发表评论,我会收到编译错误:
“类型’T’不能用作generics类型或方法’MvcContrib.FluentHtml.Elements.TextInput ‘中的类型参数’T’。没有从’T’到’MvcContrib的装箱转换或类型参数转换.FluentHtml.Elements.TextInput ”“。
在List 的情况下,错误是:
“’System.Collections.Generic.List.Add(T)’的最佳重载方法匹配有一些无效的参数参数1:无法从”转换为’T’”
我可以想象一个更直观的定义是包含2种类型的定义,对generics类型的引用和对约束类型的引用,例如:
public static TextInput ReadOnly(this TextInput element, bool value) where U: TextInput
要么
public static U ReadOnly(this U element, bool value) where U: TextInput
但这些都没有编译。
更新:这个问题是我2011年2月3日博客文章的基础。 谢谢你这个好问题!
这是合法的,它不是循环的,而且很常见。 我个人不喜欢它。
我不喜欢它的原因是:
1)过于聪明; 正如您所发现的那样,对于不熟悉类型系统的复杂性的人来说,聪明的代码很难直观地理解。
2)它没有很好地映射到我对通用类型“代表”的直觉。 我喜欢用于表示事物类别的类,以及用于表示参数化类别的generics类。 我很清楚,“字符串列表”和“数字列表”都是各种列表,仅在列表中的事物类型上有所不同。 我不太清楚“T的TextInput,其中T是T的TextInput”是什么。 不要让我思考。
3)此模式经常用于尝试在类型系统中强制实施约束,该约束在C#中实际上是不可执行的。 即这一个:
abstract class Animal where T : Animal { public abstract void MakeFriends(IEnumerable newFriends); } class Cat : Animal { public override void MakeFriends(IEnumerable newFriends) { ... } }
这里的想法是“动物猫只能与其他猫交朋友”。
问题是实际上没有强制执行所需的规则:
class Tiger: Animal { public override void MakeFriends(IEnumerable newFriends) { ... } }
现在老虎可以和猫交朋友,但不能和老虎交朋友。
要在C#中实际完成这项工作,您需要执行以下操作:
abstract class Animal { public abstract void MakeFriends(IEnumerable newFriends); }
其中“THISTYPE”是一种神奇的新语言特征,意思是“在这里填写自己的类型需要一个重写的类”。
class Cat : Animal { public override void MakeFriends(IEnumerable newFriends) {} } class Tiger: Animal { // illegal! public override void MakeFriends(IEnumerable newFriends) { ... } }
不幸的是,这也不是类型安全的:
Animal animal = new Cat(); animal.MakeFriends(new Animal[] {new Tiger()});
如果规则是“动物可以与任何一种朋友交朋友”,那么动物可以与动物交朋友。 但猫只能与猫交朋友,而不是老虎! 参数位置的东西必须是有效的; 在这个假设的情况下,我们需要协方差 ,这是行不通的。
我似乎有些偏离。 回到这个奇怪的反复出现的模式的主题:我试图只将这种模式用于常见的,易于理解的情况,如其他答案中提到的情况:
class SortedList where T : IComparable
也就是说,如果我们有希望制作它们的排序列表,我们需要每个T都能与其他T相媲美。
要实际被标记为循环,必须在依赖关系中有一个真正的循环:
class C where T : U where U : T
类型理论的一个有趣的领域(目前C#编译器处理不当)是非循环但无限的generics类型的领域。 我编写了一个无限型检测器,但它没有进入C#4编译器,并且对于可能的假设未来版本的编译器而言并不是高优先级。 如果您对某些无限类型的示例感兴趣,或者对C#循环检测器搞砸的一些示例感兴趣,请参阅我的文章:
原因是因为TextInput
类型本身就有这样的约束。
public abstract class TextInput where T: TextInput { //... }
另请注意, TextInput
是抽象的,制作此类实例的唯一方法是以类似CRTP的方式从中派生:
public class FileUpload : TextInput { }
如果没有那个约束,扩展方法将无法编译,这就是它存在的原因。
首先使用CRTP的原因是为了在基类上启用强类型方法启用Fluent接口 ,因此请考虑以下示例:
public abstract class TextInput where T: TextInput { public T Length(int length) { Attr(length); return (T)this; } } public class FileUpload : TextInput { 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 .FileName("filename.txt"); //FileUpload instance, defined on FileUpload
编辑解决OP关于递归类inheritance的注释。 这是C ++中众所周知的模式,称为Curiously Recurring Template Pattern。 在这里阅读它。 到目前为止,我不知道在C#中是否可行。 我怀疑约束与在C#中使用这种模式有关。
你使用它的方式毫无意义。 但是在相同参数的约束中使用generics参数是很正常的,这是一个更明显的例子:
class MySortedList where T : IComparable
约束表达了这样一个事实,即T类对象之间必须有一个排序,以便将它们排序。
编辑:我将解构你的第二个例子,其中约束实际上是错误的,但有助于编译。
有问题的代码是:
/*analogous method for comparison*/ public static List AddNullItem (this List list, bool value) where T : List { list.Add(null); return list; }
它没有约束就不能编译的原因是值类型不能为null
。 List
是一个引用类型,因此通过强制where T : List
您强制T
为可以为null的引用类型。 但是你也使AddNullItem
几乎无用,因为你不能再在List
等上调用它。正确的约束是:
/* corrected constraint so the compiler won't complain about null */ public static List AddNullItem (this List list) where T : class { list.Add(null); return list; }
注意:我还删除了第二个未使用的参数。
但是如果你使用default(T)
,你甚至可以删除那个约束,它是为了这个目的而提供的,当T
是引用类型时它意味着null
,对于任何值类型都是零。
/* most generic form */ public static List AddNullItem (this List list) { list.Add(default(T)); return list; }
我怀疑你的第一个方法也需要像T : class
这样的约束,但由于我没有你所使用的所有类,我无法肯定地说。
我只能猜测你发布的代码是什么。 也就是说,我可以看到像这样的generics类型约束的优点。 在任何你想要某种类型的参数可以对同一类型的参数执行某些操作的任何场景中(对我而言)都是有意义的。
这是一个不相关的例子:
public static IComparable Max (this IComparable value, T other) where T : IComparable { return value.CompareTo(other) > 0 ? value : other; }
像这样的代码可以让你写下这样的东西:
int start = 5; var max = start.Max(6).Max(3).Max(10).Max(8); // result: 10
命名空间FluentHtml
应该让你FluentHtml
这是代码的意图(启用方法调用的链接)。
public static TextInput ReadOnly (this TextInput element, bool value) where T: TextInput
让我们分解一下:
TextInput
是返回类型。
TextInput
是要扩展的类型(静态方法的第一个参数的类型)
ReadOnly
是扩展定义包含T的类型的函数的名称,即TextInput
。
where T: TextInput
是来自ReadOnly
T的约束,因此T可以在通用TextInput
。 (T是TSource!)
我不认为这是循环的。
如果你取出约束,我希望该element
试图被转换为generics类型(不是generics类型的TextInput),这显然不会起作用。