为什么在值类型上调用显式接口实现会导致它被装箱?

Ran*_*832 15 .net c# boxing interface explicit-interface

我的问题与此问题有些相关:泛型约束如何阻止使用隐式实现的接口对值类型进行装箱?,但不同,因为它不需要约束来执行此操作,因为它根本不是通用的.

我有代码

interface I { void F(); }
struct C : I { void I.F() {} }
static class P {
    static void Main()
    {    
        C x;
        ((I)x).F();
    }
}
Run Code Online (Sandbox Code Playgroud)

主要方法编译如下:

IL_0000:  ldloc.0
IL_0001:  box        C
IL_0006:  callvirt   instance void I::F()
IL_000b:  ret
Run Code Online (Sandbox Code Playgroud)

为什么不编译到这个?

IL_0000:  ldloca.s   V_0
IL_0002:  call       instance void C::I.F()
IL_0007:  ret
Run Code Online (Sandbox Code Playgroud)

我明白为什么你需要一个方法表来进行虚拟调用,但在这种情况下你不需要进行虚拟调用.如果接口正常实现,则不进行虚拟呼叫.

还相关:为什么显式接口实现是私有的? - 关于这个问题的现有答案没有充分解释为什么这些方法在元数据中被标记为私有(而不是仅仅具有不可用的名称).但即使这样也没有完全解释为什么它是盒装的,因为从C里面调用时它仍然是盒子.

Cra*_*nec 8

我认为答案是关于如何处理接口的C#规范.来自规格:

C#中有几种变量,包括字段,数组元素,局部变量和参数.变量表示存储位置,每个变量都有一个类型,用于确定可以在变量中存储的值,如下表所示.

在接下来的表格中,它表示接口

空引用,对实现该接口类型的类类型实例的引用,或对实现该接口类型的值类型的盒装值的引用

它明确表示它将是值类型的盒装值.编译器只是遵守规范

**编辑**

根据评论添加更多信息.如果编译器具有相同的效果,则编译器可以自由重写,但由于发生限制,您将使值类型的副本不具有相同的值类型.再次从规范:

装箱转换意味着制作装箱值的副本.这与reference-type到type对象的转换不同,其中值继续引用相同的实例,并且简单地被视为较少派生的类型对象.

这意味着它必须每次都进行拳击,否则你会得到不一致的行为.使用提供的程序执行以下操作可以显示一个简单的示例:

public interface I { void F(); }
public struct C : I {
    public int i;
    public void F() { i++; } 
    public int GetI() { return i; }
}

    class P
    {
    static void Main(string[] args)
    {
        C x = new C();
        I ix = (I)x;
        ix.F();
        ix.F();
        x.F();
        ((I)x).F();
        Console.WriteLine(x.GetI());
        Console.WriteLine(((C)ix).GetI());
        Console.ReadLine();
    }
}
Run Code Online (Sandbox Code Playgroud)

我添加了一个struct的内部成员,C每次F()在该对象上调用时,该成员都会增加1 .这让我们可以看到我们的值类型数据发生了什么.如果没有执行拳击x那么你会期望程序在两次调用时写出4,GetI()因为我们调用了F()四次.然而,我们得到的实际结果是1和2.原因是拳击已经复制了.

这向我们表明,如果我们选中该值并且如果我们不设置该值,则存在差异

  • 编译器可以自由地应用产生相同可观察行为的优化.然后,没有什么说JIT不会那样做.但我对此表示怀疑.我认为这是一个非常明显的优化,任何自尊的现代编译器**都应该**(强制执行!)执行.但我只是一个卑微的程序员. (2认同)
  • @Konrad我已经更新了答案,并解释了为什么它不是同一件事并且编译器无法执行此“优化”。 (2认同)