【问题标题】:Why does this generics scenario cause a TypeLoadException?为什么这个泛型场景会导致 TypeLoadException?
【发布时间】:2011-05-16 01:21:42
【问题描述】:

这有点啰嗦,所以这里是快速版本:

为什么这会导致运行时 TypeLoadException?(编译器是否应该阻止我这样做?)

interface I
{
    void Foo<T>();
}

class C<T1>
{
    public void Foo<T2>() where T2 : T1 { }
}

class D : C<System.Object>, I { } 

如果您尝试实例化 D,则会发生异常。


更长、更具探索性的版本:

考虑:

interface I
{
    void Foo<T>();
}

class C<T1>
{
    public void Foo<T2>() where T2 : T1 { }
}

class some_other_class { }

class D : C<some_other_class>, I { } // compiler error CS0425

这是非法的,因为C.Foo() 上的类型约束与I.Foo() 上的类型约束不匹配。它会生成编译器错误 CS0425。

但我认为我可能能够打破规则:

class D : C<System.Object>, I { } // yep, it compiles

通过使用Object 作为 T2 的约束,我否定该约束。我可以安全地将任何类型传递给D.Foo&lt;T&gt;(),因为一切都源自Object

即便如此,我仍然希望得到一个编译器错误。在 C# 语言 的意义上,它违反了“C.Foo() 上的约束必须与 I.Foo() 上的约束相匹配”的规则,我认为编译器将是规则。但它确实编译。似乎编译器看到了我在做什么,理解它是安全的,然后视而不见。

我以为我已经成功了,但运行时说没那么快。如果我尝试创建 D 的实例,我会收到 TypeLoadException:“类型 'D' 上的方法 'C`1.Foo' 试图隐式实现具有较弱类型参数约束的接口方法。”

但是这个错误在技术上不是错误的吗?将Object 用于C&lt;T1&gt; 是否会否定C.Foo() 的约束,从而使其等效于-不强于-I.Foo()?编译器似乎同意,但运行时不同意。

为了证明我的观点,我通过将D 排除在等式之外来简化它:

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class some_other_class { }

class C : I<some_other_class> // compiler error CS0425
{
    public void Foo<T>() { }
}

但是:

class C : I<Object> // compiles
{
    public void Foo<T>() { }
}

这对于传递给Foo&lt;T&gt;() 的任何类型都可以完美编译和运行。

为什么?运行时是否存在错误,或者(更有可能)我没有看到导致此异常的原因 - 在这种情况下,编译器不应该阻止我吗?

有趣的是,如果通过将约束从类移动到接口来反转场景......

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class C
{
    public void Foo<T>() { }
}

class some_other_class { }

class D : C, I<some_other_class> { } // compiler error CS0425, as expected

我再次否定了约束:

class D : C, I<System.Object> { } // compiles

这次运行正常!

D d := new D();
d.Foo<Int32>();
d.Foo<String>();
d.Foo<Enum>();
d.Foo<IAppDomainSetup>();
d.Foo<InvalidCastException>();

任何事情都会发生,这对我来说很有意义。 (等式中有或没有D 相同)

那么为什么第一种方式会中断?

附录:

我忘了补充一点,TypeLoadException 有一个简单的解决方法:

interface I
{
    void Foo<T>();
}

class C<T1>
{
    public void Foo<T2>() where T2 : T1 { }
}

class D : C<Object>, I 
{
    void I.Foo<T>() 
    {
        Foo<T>();
    }
}

显式实现I.Foo() 很好。只有隐式实现会导致 TypeLoadException。现在我可以这样做了:

        I d = new D();
        d.Foo<any_type_i_like>();

但这仍然是一个特例。尝试使用 System.Object 以外的任何其他内容,这将无法编译。我觉得这样做有点脏,因为我不确定它是否故意这样工作。

【问题讨论】:

  • 并非所有类型都真正继承自Object。所有堆实例都是派生自Object 的类型,但值类型存储位置仅保存该类型的字段(公共和私有),没有任何附加类型信息。这样的字段集合可以隐式转换为Object,但不是一个。请注意,将某些内容约束到 Object 类型的泛型类型参数会有效地添加 class 约束。进一步注意,具有接口约束和class 约束的通用参数将接受该接口的struct 实现,如果它们是...
  • ...在传递之前转换为堆对象。

标签: c# generics clr runtime compiler-bug


【解决方案1】:

这是一个错误 - 请参阅 Implementing Generic Method From Generic Interface Causes TypeLoadExceptionUnverifiable Code with Generic Interface and Generic Method with Type Parameter Constraint。不过,我不清楚这是 C# 错误还是 CLR 错误。

[由 OP 添加:]

这是微软在您链接到的第二个线程中所说的(我的重点):

两者之间存在不匹配 运行时使用的算法和 C#编译器判断一组是否 约束和另一个一样强 放。这种不匹配导致 C# 编译器接受一些结构 运行时拒绝和 结果是你的 TypeLoadException 看。我们正在调查以确定 如果这段代码是 那个问题。无论如何,它是 当然不是“按设计”, 编译器接受这样的代码 导致运行时异常。

问候,

Ed Maurer C# 编译器开发 铅

从我加粗的部分来看,我认为他说这是一个编译器错误。那是在 2007 年。我想这还不够严重,不足以成为他们修复它的优先事项。

【讨论】:

  • 谢谢。我希望你不介意,我编辑了你的答案,包括微软所说的关于这个错误的内容,这很好地回答了我的问题。我会接受这个答案,除非像 Eric Lippert 这样的人碰巧很快就会提供更多细节。
  • @IgbyLargeman:在我看来,它确实像一个 C# 编译器错误。我认为没有理由将方法 Foo&lt;T&gt;() where T:U 视为 Foo&lt;T&gt;() 的实现。我认为困难部分源于 C# 在匹配方法签名时没有检查约束,尽管即使 C# 匹配签名它也应该注意到不兼容的约束。
