由于您问过C#为什么要这样做,所以最好问一下C#创建者。C#的首席架构师Anders Hejlsberg在访谈中回答了为什么他们选择默认情况下不选择虚拟(如Java)的原因,相关片段如下。
请记住,Java默认情况下具有虚拟属性,并带有final关键字,以将方法标记为非虚拟方法。仍然有两个概念需要学习,但是许多人不了解最终关键字或不主动使用。C#强迫人们使用虚拟的和新的/替代来有意识地做出那些决定。
有几个原因。一是表现。我们可以看到,当人们用Java编写代码时,他们忘记了将方法标记为最终方法。因此,这些方法是虚拟的。因为它们是虚拟的,所以它们的表现也不佳。与作为虚拟方法相关的性能开销。那是一个问题。
一个更重要的问题是版本控制。关于虚拟方法有两种思路。这所学术思想学院说:“一切都应该是虚拟的,因为有一天我可能想超越它。” 务实的思想流派来自构建在现实世界中运行的真实应用程序,他说:“我们必须非常谨慎地对待虚拟化。”
当我们在平台上虚拟化某些东西时,我们对未来的发展做出了很多承诺。对于非虚拟方法,我们保证当您调用此方法时,将发生x和y。当我们在API中发布虚拟方法时,我们不仅承诺在调用此方法时还会发生x和y。我们还承诺,当您重写此方法时,相对于其他方法,我们将以特定的顺序调用它,并且状态将处于此不变状态。
每次您在API中说“ virtual”(虚拟)时,您都在创建一个回调挂钩。作为OS或API框架设计者,您必须对此非常小心。您不希望用户在API中的任意点上重写和挂接,因为您不一定会做出这些承诺。人们制作虚拟产品时可能不会完全理解自己的承诺。
采访中对开发人员如何考虑类继承设计以及如何导致他们的决定进行了更多讨论。
现在到以下问题:
我不明白为什么在世界上为什么要在我的DerivedClass中添加一个与BaseClass具有相同名称和签名的方法并定义新的行为,但是在运行时多态时,将调用BaseClass方法!(这不是覆盖,但在逻辑上应该覆盖)。
这就是派生类想要声明它不遵守基类协定但具有相同名称的方法时。(对于任何不了解C#new
和override
C#之间的区别的人,请参见此MSDN页面)。
一个非常实际的情况是:
- 您创建了一个API,该API的类名为
Vehicle
。
- 我开始使用您的API并派生了
Vehicle
。
- 您的
Vehicle
课程没有任何方法PerformEngineCheck()
。
- 在我的
Car
课堂上,我添加了一个method PerformEngineCheck()
。
- 您发布了API的新版本,并添加了一个
PerformEngineCheck()
。
- 我无法重命名方法,因为我的客户依赖于我的API,这会破坏它们。
因此,当我针对您的新API重新编译时,C#会向我警告此问题,例如
如果基数PerformEngineCheck()
不是virtual
:
app2.cs(15,17): warning CS0108: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
Use the new keyword if hiding was intended.
如果基数PerformEngineCheck()
是virtual
:
app2.cs(15,17): warning CS0114: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
现在,我必须明确决定我的班级是否实际上是在扩展基类的合同,或者是其他合同但恰好是相同的名称。
- 通过这样做
new
,如果基本方法的功能与派生方法不同,则不会破坏客户。引用的任何代码Vehicle
都不会Car.PerformEngineCheck()
被调用,但是引用了的代码Car
将继续看到与我提供的功能相同的功能PerformEngineCheck()
。
一个类似的例子是,当基类中的另一个方法可能正在调用时PerformEngineCheck()
(尤其是在较新版本中),如何防止它调用PerformEngineCheck()
派生类的?在Java中,该决定将由基类决定,但对派生类一无所知。在C#,该决定由两个基类(通过virtual
关键字),和所导出的类(通过new
和override
关键字)。
当然,编译器引发的错误也为程序员提供了一个有用的工具,以防止程序员意外地出错(即,在没有意识到的情况下重写或提供新功能)。
就像安德斯(Anders)所说,现实世界迫使我们陷入这样的问题,如果我们从头开始,我们将永远不想介入。
编辑:添加了new
必须用于确保接口兼容性的示例。
编辑:在评论时,我还遇到了Eric Lippert(当时的C#设计委员会成员之一)在其他示例场景中的文章(由Brian提到)。
第2部分:基于更新的问题
但是在C#的情况下,如果SpaceShip没有覆盖Vehicle类的加速并使用new,则代码的逻辑将被破坏。这不是不利条件吗?
谁决定SpaceShip
实际上是覆盖Vehicle.accelerate()
还是不同?它必须是SpaceShip
开发人员。因此,如果SpaceShip
开发人员决定不保留基类的合同,那么您对的调用Vehicle.accelerate()
不应转到SpaceShip.accelerate()
,还是应该?那是他们将其标记为的时间new
。但是,如果他们确定确实确实遵守合同,那么他们实际上会在合同上作标记override
。无论哪种情况,您的代码都可以通过基于contract调用正确的方法来正确运行。您的代码如何确定SpaceShip.accelerate()
实际上是覆盖Vehicle.accelerate()
还是名称冲突?(请参见上面的示例)。
然而,隐含继承的情况下,即使SpaceShip.accelerate()
不守合同的Vehicle.accelerate()
,方法调用仍然会去SpaceShip.accelerate()
。