当我在从接口派生的类中调用一个具有默认实现的接口时,为什么需要将'this'转换为接口类型?(问题涉及C# 8.0)

8

我有一个使用C# 8和.NET Core 3.1编写的简单控制台程序:

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

        }
    }
}

我不明白为什么在这行代码中需要进行强制类型转换:((ITest)(this)).test();

因为Test直接继承自ITest,所以根据定义,“this”就是ITest。

谢谢。

3个回答

11

默认接口实现与显式实现类似:它们只能通过接口类型调用,而不能通过实现类型调用。

要理解这是为什么,想象一下Test实现了两个具有相同方法签名的接口;如果没有强制转换,将使用哪个?

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

以上语法确实是被允许的,但在多个接口的情况下会生成编译错误,这与默认接口方法引入的主要动机相矛盾:

默认接口方法使API作者能够在未来版本中向接口添加方法,而不会破坏该接口现有实现的源代码或二进制兼容性。

如果Test已经实现了ITestITest2,那么如果后来将test添加到ITest2中,这将构成一种破坏性变化。


3
这种歧义在我的示例中不存在。但对于你的示例,像“ITest.test();”和“ITest2.test();”这样的语法将消除歧义,同时编译器可以检查错误。如果使用强制转换,您可能会尝试将其转换为类没有继承的接口,从而创建运行时错误而不是编译时错误。 - user1777224
1
@user1777224 语法 ITest.test(); 已经用于静态接口方法。虽然在您的特定示例中没有歧义,但编译器需要能够解释每种情况,包括许多边缘情况。直接调用 test() 安全的唯一情况是只实现了一个接口;在 .NET 中,这就是类仅允许单一继承的原因,但接口应始终允许共存而不必担心冲突。 - Johnathan Barclay
@JohnathanBarclay 说实话,这就是编译器错误/警告的作用。它没有问题在其他上下文中提醒我模棱两可的地方。 - arkon
@arkon 你正从接口使用者的角度进行思考,但接口的作者呢?引入默认接口方法的主要原因之一是为了防止破坏任何实现代码。如果在Test实现了ITest2之后,ITest2添加了test方法会怎样呢? - Johnathan Barclay
我理解你的意思,但我在自问:你认为@Charles的解决方案怎么样? - Master DJon
@MasterDJon 这样做没有任何问题。只是需要注意每个默认成员都需要一个扩展方法,并且在实现类型上定义的任何GetMode方法都会有效地隐藏扩展方法。 - Johnathan Barclay

3
这种行为在此处有记录
从C# 8.0开始,您可以为接口中声明的成员定义实现。如果一个类继承了一个接口的方法实现,那么该方法只能通过接口类型的引用访问。继承的成员不会出现在公共接口中。以下示例为接口方法定义了默认实现:
public interface IControl
{
    void Paint() => Console.WriteLine("Default Paint method");
}
public class SampleClass : IControl
{
    // Paint() is inherited from IControl.
}

以下示例调用默认实现:
var sample = new SampleClass();
//sample.Paint();// "Paint" isn't accessible.
var control = sample as IControl;
control.Paint();

任何实现IControl接口的类都可以重写默认的Paint方法,可以作为公共方法,也可以作为显式接口实现。

3
所有的都是正确的,但我不确定这解释了为什么会这样,这才是 OP 所问的。 - Johnathan Barclay
答案基本上就是,默认实现的接口方法作为显式的接口成员来表现。这就是为什么它们在类内部是隐藏的,除非你明确指定接口。 - Philip Stuyck

2

有趣的是,如果你创建了一个扩展方法,则不再需要强制转换。

我在我的项目中刚刚做了以下操作:

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

在我的情况下,模式是在IXpObject中定义的默认实现。
现在我可以使用obj.GetMode()而无需将对象转换为接口。


1
如果有两个具有相同扩展的接口,它只会给出编译时错误,我不明白为什么默认方法不能以同样的方式工作。 - kamyker
@kamyker 因为默认接口成员应该在不破坏现有代码的情况下安全地添加。这是它们引入的主要原因之一。如果由于冲突而存在编译时错误的可能性,那么这完全违背了其目的。 - Johnathan Barclay

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