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

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

我有代码

interface I { void F(); } struct C : I { void IF() {} } 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::IF() IL_0007: ret 

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

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

我认为答案是关于如何处理接口的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(); xF(); ((I)x).F(); Console.WriteLine(x.GetI()); Console.WriteLine(((C)ix).GetI()); Console.ReadLine(); } } 

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

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

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

这种区别与generics有关。 假设例程接受T类型的参数,其中T:IFoo 。 如果通过这样的例程实现IFoo的结构,则传入的参数将不是从Objectinheritance的类类型,而是相应的结构类型。 如果例程将传入的参数分配给类型为T的局部变量,则参数将按值复制,而不进行装箱。 但是,如果它被分配给IFoo类型的局部变量,那么该变量的类型将是“一个实现IFooObject ”,因此需要对该点进行装箱。

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

该值不一定是盒装的。 C#-to-MSIL转换步骤通常不执行大多数很酷的优化(由于一些原因,至少其中一些是非常好的),所以如果你看一下,你仍然可能会看到box指令MSIL,但如果JIT检测到它可以逃脱,它有时可以合法地忽略实际分配。 从.NET Fat 4.7.1开始,看起来开发人员从未投资过如何判断JIT何时合法。 .NET Core 2.1的JIT就是这样做的(不知道什么时候添加它,我只知道它在2.1中工作)。

以下是我跑来certificate它的基准测试的结果:

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