为什么这个泛型约束在存在循环引用时能够编译通过

6
我在C#中编写了一个扩展方法,用于MVCContrib Html助手,并对其泛型约束的形式感到惊讶,因为它似乎通过类型参数循环地引用自身。
尽管如此,该方法编译并按预期工作。
我希望有人能解释为什么这样可以工作,以及是否存在更直观的语法,如果没有,是否有人知道原因?
以下是编译和函数代码,但我已经删除了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>'的类型参数'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>

或者

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

但是这两个都无法编译。

如已回答,这并不是循环的继承关系。然而,值得一提的是,有时候可以创建循环继承关系,它有时会编译成功,有时不会(例如添加、删除或重命名文件和文件夹可能会导致编译随机成功或失败)。因此,存在循环继承关系存在错误的情况。(VS2010) - AnorZaken
5个回答

11

更新:这个问题是我在2011年2月3日的博客文章的基础上提出的。感谢这个好问题!


这是合法的,不具有循环性,并且相当普遍。我个人不喜欢它。
我不喜欢它的原因是:
  1. It is excessively clever; as you've discovered, clever code is hard for people unfamiliar with the intricacies of the type system to intuitively understand.

  2. It does not map well to my intuition of what a generic type "represents". I like classes to represent categories of things, and generic classes to represent parameterized categories. It is clear to me that a "list of strings" and a "list of numbers" are both kinds of lists, differing only in the type of the thing in the list. It is much less clear to me what "a TextInput of T where T is a TextInput of T" is. Don't make me think.

  3. This pattern is frequently used in an attempt to enforce a constraint in the type system which is actually not enforcable in C#. Namely this one:

    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) { ... }
    }
    
这里的想法是“Animal的子类Cat只能与其他Cat交朋友”。
问题在于所期望的规则实际上并没有得到执行。
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#循环检测器出错的例子感兴趣,请参阅我的文章:

https://ericlippert.com/2008/05/07/covariance-and-contravariance-part-11-to-infinity-but-not-beyond/


谢谢Eric,这是一个有趣的领域。我还有一些关于约束的问题,但我想我应该把它们作为另一个问题发布 :-). - Simon Francesco

5
限制存在的原因是因为TextInput类型本身具有这样的约束。
public abstract class TextInput<T> where T: TextInput<T>{
   //...
}

还要注意,TextInput<T>是抽象的,唯一的方法是采用类似CRTP的方式从中派生一个实例:
public class FileUpload : TextInput<FileUpload> {
}

如果没有这个限制,扩展方法将无法编译,这就是为什么它存在的原因。

首先使用CRTP的原因是为了在基类上启用强类型方法,从而实现流畅接口,请考虑以下示例:

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

编辑 为了回应楼主关于递归类继承的评论。这是C++中一个众所周知的模式,称为奇异递归模板模式。在这里阅读相关内容。直到今天我才知道在C#中也有可能实现它。我怀疑这个限制与在C#中使用这种模式有关。


Igor,我认为你关于类型继承自受限类型的评论是导致它没有约束就无法编译的问题的一部分。但我的真正问题在于语法,其中T似乎同时指代类型和约束,即T的TextInput和T本身,这似乎是不明确的。 - Simon Francesco
谢谢,我刚刚读了一下CRTP。哈哈,我的例子有一种“这是设计意图”的感觉,就是说你可以将循环定义为无意的递归,或者在这种情况下,看起来像循环的东西实际上是递归。它还展示了成为某种编程语言流利的难度有多大 :) - Simon Francesco
哈哈,我笑你用了“流畅”的术语。这在这里非常恰当,因为这种模式被称为流畅接口(方法链式调用)。 - Igor Zevaka

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),那么您甚至可以删除该约束,它表示当T是引用类型时为null,对于任何值类型则为全零。
/* most generic form */
public static List<T> AddNullItem<T>(this List<T> list) 
{
    list.Add(default(T));
    return list;
}

我怀疑你的第一种方法也需要像 T : class 这样的约束,但由于我没有你使用的所有类,所以无法确定。


我同意这段代码看起来毫无意义,但它可以编译并且实现了我的预期功能。你的示例太简单,无法涵盖所有使用情况。 - Simon Francesco
抱歉我误按了回车,我想继续说... 我在考虑一个 List<T>,其中我可以有一组香蕉,并拥有一个扩展方法,例如 list.AddNullItem(),因此泛型参数本身也是泛型的。 - Simon Francesco
我希望我添加的额外信息能够帮助您了解为什么没有约束条件编译不通过,但是这个约束条件并不一定正确。 - Ben Voigt
感谢你的努力,Ben。我很感激。我认为总体来说,我应该删除这个比喻,因为它并没有解决我想要解决的问题。我也知道原始类型与对象类型的问题可能会使问题变得模糊。我将编辑我的原始问题来解决这个问题。 - Simon Francesco

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 命名空间应该让你意识到这是代码的意图(启用方法调用的链接)。


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),这显然是行不通的。


嗨,杰夫,你和我有完全相同的假设,只是我们得出了不同的结论。对我来说,这感觉循环,因为使用数学或逻辑替换,约束意味着我们应该能够像这样做:ReadOnly<T> where T : TextInput<T> => ReadOnly<TextInput<T>> where T : TextInput<T> => ReadOnly<TextInput<TextInput<T>>> .... 以此类推无限次:)。 - Simon Francesco
嗨,Simon,如果你认为where T: TextInput<T>只是在说,“T是TextInput的泛型部分”,那应该就能理解了。希望如此。 :) - Jeff Meatball Yang
换句话说,不要将where子句看作逻辑替换。它的作用是定义T与另一个对象的关系,更像是组合而不是替换。 - Jeff Meatball Yang
抱歉,Jeff,但这个限制意味着“TTextInput<T>的子类(继承自)”。 - Ben Voigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接