在基类和派生类中实现接口

3
在C#中,你能否举一个好的例子,说明为什么要在基类上实现接口,并在派生类上重新实现该接口,而不是将基类方法设置为虚拟方法。
例如:
interface IMakesNoise
{
  void Speak();
}

class Cat : IMakesNoise
{
  public void Speak()
  {
    Console.WriteLine("MEOW");
  }
}

class Lion : Cat, IMakesNoise
{
  public new void Speak()
  {
    Console.WriteLine("ROAR");
  }
}

测试行为:

Cat cat = new Cat();
Cat lion = new Lion();

// Non virtual calls, acts as expected    
cat.Speak();
lion.Speak();

// Grabbing the interface out is 'virtual' in that it grabs the most derived interface implementation
(cat as IMakesNoise).Speak();
(lion as IMakesNoise).Speak();

这将会打印出以下内容:
MEOW
MEOW
MEOW
ROAR

更新:为了更好地解释“为什么”,原因是我正在实现一个编译器,我想知道C#选择接口这种实现方式的原因。


1
出于与任何方法都希望是非虚拟的相同原因...我看不出接口的存在在这里有任何相关性。 - Servy
你的代码实现了两次IMakesNoise,但是它是无用的。 - IEatBagels
1
@TopinFrassi 它并没有定义两次,它重新定义了接口的实现,这并不是无用的,它改变了输出的第四行结果。 - Servy
@TopinFrassi 我认为,它只实现了Speak方法两次,而不是整个接口。但是尽管你可能会找到一些模糊的原因来拥有两个Speak方法的实现,我建议你只需更改Lion类中的方法声明为public override void Speak(),这样就可以完全摆脱那个糟糕的“MEOW”了。 - jmodrak
@jmodrak 这段代码无法编译,因为基类中的方法不是虚拟的。 - Servy
显示剩余4条评论
3个回答

1

COM 是怎么参与进来的?我错过了什么吗? - Rick Liddle
@Rick Liddle 这是一个例子 - 当您需要在基类上“实现接口并重新实现该接口”时,就会出现这种情况。 - Viktor Arsanov
好的,我可以接受这个。 - Rick Liddle
这更多是因为COM的工作方式,而不是.NET,但这是这种模式的一个有趣原因。 - Trevor Sundberg

-1
如果 Lion : CatCat : IMakeNoise,那么根据传递性,Lion : IMakeNoise 自动成立 - 因此在 Lion 上声明它是多余和不必要的。这是因为一个 Lion 不能成为一个 Cat 而不继承所有 Cat 的属性 - 包括接口。
虚方法存在是为了让你不仅可以重写,还可以完全替换派生层次结构中的功能。换句话说,您可以从更派生的类中更改较少派生的类的功能。这与遮蔽不同,遮蔽只覆盖给定派生类的方法的功能,而不是整个类层次结构。
通过声明与基类中存在的相同的非虚方法,并添加 new 关键字来指示意图进行遮蔽行为来实现遮蔽。
通过声明与基类中存在的相同的 virtual 方法,并添加 override 关键字来指示意图进行覆盖行为来实现覆盖。
一个代码示例将使这些差异非常清晰。让我们定义一个基本的Vehicle类(作为抽象类,因此无法实例化),以及一个派生的Motorcycle类。两者都将向控制台输出有关它们拥有的轮子数量的信息:
/// <summary>
/// Represents a Vehicle.
/// </summary>
public abstract class Vehicle
{
    /// <summary>
    /// Prints the Number of Wheels to the Console.
    /// Virtual so can be changed by more derived types.
    /// </summary>
    public virtual void VirtualPrintNumberOfWheels()
    {
        Console.WriteLine("Number of Wheels: 4");
    }

    /// <summary>
    /// Prints the Number of Wheels to the Console.
    /// </summary>
    public void ShadowPrintNumberOfWheels()
    {
        Console.WriteLine("Number of Wheels: 4");
    }
}

/// <summary>
/// Represents a Motorcycle.
/// </summary>
public class Motorcycle : Vehicle
{
    /// <summary>
    /// Prints the Number of Wheels to the Console.
    /// Overrides base method.
    /// </summary>
    public override void VirtualPrintNumberOfWheels()
    {
        Console.WriteLine("Number of Wheels: 2");
    }