【解决方案2】:

唯一的解释是约束被认为是方法声明的一部分。这就是为什么在第一种情况下它是编译器错误。

当你使用object时编译器没有得到错误...嗯,那是编译器的一个错误

其他“约束”具有通用约束的相同属性:

interface I
{
    object M();
}

class C
{
    public some_type M() { return null; }
}

class D : C, I
{
}

我可以问:为什么这不起作用?

你看到了吗?这和你的问题完全一样。用some_type 实现object 是完全有效的,但是运行时和编译器都不会接受它。

如果你尝试生成 MSIL 代码,并强制执行我的示例,运行时会报错。

【讨论】:

  • 我明白你的意思。如果运行时没有抱怨怎么办?它会起作用吗?在我的第二个示例中,约束位于 I.Foo 上,运行时似乎很开心。我仍然对此感到困惑。
  • 嗨!如果运行时没有抱怨,您的第一个示例将起作用。但事实是它会抱怨。您的问题没有可理解的答案...我认为这两个示例都应该有效,但是运行时检查约束的方式使其无法正常工作。以这种方式检查约束是运行时的“规则”,因为它是这样编码的。也许在未来的版本中,他们会进行检查,以便在使用 object 时,它的工作方式就像没有任何限制一样。
【解决方案3】:

隐式接口实现要求方法声明上的通用约束是等价的,但在代码中不一定完全相同。此外,泛型类型参数具有“where T : object”的隐式约束。这就是为什么指定C&lt;Object&gt; 编译的原因,它使约束变得等同于接口中的隐式约束。 (C# Language Spec 第 13.4.3 节)。

您也正确,使用调用受约束方法的显式接口实现将起作用。它提供了从接口方法到约束不能不同的类中的实现的非常清晰的映射,然后继续调用类似名称的泛型方法(现在与界面)。此时,可以像任何通用方法调用一样解决辅助方法的约束,而不会出现任何接口解析问题。

在您的第二个示例中,将约束从类移动到接口会更好,因为默认情况下该类将从接口获取其约束。这也意味着您必须在类实现中指定约束(如果适用)(如果是 Object,则不适用)。传递 I&lt;string&gt; 意味着您不能直接在代码中指定该约束(因为字符串是密封的),因此它必须是显式接口实现的一部分,或者是与两个地方的约束相等的泛型类型。

据我所知,运行时和编译器使用单独的约束验证系统。编译器允许这种情况,但运行时验证器不喜欢它。我想强调一下,我不确定它为什么会出现问题,但我猜它不喜欢该类定义中满足接口约束的可能性,具体取决于关于 T 最终设置的内容。如果其他人对此有明确的答案,那就太好了。

【讨论】:

  • 在我的示例中使用字符串是一个糟糕的选择,因为它是密封的 - 感谢您指出这一点(现已修复)。但是最后它没有任何区别,因为我故意不匹配约束,所以除了 Object 之外什么都不会编译。不过有帮助的答案,谢谢。我看了你提到的规范部分 - 有点伤我的头。
【解决方案4】:

响应你基于sn-p的界面:

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class C : I<string> // compiler error CS0425
{
    public void Foo<T>() { }
}

我认为问题在于编译器认识到:

  1. 您尚未在 C.Foo() 上声明必要的类型约束。
  2. 如果您选择字符串作为您的类型,则 C.Foo() 上没有有效的 T,因为类型不能从字符串继承。

要在实践中看到这项工作,请指定一个可以从 T1 继承的实际类。

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class C : I<MyClass>
{
    public void Foo<T>() where T : MyClass { }
}

public class MyClass
{
}

要表明 string 类型没有以任何方式被特殊对待,只需将 sealed 关键字添加到上面的 MyClass 声明中,以查看它是否以相同的方式失败,如果您将把 T1 指定为字符串,并将字符串指定为 C.Foo() 的类型约束。

public sealed class MyClass
{
}

这是因为字符串是密封的,不能构成约束的基础。

【讨论】:

  • String 是我第一个想到的类型——一个不好的例子。感谢您指出,我将编辑问题以避免混淆。至于你的另一点 - 是的,你可以通过添加适当的约束来使其工作,但这会破坏这个问题的重点。
  • 为什么指定约束会破坏问题的重点。它恰好是编译器设置的一个非常简单的规则,覆盖方法或实现接口的方法必须重新声明并满足基本方法的方法签名的约束。如果您想编写一个新方法 Foo2 或类似方法,您不会遇到同样的问题,因为它没有您遇到的限制,但关键是该方法必须遵守它的基础,否则您需要制定规则多态性无法强制执行。
  • 我要做的第一件事是演示预期和理解的编译器错误 CS0425,以便为问题的其余部分设置背景。关键是当我使用 Object 作为类型参数用作约束时,我可以规避匹配约束的要求,但这会导致运行时错误。如果我指定了匹配的约束,我就没有问题要问了。 :) (我的问题显然过于冗长和对话,而不是清晰简洁,对此我深表歉意)
  • 我认为这是一个很好的话题,我想我只是不确定你是否理解编译器实际上在抱怨什么。现在我意识到问题更多是关于一些特殊的边缘情况,以及为什么编译器要等到运行时才强制执行似乎更像是编译时约束的东西。祝你找到答案。
猜你喜欢
  • 1970-01-01
  • 2013-02-25
  • 2014-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-02
相关资源
最近更新 更多