3

The only thing I can think of is as follows, which is far from ideal:

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

Is there a more straightforward way to do this?

(Related and how I got to this question: Calling C# interface default method from implementing class)

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
Velocirobtor
  • 188
  • 7
  • 2
    @GSerg But casting a valuetype to an interface will box it, which is the case here. – Velocirobtor Sep 06 '19 at 19:27
  • 2
    I overloooked the `struct`. You are [correct](https://stackoverflow.com/q/3032750/11683). Still, the cast is required. – GSerg Sep 06 '19 at 19:29
  • @GSerg Ah ok, that's unfortunate. I had hoped to (ab)use DIMs as an alternative for the missing inheritance with structs, but if there's this much overhead involved I guess I'll leave it be. Anyways, thanks for the answer. – Velocirobtor Sep 06 '19 at 19:50
  • I haven't dug into the new feature yet, but what about not implementing the method at all? Then `bazInstance.Foo()` should call the method with no box. – Joel Coehoorn Sep 06 '19 at 19:50
  • @JoelCoehoorn That [doesn't happen](https://stackoverflow.com/q/57761799/11683). – GSerg Sep 06 '19 at 19:57
  • This is possibly a duplicate of [Why does calling an explicit interface implementation on a value type cause it to be boxed?](https://stackoverflow.com/questions/5812099/why-does-calling-an-explicit-interface-implementation-on-a-value-type-cause-it-t). As [some of the answers](https://stackoverflow.com/a/51338291/134204) explain, the struct isn't *always* boxed. In fact, one of the comments from Andy Ayers explains that the JIT can optimize boxing in many cases – Panagiotis Kanavos Sep 09 '19 at 10:46
  • @Velocirobtor how did you determine that the cast boxes? Did you use BenchmarkDotNet in a release configuration as the possible duplicate shows? – Panagiotis Kanavos Sep 09 '19 at 10:50
  • @Velocirobtor there are no allocations !? – Panagiotis Kanavos Sep 09 '19 at 10:59

2 Answers2

3

I don't think there are any allocations. This answer to this possibly duplicate question explains that the JIT compiler can avoid boxing in many cases, including explicit interface implementation calls. Andy Ayers from the JIT team verified this in a comment and provided a link to the PR that implemented this.

I adapted the code in that answer :

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

}

The results don't show any allocations :


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 |     - |     - |     - |         - |

I changed the return type to an int just like the linked answer to ensure the method won't be optimized away

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Neat, didn't know that .NET core did those optimizations. I actually didn't have a C#8 ready compiler handy, so I tested it with implicit implementations and .NET framework. Sadly that one doesn't optimize away the boxing. I'll have to play around with it a little, to see if the optimization is guaranteed. – Velocirobtor Sep 09 '19 at 11:17
  • Thanks for finding the answer. You're probably right about the duplicate, if DIMs behave the same as implicit interface implementations. – Velocirobtor Sep 09 '19 at 11:18
  • @Velocirobtor .NET Core and .NET ... Old are very different. The linked question shows that explicit interface implementations get optimized since Core 2.1. DIMs depend on Core 2.0 runtime features and I suspect many of them deal with optimizations. – Panagiotis Kanavos Sep 09 '19 at 11:19
  • As noted above some boxes can be elided by the jit, provided the box creation and consumption are both visible to the jit, and the box is not `dup`d in the IL... in Core the async state machine core logic now depends on this. Some of these opts have made it back to full framework, 4.8 for instance should be able to do this too. Still [a lot](https://github.com/dotnet/coreclr/issues/14472) we can do to improve here... – Andy Ayers Sep 09 '19 at 18:35
0

Haven't set myself up for c# 8.0 yet, so I'm not sure this'll work, but here's an idea you could try:

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

    public IBar AsBar()
    {
        return this;
    }
}
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
John Wu
  • 50,556
  • 8
  • 44
  • 80
  • I don't object to the downvote, but downvoter, please help me understand what the problem is so I can learn. – John Wu Sep 06 '19 at 23:32
  • I didn't downvote, but you're inheriting from a non-interface which isn't valid (unless it is in C# 8.0). – Wai Ha Lee Sep 07 '19 at 00:32
  • In the OP's example. `Bar` is the name of an interface. Personally I would have called it `IBar`. – John Wu Sep 07 '19 at 00:34
  • Hmm. Then it might be because the `AsBar` method is still boxing to the interface. – Wai Ha Lee Sep 07 '19 at 00:35
  • 1
    I can't speak for the downvoter, but I would guess that they found two things wrong with your answer, at least: 1) your answer is speculative, while authors of questions deserve _real_ answers, sure to solve their problem, and 2) your answer looks to me like it probably winds up boxing the `Baz` value anyway, which is exactly what the OP was trying to avoid. – Peter Duniho Sep 07 '19 at 02:25
  • Nice idea. Unfortunately Wai Ha Lee is right and this solution still boxes. (But you're absolutely right about the interface name, I will update that) – Velocirobtor Sep 07 '19 at 02:28