15

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

我有代码

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 内部调用时它仍然装箱。

4

3 回答 3

8

我认为答案在于如何处理接口的 C# 规范。从规范:

C#中有多种变量,包括字段、数组元素、局部变量和参数。变量代表存储位置,每个变量都有一个类型,决定了变量可以存储哪些值,如下表所示。

在下面的表格下,它说的是一个接口

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

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

** 编辑 **

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

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

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

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那么您会期望程序为两次调用都写出 4,GetI()因为我们调用F()了四次。但是我们得到的实际结果是1和2。原因是拳击做了一个副本。

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

于 2011-04-28T00:35:27.490 回答
3

该值不一定会被装箱。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;
    }
}
于 2018-07-14T11:58:49.383 回答
2

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

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

ExecF<T>(ref T thing) where T:I定义一个静态方法可能会有所帮助,然后可以I.F()thing. 这种方法不需要任何装箱,并且会尊重I.F().

于 2011-12-23T18:34:13.710 回答