为啥泛型类型约束不可继承/分层强制执行

Posted

技术标签:

【中文标题】为啥泛型类型约束不可继承/分层强制执行【英文标题】:Why aren't generic type constraints inheritable/hierarchically enforced为什么泛型类型约束不可继承/分层强制执行 【发布时间】:2011-12-22 15:56:05 【问题描述】:

物品类别

public class Item

    public bool Check(int value)  ... 

具有泛型类型约束的基本抽象类

public abstract class ClassBase<TItem>
    where TItem : Item

    protected IList<TItem> items;

    public ClassBase(IEnumerable<TItem> items)
    
        this.items = items.ToList();
        

    public abstract bool CheckAll(int value);

没有约束的继承类

public class MyClass<TItem> : ClassBase<TItem>

    public override bool CheckAll(int value)
    
        bool result = true;
        foreach(TItem item in this.items)
        
            if (!item.Check(value)) // this doesn't work
            
                result = false;
                break;
            
        
        return result;
    

我想知道为什么泛型类型约束不可继承?因为如果我的继承类继承自基类并传递其对基类具有约束的泛型类型,则它自动意味着继承类中的泛型类型应该具有相同的约束,而无需显式定义它。不应该吗?

是我做错了什么,理解错了还是泛型类型约束真的不可继承?如果后者是真的,这到底是为什么

一点补充说明

为什么我认为在一个类上定义的泛型类型约束应该被继承或在子类上强制执行?让我给你一些额外的代码,让它不那么明显。

假设我们拥有上述所有三个类。然后我们也有这个类:

public class DanteItem

    public string ConvertHellLevel(int value)  ... 

我们可以看到这个类不是从Item 继承的,所以它不能像ClassBase&lt;DanteItem&gt; 那样用作具体类(忘记ClassBase 现在是抽象的事实。它也可以是一个普通班)。由于MyClass 没有为其泛型类型定义任何约束,因此拥有MyClass&lt;DanteItem&gt; 似乎完全有效...

但是。这就是为什么我认为泛型类型约束应该像成员泛型类型约束一样在继承类上继承/强制执行,因为如果我们查看MyClass 的定义,它会说:

MyClass<T> : ClassBase<T>

TDanteItem 时,我们可以看到它自动不能与MyClass 一起使用,因为它是从ClassBase&lt;T&gt; 继承的,而DanteItem 不满足其泛型类型约束。我可以说MyClass 上的**泛型类型取决于ClassBase 泛型类型约束,因为否则MyClass 可以用任何类型实例化。但我们知道不可能。

如果我将MyClass 定义为:

public class MyClass<T> : ClassBase<Item>

在这种情况下,T 与基类的泛型类型没有任何关系,因此它独立于它。

这是一个有点长的解释/推理。我可以简单地总结为:

如果我们不对MyClass 提供泛型类型约束,则暗示我们可以用任何具体类型 实例化MyClass。但我们知道这是不可能的,因为MyClass 是从ClassBase 继承而来的,并且它具有泛型类型约束。

我希望现在这更有意义。

【问题讨论】:

