繁体   English   中英

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

[英]Why does calling an explicit interface implementation on a value type cause it to be boxed?

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

我有代码

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 引用,对实现该接口类型的 class 类型的实例的引用,或对实现该接口类型的值类型的装箱值的引用

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

** 编辑 **

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

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

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

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中添加了一个内部成员,每次在 object 上调用F()时,该成员都会增加 1。 这让我们可以看到我们的值类型的数据发生了什么。 如果没有在x上执行装箱,那么您会期望程序在两次调用GetI()时都写出 4,因为我们调用了F()四次。 但是我们得到的实际结果是1和2。原因是拳击做了一个副本。

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

该值不一定会被装箱。 C# 到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;
    }
}

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

这种区别在 generics 中起作用。 假设例程接受T类型的参数,其中T:IFoo 如果将这样的例程传递给实现 IFoo 的结构,则传入的参数将不是从 Object 继承的 class 类型,而是适当的结构类型。 如果例程将传入的参数分配给类型为T的局部变量,则该参数将按值复制,而无需装箱。 但是,如果将其分配给IFoo类型的局部变量,则该变量的类型将是“实现IFooObject ”,因此此时需要装箱。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM