Why do I need to cast 'this' to an interface with a default implementation in C# 8.0 when I call it in the class derived form that interface?
Asked Answered
C

3

8

I have this Simple Console program in .NET Core 3.1 with C# 8:

using System;

namespace ConsoleApp34
{

    public interface ITest
    {
        public void test()
        {
            Console.WriteLine("Bye World!");

        }
    }

    public class Test : ITest
    {
        public void CallDefault()
        {
            ((ITest)(this)).test();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            var t = new Test();
            t.CallDefault();

        }
    }
}

I don't understand why the cast is necessary in the line ((ITest)(this)).test();

Test is directly derived from ITest, so, by definition, 'this' IS ITest

Thank you.

Cree answered 14/8, 2020 at 12:51 Comment(0)
J
12

Default interface implementations work similarly to explicit implementations: they can only be called through the interface type, not the implementing type.

To understand why this is so, imagine Test implemented 2 interfaces with the same method signature; which would be used if there was no cast?

public interface ITest2
{
    public void test()
    {
        Console.WriteLine("Hello World!");
    }
}

public class Test : ITest, ITest2
{
    public void CallDefault()
    {
        test(); // Do we use ITest.test() or ITest2.test()?
    }
}

It is true that the above syntax could be allowed, and instead generate a compilation error in the case of multiple interfaces, however, this contradicts with the primary motivation behind the introduction of default interface methods:

Default interface methods enable an API author to add methods to an interface in future versions without breaking source or binary compatibility with existing implementations of that interface.

If Test already implemented ITest & ITest2, then test was added to ITest2 at a later date, this would constitute a breaking change.

Jeremie answered 14/8, 2020 at 12:56 Comment(6)
This ambiguity does not exist in my example. But for your example a syntax like 'ITest.test();' and 'ITest2.test();' would remove the ambiguity, while the compiler can check for mistakes. If you use a cast you could try to cast to an interface from which the class does not inherit creating a runtime error instead of a compile time error..Cree
@Cree The syntax ITest.test(); is already used for static interface methods. Although there is no ambiguity in your specific example, the compiler needs to be able to interpret every situation, including many edge cases. The only situation that calling test() directly would be safe is if only 1 interface was implemented; in .NET, this is the reason why classes only allow single inheritance, but interfaces should always be allowed to co-exits without any concern of conflict.Jeremie
@JohnathanBarclay Frankly, that's what compiler errors/warnings are for. It has no problem reminding me of ambiguity in other contexts.Embarkment
@Embarkment You're thinking from the perspective of the interface consumer, but what about the interface author? One of the primary reasons for introducing default interface methods was to prevent breaking any implementing code. What if test was added to ITest2 after it had already been implemented by Test?Jeremie
I understand what you are saying, but I'm asking myself: what do you think about the @Charles solution?Kaseykasha
@MasterDJon Nothing wrong with doing that. Just comes with the caveats that an extension method would be required for each default member, and any GetMode method defined on the implementing type would effectively hide the extension method.Jeremie
T
3

This behavior is documented here.

Beginning with C# 8.0, you can define an implementation for members declared in an interface. If a class inherits a method implementation from an interface, that method is only accessible through a reference of the interface type. The inherited member doesn't appear as part of the public interface. The following sample defines a default implementation for an interface method:

public interface IControl
{
    void Paint() => Console.WriteLine("Default Paint method");
}
public class SampleClass : IControl
{
    // Paint() is inherited from IControl.
}

The following sample invokes the default implementation:

var sample = new SampleClass();
//sample.Paint();// "Paint" isn't accessible.
var control = sample as IControl;
control.Paint();

Any class that implements the IControl interface can override the default Paint method, either as a public method, or as an explicit interface implementation.

Truancy answered 14/8, 2020 at 12:57 Comment(2)
All true, but I'm not sure this explains why this is the case, which is what the OP asked for.Jeremie
The answer is basically as simple as that the default implemented methods on the interface behave as explicit interface members. Which is why they are hidden inside the class unless you explicitly specify the interface.Garek
G
3

It's funny, if you create an extension method then no more cast is needed.

Just did the following in my project:

public static class IXpObjectExtensions
{
    public static Mode GetMode(this IXpObject obj) => obj.Mode;
}

Mode is defined with a default implementation in IXpObject in my case.
I can now use obj.GetMode() without casting the object to the interface.

Gains answered 8/12, 2022 at 6:52 Comment(2)
Ye and if there are 2 interfaces with same extension it just gives compile time error, I don't get why default methods couldn't work the same way.Guipure
@Guipure Because default interface members should be added safely without breaking existing code. That's one of the primary reasons for their introduction. If there was a chance of a compile time error because of a clash, that defeats the purpose entirely.Jeremie

© 2022 - 2024 — McMap. All rights reserved.