这是一个简单的泛型类型,其唯一的泛型参数约束为引用类型:
class A<T> where T : class
{
public bool F(T r1, T r2)
{
return r1 == r2;
}
}
csc.exe生成的 IL 是:
ldarg.1
box !T
ldarg.2
box !T
ceq
因此,在进行比较之前,每个参数都是装箱。
但是如果约束表明“T”永远不应该是值类型,那么为什么编译器会尝试将r1
和r2
打包?
答案 0 :(得分:43)
需要满足生成的IL的可验证性约束。请注意,无法验证的并不一定意味着不正确。只要其安全上下文允许运行无法验证的代码,它就可以在没有box
指令的情况下正常工作。验证是保守的,基于固定的规则集(如可达性)。为简化起见,他们选择不关心验证算法中是否存在泛型类型约束。
Common Language Infrastructure Specification (ECMA-335)
第9.11节:通用参数的约束
... 泛型参数的约束仅限制泛型参数的类型 可以用。实例化。 验证(参见第III部分)要求使用字段,属性或方法 已知通用参数通过满足约束提供,但不能 通过通用参数直接访问/调用,除非它是第一次装箱 (参见第III部分)或
callvirt
指令以constrained
前缀指令为前缀。 ...
删除box
说明会导致无法验证的代码:
.method public hidebysig instance bool
F(!T r1,
!T r2) cil managed
{
ldarg.1
ldarg.2
ceq
ret
}
c:\Users\Mehrdad\Scratch>peverify sc.dll
Microsoft (R) .NET Framework PE Verifier. Version 4.0.30319.1
Copyright (c) Microsoft Corporation. All rights reserved.
[IL]: Error: [c:\Users\Mehrdad\Scratch\sc.dll : A`1[T]::F][offset 0x00000002][fo
und (unboxed) 'T'] Non-compatible types on the stack.
1 Error(s) Verifying sc.dll
更新(对评论的回答):正如我上面提到的,可验证性并不等同于正确性(这里我从类型安全的角度谈论“正确性”)。 可验证的程序是正确的程序的严格子集(即所有可验证的程序都是明确正确的,但是有正确的程序是不可验证的)。因此,可验证性是一种比正确性更强的属性。由于C#是一种图灵完备语言,Rice's theorem表明在一般情况下证明程序正确是不可判定的。
让我们回到我的可达性类比,因为它更容易解释。假设您正在设计C#。有一点想到的是什么时候发出关于无法访问的代码的警告,并在优化器中完全删除那段代码,但是你将如何检测所有无法访问的代码?赖斯的定理再一次说你不能为所有程序做到这一点。例如:
void Method() {
while (true) {
}
DoSomething(); // unreachable code
}
这是C#编译器实际警告的内容。但它没有警告:
bool Condition() {
return true;
}
void Method() {
while (Condition()) {
}
DoSomething(); // no longer considered unreachable by the C# compiler
}
人类可以证明在后一种情况下控制流量永远不会到达那条线。有人可能会争辩说,在这种情况下,编译器可以静态证明DoSomething
无法访问,但事实并非如此。为什么?关键是你不能为所有程序做到这一点,所以你应该在某个点画线。在此阶段,您必须定义可判定的属性并将其命名为“reachability”。例如,为了实现可达性,C#坚持使用常量表达式,并且根本不会查看函数的内容。分析的简单性和设计的一致性是决定在哪里划线的重要目标。
回到我们的可验证性概念,这是一个类似的问题。与正确性不同,可验证性是一种可判定的属性。作为运行时设计器,您必须根据性能考虑因素,易于实现,易于规范,一致性来决定如何定义可验证性,从而使编译器可以轻松地生成可验证的代码。像大多数设计决策一样,它涉及很多权衡。最终,CLI设计人员决定在检查可验证性时,他们根本不会考虑通用约束。
答案 1 :(得分:15)
Mehrdad的回答非常好;我只想补充几点:
首先,是的,在这种情况下,这只是为了让验证者高兴。抖动当然应该优化掉装箱指令,因为装箱参考类型没有意义。
然而,有些情况下保持验证者的快乐,我们必须引入未经优化的拳击指令。例如,如果你说:
class B<T> { public virtual void M<U>(U u) where U : T {...} }
class D : B<int>
{
public override void M<U>(U u)
{
C#编译器知道在D.M中,U只能是int。然而,为了可以验证,有些情况下你必须装箱对象,然后取消装箱到int。抖动并不总能优化掉这些;我们已经向抖动团队指出,这是一种可能的优化,但情况是如此模糊,以至于不太可能为许多实际客户带来巨大的胜利。他们可以花时间进行更大胆的优化。