原则23:理解接口方法和虚函数的区别

优质
小牛编辑
138浏览
2023-12-01

咋一看,实现接口和重载一个虚函数似乎是一样的。都是定义一个在另一个类中声明的成员。第一眼的感觉是很有欺骗性的。实现接口和重载虚函数是非常不同的。在接口声明的成员是非虚的——至少不是默认的。子类不能重载基类实现的接口的成员。接口可以显示实现,可以把它们中 public 接口中隐藏。它们的概念不同而且使用也不同。

但是你可以这样实现接口以至于子类可以修改你的实现。你只需要对子类做一个 hook 就行了。

为了说明它们的不同,定义一个简单的几块和它的实现类:

interface IMsg 
{
    void Message(); 
}
public class MyClass : IMsg 
{
    public void Message() 
    {
        Console.WriteLine("MyClass"); 
    }
}

Message() 方法是 MyClass 类公有接口的一部分。Message 也可以通过 IMsg 指针访问,它是 MyClass 类型的一部分。现在通过添加子类变得更复杂:

public class MyDerivedClass : MyClass 
{
    public void Message() 
    {
        Console.WriteLine("MyDerivedClass"); 
    }
}

注意到我不得不添加 new 关键字用以区别之前的 Message 方法(查看原则33)(译者注:这应该是第一版的叙述)。 MyClass.Message() 是非虚的。子类不能提供重载的 Message 版本。 MyClass 创建了新的 Message 方法,但是这个方法没有重载 MyClass.Message : 它会被隐藏。更重要的是,MyClass.Message 仍然可通过 IMsg 引用访问:

MyDerivedClass d = new MyDerivedClass();
d.Message(); // prints "MyDerivedClass". 
IMsg m = d as IMsg;
m.Message(); // prints "MyClass"

接口的方法是非虚的。当你实现接口,你就在这个类中声明这个合约的具体的实现。

但是你经常想要创建接口,在基类实现它们,并且在子类修改它们的行为。你确实可以做到。你有两种选择。要是你不能接触到基类,你可以在子类中重新实现接口:

public class MyDerivedClass : MyClass 
{
    public new void Message() 
    {
        Console.WriteLine("MyDerivedClass"); 
    }
}

新增的关键字使得 IMsg 改变行为子类的行为以至于 IMsge.Message 可以调用子类的版本:

MyDerivedClass d = new MyDerivedClass();
d.Message(); // prints "MyDerivedClass". 
IMsg m = d as IMsg;
m.Message(); // prints " MyDerivedClass "

如果你仍然使用 new 关键字在 MyDerivedClass.Message 方法上。给你个提示:仍然还会有问题(查看原则33)。子类的版本仍然可以通过子类的引用访问到:

MyDerivedClass d = new MyDerivedClass();
d.Message(); // prints "MyDerivedClass".
IMsg m = d as IMsg;
m.Message(); // prints "MyDerivedClass" 
MyClass b = d;
b.Message(); // prints "MyClass"

修复这个问题方法是修改基类,声明接口方法为 virtual :

public class MyClass : IMsg 
{
    public virtual void Message() 
    {
        Console.WriteLine("MyClass"); 
    }
}
public class MyDerivedClass : MyClass 
{
    public override void Message() 
    {
        Console.WriteLine("MyDerivedClass"); 
    }
}

MyDerivedClass——其他所有继承自 MyClass ——都可以声明它们自己的 Message() 方法。重载的版本总是会被调用:无论是通过 MyDerivedClass 引用,还是通过 IMsg 引用,或者是通过 MyClass 引用。

要是你不喜欢虚函数的掺杂,你只需要在定义 MyClass 上定义做一个小的变化:

public abstract class MyClass : IMsg 
{
    public abstract void Message(); 
}

是的,你可以实现接口却没有实际实现这个接口的方法。通过声明接口方法的 abstract 版本,你就是声明继承的子类都必须实现这个接口。 IMsg 是 MyClass 声明的一部分,但是定义的方法被延迟到子类中实现。

子类可以防止进一步的重载的密封方法:

public class MyDerivedClass2 : MyClass 
{
    public sealed override void Message() 
    {
        Console.WriteLine("MyDerivedClass"); 
    }
}

另一个解决方案是实现这个的接口中调用一个虚方法,以让子类加入接口的合约中。你可以在 MyClass 中这样做:

public class MyClass2 : IMsg 
{
    protected virtual void OnMessage() 
    { 
    }
    public void Message() 
    {
        OnMessage();
        Console.WriteLine("MyClass");
    } 
}

任何子类重载 OnMessage() 添加它们自己的工作到声明在 MyClass2 的 Message() 方法中。这个模式你在前面类实现 IDisposable 中见过 (查看原则17)。

显式接口实现(查看原则31)使你能够实现 一个接口,也可以隐藏你的类的公共接口。它的使用实现接口和重载虚函数变得不那么清晰。你可以使用显示接口实现限制使用者可以有访问更多的接口方法版本。 IComarable 习惯会在原则31详细展示这点。

还有最后一个添加接口和基类一起工作的惊喜。基类可以提供接口中方法的默认实现。然后,子类可以声明实现这个接口并从基类中继承这个接口的实现,正如下面例子一样。

public class DefaultMessageGenerator 
{
    public void Message() 
    {
        Console.WriteLine("This is a default message"); 
    }
}
public class AnotherMessageGenerator :DefaultMessageGenerator, IMsg 
{
    // No explicit Message() method needed. 
}

注意到子类可以声明接口是其的一部分合约,即使它没有提供任何 IMsg 方法的实现。只要它由恰当的公有可访问签名的方法,那么满足接口的合约。使用这个方法,你可以不用显示接口实现。

实现接口比创建和重载虚函数有更多选择。你可以创建 sealed 实现,虚实现,或者是类继承接口的抽象约束。你也可以创建 sealed 实现并提供一个虚函数调用来实现接口。你可以准确地决定怎样和什么时候子类修改你的基类实现的接口的默认行为。接口方法不是虚方法而是独立的合约。

小结:

这个原则没有大量枚举接口和继承各种组合使用的不同,那都是专牛角尖的人才去干的,而是用原理上梳理了下两者的不同,当然也有点坑需要记住的:接口的显示实现会隐藏子类的实现,添加 new 关键字可以解决这个问题,但是还不是多态。