在不装箱的情况下从实现结构体调用C#接口默认方法

3
我所能想到的唯一办法如下,但这远非理想:
interface IBar {
    void Foo() => Console.WriteLine("Hello from interface!");
}

struct Baz : IBar {
    // compiler error
    void Test1() => this.Foo();

    // IIRC this will box
    void Test2() => ((IBar)this).Foo();

    // this shouldn't box but is pretty complicated just to call a method
    void Test3() {
        impl(ref this);

        void impl<T>(ref T self) where T : IBar
            => self.Foo();  
    }
}

有没有更简单的方法来做这件事?

(相关问题和我提出此问题的原因:从实现类调用C#接口默认方法)


2
@GSerg 但将值类型转换为接口会使其装箱,这在这种情况下是适用的。 - Velocirobtor
2
我忽略了 struct。你是正确的。尽管如此,类型转换是必需的。 - GSerg
@GSerg 噢,好吧,那很不幸。我曾希望使用DIMs作为结构体缺失继承的替代方案,但如果涉及这样多的开销,我想我还是算了。无论如何,感谢你的回答。 - Velocirobtor
我还没有深入研究新功能,但是不实现该方法怎么样?然后bazInstance.Foo()就应该无需盒子调用该方法。 - Joel Coehoorn
@Velocirobtor,你是如何确定转换箱的?是否像可能的重复问题所示,使用了BenchmarkDotNet进行了发布配置的测试? - Panagiotis Kanavos
显示剩余3条评论
2个回答

3
我认为没有任何分配。这个答案回答了这个可能是重复问题,解释了JIT编译器在许多情况下可以避免装箱的情况,包括显式接口实现调用。 JIT团队的Andy Ayers在评论中验证了这一点,并提供了 链接 这是实现这一点的PR。
我改编了那个答案中的代码:
using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace DIMtest
{
    interface IBar {
        int Foo(int i) => i;
    }

    struct Baz : IBar {
        //Does this box?        
        public int  Test2(int i) => ((IBar)this).Foo(i);
    }


    [MemoryDiagnoser, CoreJob,MarkdownExporter]
    public class Program
    {
        public static void Main() => BenchmarkRunner.Run<Program>();

        [Benchmark]
        public int ViaDIMCast()
        {
            int sum = 0;
            for (int i = 0; i < 1000; i++)
            {
                sum += (new Baz().Test2(i));
            }

            return sum;
        }
    }

}

结果未显示任何分配:

BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18956
Intel Core i7-3770 CPU 3.40GHz (Ivy Bridge), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.0.100-preview9-014004
  [Host] : .NET Core 3.0.0-preview9-19423-09 (CoreCLR 4.700.19.42102, CoreFX 4.700.19.42104), 64bit RyuJIT
  Core   : .NET Core 3.0.0-preview9-19423-09 (CoreCLR 4.700.19.42102, CoreFX 4.700.19.42104), 64bit RyuJIT

Job=Core  Runtime=Core  

|     Method |     Mean |    Error |   StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|----------- |---------:|---------:|---------:|------:|------:|------:|----------:|
| ViaDIMCast | 618.5 ns | 12.05 ns | 13.40 ns |     - |     - |     - |         - |

我将返回类型更改为int,就像链接答案一样,以确保该方法不会被优化删除。


很好,我不知道.NET Core可以进行这些优化。实际上,我手头没有准备好的C#8编译器,所以我用隐式实现和.NET Framework进行了测试。可惜后者无法优化掉装箱操作。我需要再尝试一下,看看是否可以保证优化。 - Velocirobtor
感谢您找到答案。如果DIMs的行为与隐式接口实现相同,那么您可能是正确的关于重复的问题。 - Velocirobtor
@Velocirobtor .NET Core和.NET... Old非常不同。链接的问题显示,自Core 2.1以来,显式接口实现得到了优化。DIMs依赖于Core 2.0运行时功能,我怀疑其中许多涉及优化。 - Panagiotis Kanavos
如上所述,JIT可以省略一些框,前提是框的创建和使用都对JIT可见,并且该框在IL中没有被“dup”。在Core中,异步状态机核心逻辑现在依赖于此。其中一些优化已经回到了完整的框架中,例如4.8也应该能够做到这一点。仍然有很多工作要做来改进这里... - Andy Ayers

0

我还没有为c# 8.0做好准备,所以我不确定这个会不会有效,但是这里有一个你可以尝试的想法:

struct Baz : IBar
{
    public void CallFoo()
    {
        this.AsBar().Foo();
    }

    public IBar AsBar()
    {
        return this;
    }
}

我不反对投票踩我,但投票者,请帮我理解问题所在,这样我就可以学习了。 - John Wu
我没有点踩,但你继承了一个非接口,这是无效的(除非在C# 8.0中)。 - Wai Ha Lee
在OP的例子中,“Bar”是一个接口的名称。个人而言,我会称之为“IBar”。 - John Wu
嗯。那可能是因为 AsBar 方法仍然将接口装箱。 - Wai Ha Lee
1
我不能代表那个点踩者说话,但我猜他们至少发现了你的回答有两个问题:1)你的回答是推测性的,而问题的提问者应该得到“真正”的答案,以确保解决他们的问题;2)在我看来,你的回答可能最终仍然会将“Baz”值装箱,这正是OP试图避免的。 - Peter Duniho
好主意。不幸的是,李伟夏是正确的,这个解决方案仍然有局限性。(但你关于接口名称的想法绝对正确,我会更新它) - Velocirobtor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接