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

发布于 2024-11-03 21:31:23 字数 996 浏览 6 评论 0原文

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

我有代码

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

Main 方法编译为:

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

My question is somewhat related to this one: How does a generic constraint prevent boxing of a value type with an implicitly implemented interface?, but different because it shouldn't need a constraint to do this because it's not generic at all.

I have the code

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

The main method compiles to this:

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

Why doesn't it compile to this?

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

I see why you need a method table to make a virtual call, but you don't need to make a virtual call in this case. If the interface is implemented normally it doesn't make a virtual call.

Also related: Why are explicit interface implementations private? - the existing answers on this question don't adequately explain why the methods are marked as private in the metadata (rather than merely having unusable names). But even this doesn't fully explain why it's boxed, since it still boxes when called from inside C.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

_畞蕅 2024-11-10 21:31:23

我认为答案就在如何处理接口的 C# 规范中。从规格来看:

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

在下面的表格下,它表示接口

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

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

** 编辑 **

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

拳击转换意味着进行
被装箱的值的副本。这是
与 a 的转换不同
类型对象的引用类型,在
该值继续引用
同一个实例,只是
被视为衍生较少的类型
对象。

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

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 执行装箱,那么您会期望程序在调用 F() 时为 GetI() 的两次调用都写出 4 > 四次。然而我们得到的实际结果是1和2。原因是拳击已经复制了。

这表明我们对值进行装箱和不对值进行装箱之间存在差异

I think the answer is in the C# specification of how interfaces can be treated. From the Spec:

There are several kinds of
variables in C#, including fields,
array elements, local variables, and
parameters. Variables represent
storage locations, and every variable
has a type that determines what
values can be stored in the variable,
as shown by the following table.

Under the table that follows it says for an Interface

A null reference, a reference to an instance of a class type that
implements that interface type, or a
reference to a boxed value of a value
type that implements that interface
type

It says explicitly that it will be a boxed value of a value type. The compiler is just obeying the specification

** Edit **

To add more information based upon the comment. The compiler is free to rewrite if it has the same effect but because the boxing occurs you make a copy of the value type not have the same value type. From the specification again:

A boxing conversion implies making a
copy of the value being boxed. This is
different from a conversion of a
reference-type to type object, in
which the value continues to reference
the same instance and simply is
regarded as the less derived type
object.

This means it has to do the boxing every time or you'd get inconsistent behavior. A simple example of this can be shown by doing the following with the provided program:

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

I added an internal member to struct C that is incremented by 1 every time that F() is called on that object. This lets us see what is happening to the data of our value type. If boxing was not performed on x then you would expect the program to write out 4 for both calls to GetI() as we call F() four times. However the actual result we get is 1 and 2. The reason is that the boxing has made a copy.

This shows us that there is a difference between if we box the value and if we don't box the value

×眷恋的温暖 2024-11-10 21:31:23

该值不一定被装箱。 C# 到 MSIL 的转换步骤通常不会执行大多数很酷的优化(出于某些原因,至少其中一些是非常好的优化),因此您可能仍然会看到 指令,如果你查看 MSIL,但 JIT 有时可以合法地删除实际分配(如果它检测到它可以逃脱它)。从 .NET Framework 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;
    }
}

The value doesn't necessarily get boxed. The C#-to-MSIL translation step usually doesn't do most of the cool optimizations (for a few reasons, at least some of which are really good ones), so you'll likely still see the box instruction if you look at the MSIL, but the JIT can sometimes legally elide the actual allocation if it detects that it can get away with it. As of .NET Framework 4.7.1, it looks like the developers never invested in teaching the JIT how to figure out when this was legal. .NET Core 2.1's JIT does this (not sure when it was added, I just know that it works in 2.1).

Here are the results from a benchmark I ran to prove it:

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 |

Benchmark source code:

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;
    }
}
止于盛夏 2024-11-10 21:31:23

问题在于,不存在“只是”接口类型的值或变量。相反,当尝试定义这样的变量或转换为这样的值时,使用的实际类型实际上是“实现接口的对象”。

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

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

The problem is that there's no such thing as a value or variable which is "just" an interface type; instead, when an attempt is made to define to such a variable or cast to such a value, the real type that is used is, effectively, "an Object that implements the interface".

This distinction comes into play with generics. Suppose a routine accepts a parameter of type T where T:IFoo. If one passes such a routine a struct which implements IFoo, the passed-in parameter won't be a class type that inherits from Object, but will instead be the appropriate struct type. If the routine were to assign the passed-in parameter to a local variable of type T, the parameter would be copied by value, without boxing. If it were assigned to a local variable of type IFoo, however, the type of that variable would be "an Object that implements IFoo", and thus boxing would be required that that point.

It may be helpful to define a static ExecF<T>(ref T thing) where T:I method which could then invoke the I.F() method on thing. Such a method would not require any boxing, and would respect any self-mutations performed by I.F().

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文