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

时间:2011-04-27 23:57:33

标签: c# .net interface boxing explicit-interface

我的问题与此问题有些相关:How does a generic constraint prevent boxing of a value type with an implicitly implemented interface?,但不同,因为它不需要约束来执行此操作,因为它根本不是通用的。

我有代码

interface I { void F(); }
struct C : I { void I.F() {} }
static class P {
    static void Main()
    {    
        C x;
        ((I)x).F();
    }
}

主要方法编译如下:

IL_0000:  ldloc.0
IL_0001:  box        C
IL_0006:  callvirt   instance void I::F()
IL_000b:  ret

为什么不编译到这个?

IL_0000:  ldloca.s   V_0
IL_0002:  call       instance void C::I.F()
IL_0007:  ret

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

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

3 个答案:

答案 0 :(得分:8)

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

  

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

在下面的表中,它表​​示接口

  

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

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

**编辑**

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

  

拳击转换意味着制作一个   盒装价值的副本。这是   不同于a的转换   reference-type to type object,in   值继续引用   相同的实例,简直就是   被认为是较少派生的类型   对象

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

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();
    }
}

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

这告诉我们,如果我们选中该值并且如果我们没有将值

打包,则存在差异

答案 1 :(得分:2)

问题在于,没有“只是”接口类型的值或变量这样的东西;相反,当尝试定义这样的变量或转换为这样的值时,所使用的实际类型实际上是“实现接口的Object”。

这种区别与仿制药一起发挥作用。假设例程接受T类型的参数T:IFoo。如果一个例程传递了一个实现IFoo的结构,那么传入的参数将不是从Object继承的类类型,而是相应的结构类型。如果例程将传入的参数分配给类型为T的局部变量,则该参数将按值复制,而不进行装箱。但是,如果它被分配给类型为IFoo的局部变量,那么该变量的类型将是“实现Object的{​​{1}}”,因此需要装箱那个点

定义静态IFoo方法可能会有所帮助,然后可以在ExecF<T>(ref T thing) where T:I上调用I.F()方法。这种方法不需要任何装箱,并且会尊重thing执行的任何自我突变。

答案 2 :(得分:2)

该值不会不必要装箱。从C#到MSIL的转换步骤通常不会执行大多数出色的优化(出于某些原因,至少其中一些确实是非常好的),因此,如果您仍然会看到box指令,您可以查看MSIL,但是JIT有时可以合法地取消实际分配,只要它检测到可以摆脱分配。从.NET Fat 4.7.1开始,开发人员似乎从未投资于教导JIT如何确定何时是合法的。 .NET Core 2.1的JIT可以做到这一点(不确定何时添加它,我只知道它可以在2.1中运行)。

这是我为证明该基准而得出的结果:

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i7-6850K CPU 3.60GHz (Skylake), 1 CPU, 12 logical and 6 physical cores
Frequency=3515626 Hz, Resolution=284.4444 ns, Timer=TSC
.NET Core SDK=2.1.302
  [Host] : .NET Core 2.1.2 (CoreCLR 4.6.26628.05, CoreFX 4.6.26629.01), 64bit RyuJIT
  Clr    : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3131.0
  Core   : .NET Core 2.1.2 (CoreCLR 4.6.26628.05, CoreFX 4.6.26629.01), 64bit RyuJIT


                Method |  Job | Runtime |     Mean |     Error |    StdDev |  Gen 0 | Allocated |
---------------------- |----- |-------- |---------:|----------:|----------:|-------:|----------:|
       ViaExplicitCast |  Clr |     Clr | 5.139 us | 0.0116 us | 0.0109 us | 3.8071 |   24000 B |
 ViaConstrainedGeneric |  Clr |     Clr | 2.635 us | 0.0034 us | 0.0028 us |      - |       0 B |
       ViaExplicitCast | Core |    Core | 1.681 us | 0.0095 us | 0.0084 us |      - |       0 B |
 ViaConstrainedGeneric | Core |    Core | 2.635 us | 0.0034 us | 0.0027 us |      - |       0 B |

基准源代码:

using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Attributes.Exporters;
using BenchmarkDotNet.Attributes.Jobs;
using BenchmarkDotNet.Running;

[MemoryDiagnoser, ClrJob, CoreJob, MarkdownExporterAttribute.StackOverflow]
public class Program
{
    public static void Main() => BenchmarkRunner.Run<Program>();

    [Benchmark]
    public int ViaExplicitCast()
    {
        int sum = 0;
        for (int i = 0; i < 1000; i++)
        {
            sum += ((IValGetter)new ValGetter(i)).GetVal();
        }

        return sum;
    }

    [Benchmark]
    public int ViaConstrainedGeneric()
    {
        int sum = 0;
        for (int i = 0; i < 1000; i++)
        {
            sum += GetVal(new ValGetter(i));
        }

        return sum;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static int GetVal<T>(T val) where T : IValGetter => val.GetVal();

    public interface IValGetter { int GetVal(); }

    public struct ValGetter : IValGetter
    {
        public int _val;

        public ValGetter(int val) => _val = val;

        [MethodImpl(MethodImplOptions.NoInlining)]
        int IValGetter.GetVal() => _val;
    }
}