为什么这种泛型方案会导致类型加载异常
本文关键字:类型 加载 异常 泛型 方案 为什么 | 更新日期: 2023-09-27 17:56:25
这有点啰嗦,所以这里是快速版本:
为什么这会导致运行时 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<T>()
,因为一切都源于Object
。
即便如此,我仍然希望得到编译器错误。在 C# 语言意义上,它违反了"C.Foo() 上的约束必须与 I.Foo() 上的约束匹配"的规则,我认为编译器会坚持这些规则。但它确实可以编译。编译器似乎看到了我在做什么,理解它是安全的,并且视而不见。
我以为我已经侥幸逃脱了,但运行时说没有那么快。如果我尝试创建D
的实例,我会得到一个TypeLoadException:"类型'D'上的方法'C'1.Foo'试图隐式实现具有较弱类型参数约束的接口方法。
但这个错误在技术上不是错误的吗?使用 Object
表示C<T1>
是否否定了对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<T>()
的任何类型都可以完美编译和运行。
为什么?运行时中是否存在错误,或者(更有可能)是否有我没有看到此异常的原因 - 在这种情况下,编译器不应该阻止我?
有趣的是,如果通过将约束从类移动到接口来反转场景......
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 以外的任何其他内容,这不会编译。我觉得这样做有点肮脏,因为我不确定它是否故意这样工作。
会导致 TypeLoadException 和 使用泛型接口的不可验证代码和具有类型参数约束的泛型方法。 不过,我不清楚这是 C# 错误还是 CLR 错误。
[由OP添加:]
以下是Microsoft在您链接到的第二个线程中所说的内容(我的强调):
两者之间存在不匹配 运行时使用的算法和 C# 编译器,用于确定是否一组 约束与另一个约束一样强 设置。这种不匹配会导致 C# 编译器接受某些构造 运行时拒绝的,并且 结果是 TypeLoadException you 看。我们正在调查以确定 如果此代码是 这个问题。无论如何,它是 当然不是"设计",即 编译器接受这样的代码 导致运行时异常。
问候
Ed Maurer C# 编译器开发 铅
从我加粗的部分来看,我认为他说这是一个编译器错误。那是在2007年。我想这还不够严重,不足以成为他们解决它的优先事项。
唯一的解释是约束被视为方法声明的一部分。这就是为什么在第一种情况下它是编译器错误的原因。
编译器在使用时没有收到错误object
...嗯,这是编译器的错误。
其他"约束"具有与泛型约束相同的属性:
interface I
{
object M();
}
class C
{
public some_type M() { return null; }
}
class D : C, I
{
}
我可以问:为什么这不起作用?
你看?这和你的问题是一样的。用some_type
实现object
是完全有效的,但运行时和编译器都不会接受它。
如果您尝试生成 MSIL 代码,并强制实现我的示例,运行时将抱怨。
隐式接口实现要求方法声明上的泛型约束是等效的,但在代码中不一定完全相同。此外,泛型类型参数具有隐式约束"其中 T : 对象"。这就是为什么指定C<Object>
编译时,它会导致约束等效于接口中的隐式约束。(C# 语言规范的第 13.4.3 节)。
您还正确,使用调用受约束方法的显式接口实现将起作用。它提供了从接口方法到类中的实现的非常清晰的映射,其中约束不能不同,然后继续调用名称相似的泛型方法(现在与接口无关)。此时,可以使用与任何泛型方法调用相同的方式解决辅助方法上的约束,而不会产生任何接口解析问题。
在第二个示例中,将约束从类移动到接口更好,因为默认情况下,类将从接口获取其约束。这也意味着您必须在类实现中指定约束(如果适用)(对于 Object 则不适用)。传递I<string>
意味着不能在代码中直接指定该约束(因为字符串是密封的),因此它必须是显式接口实现的一部分,或者是与两个位置的约束相等的泛型类型。
据我所知,运行时和编译器使用单独的验证系统进行约束。编译器允许这种情况,但运行时验证程序不喜欢这种情况。我想强调的是,我不确定为什么它有问题,但我猜它不喜欢该类定义中不满足接口约束的潜力,这取决于 T 最终被设置的内容。如果其他人对此有明确的答案,那就太好了。
响应基于接口的代码段:
interface I<T1>
{
void Foo<T2>() where T2 : T1;
}
class C : I<string> // compiler error CS0425
{
public void Foo<T>() { }
}
我认为问题在于编译器认识到:
- 您尚未在 C.Foo() 上声明必要的类型约束。
- 如果选择字符串作为类型,则 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
{
}
为了表明字符串类型没有以任何方式被特殊对待,只需将密封关键字添加到上面的 MyClass 声明中,以便在 C.Foo() 上将 T1 指定为字符串和字符串作为类型约束时看到它以相同的方式失败。
public sealed class MyClass
{
}
这是因为字符串是密封的,不能构成约束的基础。