***.com/questions/1420581/… 的可能重复项 我正在查看语言规范,我看到“由于没有继承类型参数,因此也永远不会继承约束”(第 4.4.4 节)。这引出了一个问题“为什么不继承类型参数?”这可能会导致对开放类型和封闭类型以及类似事物的更深入讨论,也在规范中。从实际的角度来看,必须重述约束可能对您离基本类型越远越好,尤其是在闭源程序集中时。 @AnthonyPegram:类型参数不是继承的(1)因为它们不是成员,(2)因为这没有任何意义。您从 type 继承。未构造的泛型类型不是类型,因此您不能从它继承。要从泛型类型继承,您必须构造类型,这意味着用某些东西代替类型参数。类型参数不是您从基类获得的事物!它是基类中的一个hole,您必须先填充它才能继承from @EricLippert 我相信让 OP 跳闸的是,虽然约束不是 de jure 继承的(所以依赖于约束编译的代码,嗯,赢了't) 基类型的约束仍然与派生类型的用户非常相关,因为派生类型的类型参数仍必须满足基类型的约束。 在使用泛型类进行继承时,有很多可能的场景/组合,尤其是当一个类具有多个泛型参数时。一个彻底的答案可能会提供一堆示例,其中这种行为的隐含性质可能会导致混淆。我认为一个非常适用的quote from Eric Lippert 是:C# 的设计原则之一是“如果你说错了,我们会告诉你,而不是试图猜测你的意思” 【参考方案1】:

另一个更新:

这个问题是the subject of my blog in July 2013。谢谢你的好问题!

更新:

我对此进行了更多思考,我认为问题在于您根本不想要继承。相反,您想要的是必须放置在类型参数上的所有约束,以便将该类型参数用作另一个类型中的类型参数,以便自动推导并无形地添加到类型参数的声明中. 是吗?

一些简化的例子:

class B<T> where T:C 
class D<U> : B<U> 

U 是在必须是 C 的上下文中使用的类型参数。因此,您认为编译器应该推断出这一点,并自动在 U 上放置 C 的约束。

这个怎么样?

class B<T, U> where T : X where U : Y 
class D<V> : B<V, V> 

现在 V 是在必须同时是 X 和 Y 的上下文中使用的类型参数。因此,您认为编译器应该推断出这一点并自动在 V 上设置 X 和 Y 的约束。是吗?

这个怎么样?

class B<T> where T : C<T> 
class C<U> : B<D<U>> where U : IY<C<U>> 
class D<V> : C<B<V>> where V : IZ<V> 

我只是编造的,但我向你保证,这是一个完全合法的类型层次结构。请描述一个明确且一致的规则,该规则不会进入无限循环以确定 T、U 和 V 上的所有约束。不要忘记处理已知类型参数为引用类型且接口约束具有的情况协方差或逆变注释!此外,无论 B、C 和 D 在源代码中以什么顺序出现,算法都必须具有完全相同相同结果的特性。

如果约束推断是您想要的功能,那么编译器必须能够处理此类情况,并在无法处理时给出明确的错误消息。

基本类型有什么特别之处?为什么不完全实现这个功能呢?

class B<T> where T : X 
class D<V>  B<V> bv; 

V 是在必须可转换为 X 的上下文中使用的类型参数;因此编译器应该推断出这个事实并在 V 上设置 X 的约束。是吗?还是没有?

为什么字段是特殊的?这个呢:

class B<T>  static public void M<U>(ref U u) where U : T  
class D<V> : B<int>  static V v; static public void Q()  M(ref v);  

V 是在只能是 int 的上下文中使用的类型参数。因此 C# 编译器应该推断出这个事实并自动在 V 上放置一个 int 约束。

是吗?没有?

你知道这是怎么回事吗?它停在哪里?为了正确实现您想要的功能,编译器必须进行整个程序分析。

编译器不会进行这种级别的分析,因为那是本末倒置。当你构造一个泛型时,需要向编译器证明你已经满足了约束。编译器的工作不是弄清楚您要说什么并计算出哪些进一步的约束集满足原始约束。

出于类似的原因,编译器也不会尝试代表您自动推断接口中的方差注释。有关详细信息,请参阅我关于该主题的文章。

http://blogs.msdn.com/b/ericlippert/archive/2007/10/29/covariance-and-contravariance-in-c-part-seven-why-do-we-need-a-syntax-at-all.aspx


原答案:

我想知道为什么泛型类型约束不可继承?

只有成员被继承。约束不是成员。

如果我的继承类继承自基类并传递其对基类有约束的泛型类型,则自动意味着继承类中的泛型类型应该具有相同的约束,而无需显式定义它。不应该吗?

你只是在断言某事应该是怎样的,而没有提供任何解释为什么它应该是这样的。向我们解释为什么您认为世界应该是这样的; 好处是什么,缺点是什么,成本是什么?

是我做错了什么,理解错了还是泛型类型约束真的不可继承?

通用约束不会被继承。

如果后者是真的,那到底是为什么?

默认情况下,功能“未实现”。我们不必提供实现某功能的原因! 每个功能只有在有人花钱实施之前才会实施。

现在,我要注意泛型类型约束继承在方法上的。方法是成员,成员是继承的,约束是方法的一部分(尽管不是其签名的一部分)。因此,约束在继承时与方法一起出现。当你说:

class B<T> 

    public virtual void M<U>() where U : T 


class D<V> : B<IEnumerable<V>>

    public override void M<U>() 

那么D&lt;V&gt;.M&lt;U&gt;继承约束,用IEnumerable&lt;V&gt;代替T;因此,约束是 U 必须可转换为 IEnumerable&lt;V&gt;。请注意,C# 不允许您重述约束。在我看来,这是一个错误的特征;为了清楚起见,我希望能够重申约束。

但是 D 并没有从 B 继承任何对 T 的约束;我不明白它怎么可能。 M 是 B 的 成员,并由 D 及其约束继承。但是 T 本来就不是 B 的成员,那么有什么可以继承的呢?

我真的不明白你想要这里的什么功能。能详细解释一下吗?

【讨论】:

在继承层次结构中,如果只传播泛型和约束,则必须为每个继承级别重新定义约束。这会导致重复代码。不能为每个继承级别自动强制约束而不是重新定义吗?或者 Visual Studio 是否可以为您添加约束(使用著名的 Ctrl + .)? @EricLippert 在 public class B where T : IConstraint1 where U : IConstraint2 public class D : B 的情况下,编译器给出错误如果您不指定 where V : IConstraint1, IConstraint2 如果编译器已经可以将其作为错误信号并指示您做什么,那么为什么我必须插入该语句?在 D : B 的情况下,我理解为什么这会导致大量“编译器工作”,但在 D : B 的情况下则不然?还是我错过了什么? @WouterdeKort:一个合理的问题。有这样的编程语言; JScript 将自动插入缺少的分号(有时这样做不正确)。当您尝试写入只读属性而不是报告错误时,JScript 将静默失败!简而言之,JScript 会尝试对您隐藏错误。这不是 C# 的基本设计原则; C# 不是“代表用户进行猜测并希望最好的”语言。 C# 是一种“报告错误并让开发人员按照开发人员认为合适的方式进行修复”的语言。 @RobertKoritnik:当然有一个猜测。您提议编译器猜测开发人员打算对 B 中的 T 进行约束。如果开发人员打算以任何方式限制 B 的用户怎么办?开发人员可以选择解决问题的方法:将约束放在 B 上,删除 A 上的约束,或者让 B 不从 A 派生。您希望编译器猜测正确的解决方案是第一个解决方案。现在,为了论证起见,假设 99% 的时间 正确的解决方案。 1% 的错误程序有多少? @RobertKortinik:虽然我同意这样的例子可能不太可能(1)你可能会惊讶于有些人在现实代码中推动类型系统的努力,以及(2)不管类型是否拓扑可能与否,它们是合法,因此编译器必须能够处理它们。【参考方案2】:

以下是这种行为的隐含性质导致的行为与预期不同的情况:

我认识到这种情况在设置数量上可能看起来很奢侈,但这只是此行为可能导致问题的一个示例。软件应用程序可能很复杂,因此即使这种情况看起来很复杂,但我不会说这不可能发生。

在这个例子中,有一个 Operator 类实现了两个类似的接口:IMonitor 和 IProcessor。两者都有一个 start 方法和一个 IsStarted 属性,但是 Operator 类中每个接口的行为是独立的。 IE。 Operator 类中有一个 _MonitorStarted 变量和一个 _ProcessorStarted 变量。

MyClass&lt;T&gt; 派生自 ClassBase&lt;T&gt;。 ClassBase 对 T 有一个类型约束,它必须实现 IProcessor 接口,并且根据建议的行为 MyClass 继承该类型约束。

MyClass&lt;T&gt; 有一个 Check 方法,该方法是在假设它可以从内部 IProcessor 对象中获取 IProcessor.IsStarted 属性的值的情况下构建的。

假设有人更改了 ClassBase 的实现,以移除 IProcessor 对泛型参数 T 的类型约束,并将其替换为 IMonitor 的类型约束。此代码将静默工作,但会产生不同的行为。原因是MyClass&lt;T&gt; 中的 Check 方法现在调用的是 IMonitor.IsStarted 属性而不是 IProcessor.IsStarted 属性,即使 MyClass&lt;T&gt; 的代码根本没有改变。

public interface IMonitor

    void Start();

    bool IsStarted  get; 


public interface IProcessor

    void Start();

    bool IsStarted  get; 


public class Operator : IMonitor, IProcessor

    #region IMonitor Members

    bool _MonitorStarted;

    void IMonitor.Start()
    
        Console.WriteLine("IMonitor.Start");
        _MonitorStarted = true;
    

    bool IMonitor.IsStarted
    
        get  return _MonitorStarted; 
    

    #endregion

    #region IProcessor Members

    bool _ProcessorStarted;

    void IProcessor.Start()
    
        Console.WriteLine("IProcessor.Start");
        _ProcessorStarted = true;
    

    bool IProcessor.IsStarted
    
        get  return _ProcessorStarted; 
    

    #endregion


public class ClassBase<T>
    where T : IProcessor

    protected T Inner  get; private set; 

    public ClassBase(T inner)
    
        this.Inner = inner;
    

    public void Start()
    
        this.Inner.Start();
    


public class MyClass<T> : ClassBase<T>
    //where T : IProcessor

    public MyClass(T inner) : base(inner)  

    public bool Check()
    
        // this code was written assuming that it is calling IProcessor.IsStarted
        return this.Inner.IsStarted;
    


public static class Extensions

    public static void StartMonitoring(this IMonitor monitor)
    
        monitor.Start();
    

    public static void StartProcessing(this IProcessor processor)
    
        processor.Start();
    



class Program

    static void Main(string[] args)
    
        var @operator = new Operator();

        @operator.StartMonitoring();

        var myClass = new MyClass<Operator>(@operator);

        var result = myClass.Check();

        // the value of result will be false if the type constraint on T in ClassBase<T> is where T : IProcessor
        // the value of result will be true if the type constraint on T in ClassBase<T> is where T : IMonitor
    

【讨论】:

【参考方案3】:

我认为你很困惑,因为你也在用TItem 声明你的派生类。

如果您考虑使用 Q 代替的话。

public class MyClass<Q> : BaseClass<Q>

 ...

那么如何确定Qitem 类型呢?

您还需要将约束添加到派生类 Generic Type 中

public class MyClass<Q> : BaseClass<Q> were Q : Item  ...  

【讨论】:

我看不出更改类型名称有何改变?代码暗示了约束,但编译器不遵守这一点。为什么是这个问题的重点。 @MylesMcDonnell:编译器当然尊重约束。它通过在违反约束时产生错误来兑现它。派生类的声明违反了约束。 是的,对不起,写得不好。我应该说编译器不继承类型约束。我认为问题是,如果具体类型根据定义受到基类型约束的约束,那么必须再次在具体类上显式定义此约束似乎是多余的,那么为什么会这样呢?【参考方案4】:

因为 ClassBase 对他的模板有一个约束(应该是 typeof Item),所以你也必须将这个约束添加到 MyClass 中。 如果您不这样做,您可以创建 MyClass 的新实例,其中模板不是 Item 类型。创建基类时,它会失败。

[编辑] 嗯,现在重新阅读你的问题,我看到你的代码编译了吗?好的。

好吧,我的 MyClass 你不知道 this.items 的基本类型,所以你不能调用 Check 方法。 this.items 属于 IList 类型,并且在您的类中,没有指定 TItem,这就是该类不理解 Check 方法的原因。

让我反驳您的问题,您为什么不想将约束添加到您的 MyClass 类?给定任何其他类类型作为此类的模板,将导致错误。为什么不通过添加约束来防止此错误,以免编译时失败。

【讨论】:

啊当然了,因为Check方法未知,所以编译不了。 它无法编译,因为类型约束没有被继承(这就是 Check 不存在的原因),根据问题的要点。

以上是关于为啥泛型类型约束不可继承/分层强制执行的主要内容,如果未能解决你的问题,请参考以下文章

unity的C#学习——泛型的创建与继承泛型集合类泛型中的约束和反射

Java泛型

Java泛型

Java泛型

Kotlin泛型 ① ( 泛型类 | 泛型参数 | 泛型函数 | 多泛型参数 | 泛型类型约束 )

泛型(第四部分)