38

这是一个简单的泛型类型,其唯一的泛型参数受限于引用类型:

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”永远不应该是一个值类型,为什么编译器要装箱r1r2

4

2 回答 2

45

它需要满足生成的 IL 的可验证性约束。请注意,unverifiable不一定意味着不正确box只要其安全上下文允许运行无法验证的代码,它就可以在没有指令的情况下正常工作。验证是保守的,并且基于固定的规则集(如可达性)。为了简化事情,他们选择不关心验证算法中泛型类型约束的存在。

公共语言基础设施规范 (ECMA-335)

第 9.11 节:对泛型参数的约束

...对泛型参数的约束仅限制可以实例化泛型参数的类型。 验证(参见第三部分)要求已知泛型参数通过满足约束提供的字段、属性或方法不能通过泛型参数直接访问/调用,除非它首先被装箱 (参见第三部分)或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# 是图灵完备的语言,赖斯定理指出,证明程序是正确的在一般情况下是不可判定的。

让我们回到我的可达性类比,因为它更容易解释。假设您正在设计 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是不可达的,但事实并非如此。为什么?关键是你不能对所有程序都这样做,所以你应该在某个点画线。在这个阶段,您必须定义一个可判定的属性并将其称为“可达性”。例如,为了可访问性,C# 坚持使用常量表达式,根本不会查看函数的内容。分析的简单性和设计的一致性是决定在哪里划线的重要目标。

回到我们的可验证性概念,这是一个类似的问题。与正确性不同,可验证性是可判定的属性。作为运行时设计者,您必须决定如何定义可验证性,基于性能考虑、易于实现、易于规范、一致性,使编译器可以轻松自信地生成可验证代码。像大多数设计决策一样,它涉及很多权衡。最终,CLI 设计人员决定,在检查可验证性时,他们根本不想过多关注通用约束。

于 2010-12-18T00:57:09.717 回答
16

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# 编译器知道在 DM 中,U 只能是 int。然而,为了可验证,有些情况下必须将 u 装箱为 object,然后将其拆箱为 int。抖动并不总是优化掉这些;我们已经向 jitter 团队指出,这是一种可能的优化,但情况如此模糊,以至于不太可能为许多实际客户带来巨大的胜利。他们可以花时间进行一些物有所值的优化。

于 2010-12-18T15:28:45.517 回答