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

本文关键字:类型 调用 显式接口实现 为什么 | 更新日期: 2023-09-27 17:53:14

我的问题与此有一定关系:泛型约束如何防止使用隐式实现的接口对值类型进行装箱?,但不同的是,它不需要约束,因为它根本不是通用的。

我有代码

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

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

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

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

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

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

下表显示接口

null引用,对类类型实例的引用实现该接口类型,或者对值的装箱值的引用实现该接口的类型型

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

**编辑**

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

拳击转换意味着要装箱的值的副本。这是不同于引用类型到类型对象,在中该值继续引用相同的实例被认为是派生较少的类型对象

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

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

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

这向我们表明,如果我们装箱该值和不装箱值之间存在差异

该值不一定会被装箱。C#到MSIL的转换步骤通常不会进行大多数很酷的优化(出于一些原因,至少其中一些是非常好的(,所以如果你查看MSIL,你可能仍然会看到box指令,但如果JIT检测到它可以逃脱惩罚,它有时可以合法地取消实际分配,看起来开发人员从未投资于教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;
    }
}

问题是,没有值或变量"只是"一种接口类型;相反,当试图定义为这样的变量或强制转换为这样的值时,实际使用的类型实际上是"实现接口的Object"。

这种区别在泛型中发挥作用。假设一个例程接受一个类型为T的参数,其中T:IFoo。如果向这样的例程传递一个实现IFoo的结构,那么传入的参数将不是从Object继承的类类型,而是适当的结构类型。如果例程将传入的参数分配给T类型的局部变量,则参数将按值复制,而不装箱。然而,如果它被分配给IFoo类型的局部变量,那么该变量的类型将是"实现IFooObject",因此在这一点上需要装箱。

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