    /// <summary>
    /// Prints the Number of Wheels to the Console.
    /// Shadows base method.
    /// </summary>
    public new void ShadowPrintNumberOfWheels()
    {
        Console.WriteLine("Number of Wheels: 2");
    }
}

以上我们定义了两个类:一个抽象基类 Vehicle,其中有一个虚方法和一个非虚方法,它们都做同样的事情;还有一个实现了 Vehicle 类的 Motorcycle 类,同时重写了虚方法并遮蔽了普通方法。现在我们将使用不同类型签名调用这些方法,以查看它们之间的区别:
static void Main(string[] args)
{
    // Instantiate a Motorcycle as type Motorcycle
    Motorcycle vehicle = new Motorcycle();

    vehicle.ShadowPrintNumberOfWheels();
    vehicle.VirtualPrintNumberOfWheels();

    // Instantiate a Motorcycle as type Vehicle
    Vehicle otherVehicle = new Motorcycle();

    // Calling Shadow on Motorcycle as Type Vehicle
    otherVehicle.ShadowPrintNumberOfWheels();
    otherVehicle.VirtualPrintNumberOfWheels();

    Console.ReadKey();
}

结果如下:

Number of Wheels: 2
Number of Wheels: 2
Number of Wheels: 4
Number of Wheels: 2

1
这个OP非常明确地表明他确实理解虚拟和非虚拟实现之间的功能差异。问题是在问,“为什么你会想要使用非虚拟实现而不是虚拟实现”。这并没有回答该问题,而只是重复了已经在问题中提到的信息。 - Servy
此外,你的第一行非常不正确。是的,Lion 实现了 IMakeNoise 而没有显式地重新实现它,但是因为在他的实现中该方法是非虚拟的,重新实现接口会功能性地改变接口的实现方式,使其成为一个重要且语义上有意义的事情。只有在实现方法是虚拟的时候才是不必要的,而这在这里并不是这种情况,再次引出了“为什么你想这样做?”的问题。 - Servy
3
如果Lion没有显式重新实现接口,那么将Lion实例强制转换为接口将导致调用CatSpeak实现,而不是Lion的实现。 重新实现接口会改变这种行为。你可以在OP的代码中自己看到这一点。如果你从Lion中移除IMakeNoise并重新运行他的代码,你将看到"MEOW"打印了四次,而不是三次。如果想知道C#为什么这样做的设计决策,我知道Eric Lippert已经在博客中写过相关内容。 - Servy
有趣。我从未知道这一点(我想是因为我从未制作过如此混乱的设计)... 很好知道,谢谢@Servy! - Haney
3
找到了一个相关链接,该SO答案的末尾附有相关博客文章的链接。 - Servy
显示剩余5条评论

-1

我对上面的代码示例有一些问题,但我会将它们大多数放在一边,重点放在最重要的问题上:它违反了面向接口编程而不是实现的原则。如果你将猫和狮子实例的声明更改为以下内容,则可以解决你看到的问题。

IMakesNoise cat = new Cat();
IMakesNoise lion = new Lion();

在这种情况下,您的输出应该是预期的 MEOW、ROAR、MEOW、ROAR。请参见 此处 的演示。

编程到接口还可以更轻松地采用控制反转/依赖注入。


代码示例的整个目的是展示将表达式作为实际类型与接口输入的区别。他特别涵盖了两种可能性,以演示它们之间的差异。你实际上只是删除了前两个输出值,并重复了第三和第四个选项两次。这只是从代码示例中删除信息,没有更多的意义。 - Servy
@Servy 好的,那么我猜我的回答他的问题(“你能举一个好的例子来说明为什么你会在基类上实现一个接口,并在派生类上重新实现该接口吗?”)归结为“我不会这样做,如果你按照接口而不是实现编程,你就不必这样做。” - Rick Liddle
是的,实际上你需要这样做。如果他没有在这里重新实现接口,那么输出将会不同。如果没有重新实现接口,它将会输出两次“MEOW”,因为该方法不是虚拟的。 - Servy
@Servy 出于好奇,你有他的问题的答案吗?我很想知道你如何单独回答这个问题,而不是作为其他答案/评论的计数器。 - Rick Liddle
这基本上就是我的答案。为了回答在评论中提出的问题,可以简单地问问谷歌,这是一个相当常见的问题,而这里的变化与你从他们那里得到的任何答案都不相关。 - Servy

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