为什么C#是用不同于Java的“ new”和“ virtual + override”关键字构成的?


61

在Java中没有virtualnewoverride对于方法定义的关键字。因此,方法的工作很容易理解。因为如果DerivedClass扩展BaseClass的和具有相同的名称和它们的签名的方法BaseClass的那么压倒一切将发生在运行时多态性(提供的方法是不是static)。

BaseClass bcdc = new DerivedClass(); 
bcdc.doSomething() // will invoke DerivedClass's doSomething method.

现在来到C#时,可能会有太多的困惑,并且很难理解newor virtual+derivenew +虚拟重写的工作方式。

我不明白为什么在世界上为什么要在我的方法中添加一个DerivedClass具有相同名称和签名的方法,BaseClass并定义一个新的行为,但是在运行时多态时,BaseClass将调用该方法!(这不是覆盖,但在逻辑上应该覆盖)。

如果virtual + override逻辑实现是正确的,则程序员必须考虑在编码时应允许用户重写的方法。里面有一些有利条件(让我们现在不去那里)。

那么,为什么在C#中有如此大的空间用于逻辑推理和混乱。所以,我可以重新塑造我的问题是在现实世界方面,我应该考虑使用virtual + override替代new,还可以使用的new,而不是virtual + override


在获得了特别是Omar的一些非常好的回答之后,我发现C#设计人员更加强调程序员在创建方法之前应该思考的问题,这种方法很好并且可以处理Java中的一些菜鸟错误。

现在我想到一个问题。就像在Java中,如果我有类似的代码

Vehicle vehicle = new Car();
vehicle.accelerate();

后来我提出新的类SpaceShip衍生Vehicle。然后我想将所有内容更改car为一个SpaceShip对象,而我只需要更改一行代码

Vehicle vehicle = new SpaceShip();
vehicle.accelerate();

这不会在任何代码点破坏我的任何逻辑。

但是在C#的情况下,如果SpaceShip不覆盖Vehicleaccelerate并使用,new则代码的逻辑将被破坏。这不是不利条件吗?


77
您只是习惯了Java的使用方式,并且根本没有花时间按照自己的术语来理解C#关键字。我使用C#,立即理解了这些术语,并发现Java奇怪的方式。
罗伯特·哈维

12
IIRC,C#这样做是为了“提高清晰度”。如果您必须明确地说“新”或“覆盖”,它可以使您立即清楚地看到正在发生的事情,而不必费力地试图弄清楚该方法是否覆盖了基类中的某些行为。我还发现能够指定要指定为虚拟的方法以及不指定为虚拟的方法非常有用。(Java这样做final;这是相反的方式)。
罗伯特·哈维

15
“在Java中,没有虚拟的,新​​的,覆盖方法的关键字来定义方法。”除了之外@Override
svick


3
@svick注释和关键字不是一回事:)
Anirban Nag'tintinmj'2014年

Answers:


86

由于您问过C#为什么要这样做,所以最好问一下C#创建者。C#的首席架构师Anders Hejlsberg在访谈中回答了为什么他们选择默认情况下不选择虚拟(如Java)的原因,相关片段如下。

请记住,Java默认情况下具有虚拟属性,并带有final关键字,以将方法标记为非虚拟方法。仍然有两个概念需要学习,但是许多人不了解最终关键字或不主动使用。C#强迫人们使用虚拟的和新的/替代来有意识地做出那些决定。

有几个原因。一是表现。我们可以看到,当人们用Java编写代码时,他们忘记了将方法标记为最终方法。因此,这些方法是虚拟的。因为它们是虚拟的,所以它们的表现也不佳。与作为虚拟方法相关的性能开销。那是一个问题。

一个更重要的问题是版本控制。关于虚拟方法有两种思路。这所学术思想学院说:“一切都应该是虚拟的,因为有一天我可能想超越它。” 务实的思想流派来自构建在现实世界中运行的真实应用程序,他说:“我们必须非常谨慎地对待虚拟化。”

当我们在平台上虚拟化某些东西时,我们对未来的发展做出了很多承诺。对于非虚拟方法,我们保证当您调用此方法时,将发生x和y。当我们在API中发布虚拟方法时,我们不仅承诺在调用此方法时还会发生x和y。我们还承诺,当您重写此方法时,相对于其他方法,我们将以特定的顺序调用它,并且状态将处于此不变状态。

每次您在API中说“ virtual”(虚拟)时,您都在创建一个回调挂钩。作为OS或API框架设计者,您必须对此非常小心。您不希望用户在API中的任意点上重写和挂接,因为您不一定会做出这些承诺。人们制作虚拟产品时可能不会完全理解自己的承诺。

采访中对开发人员如何考虑类继承设计以及如何导致他们的决定进行了更多讨论。

现在到以下问题:

我不明白为什么在世界上为什么要在我的DerivedClass中添加一个与BaseClass具有相同名称和签名的方法并定义新的行为,但是在运行时多态时,将调用BaseClass方法!(这不是覆盖,但在逻辑上应该覆盖)。

这就是派生类想要声明它不遵守基类协定但具有相同名称的方法时。(对于任何不了解C#newoverrideC#之间的区别的人,请参见此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关键字),所导出的类(通过newoverride关键字)。

当然,编译器引发的错误也为程序员提供了一个有用的工具,以防止程序员意外地出错(即,在没有意识到的情况下重写或提供新功能)。

就像安德斯(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()


12
现在性能点已经完全过时了。作为证明,请参阅我的基准测试,该基准显示通过非最终但永不重载的方法访问字段仅需一个周期。
maaartinus

7
当然可以。问题是,当C#决定这样做时,为什么要在那个时候这样做,因此这个答案是正确的。如果问题是它是否仍然有意义,那就是另一种讨论,恕我直言。
Omer Iqbal 2014年

1
我完全同意你的看法。
maaartinus 2014年

2
恕我直言,虽然功能非虚拟肯定有用途,但是当某些预期不会覆盖基类方法或实现接口的东西这样做时,会发生意外陷阱的风险。
2014年

3
@Nilzor:因此,关于学术与实用的争论。务实的是,打破某些事物的责任在于最后的改变者。如果您更改了基类,而现有的派生类依赖于它不变,那不是他们的问题(“它们”可能不再存在)。因此,即使该类永远不应该是virtual,基类也被锁定在其派生类的行为中。正如您所说,RhinoMock存在。是的,如果您正确完成所有方法,那么一切都是等效的。这里我们指向曾经构建的每个Java系统。
deworde 2014年

94

之所以这样做是因为这是正确的做法。事实是,允许所有方法都被覆盖是错误的。这会导致脆弱的基类问题,在这种情况下,您无法判断对基类的更改是否会破坏子类。因此,您必须将不应该覆盖的方法列入黑名单,或者将允许覆盖的方法列入白名单。在这两种方法中,白名单不仅更安全(因为您不能偶然创建脆弱的基类),而且还需要较少的工作,因为您应该避免继承以支持合成。


7
允许任何任意方法被覆盖是错误的。您应该只能覆盖超类设计器已指定为可安全覆盖的方法。
Doval 2014年

21
埃里克·利珀特(Eric Lippert)在他的文章《虚拟方法和脆性基类》中对此进行了详细讨论。
布莱恩(Brian

12
Java具有final修饰符,那么怎么了?
Sarge Borsch 2014年

13
@corsiKa“对象应该可以扩展”-为什么?如果基类的开发人员从未考虑过继承,则几乎可以保证代码中存在细微的依赖关系和假设,这些缺陷和假设会导致错误(还记得HashMap吗?)。为继承而设计,或者使合同清楚,或者不确保没有人可以继承该类。@Override之所以被添加为创可贴,是因为当时更改行为为时已晚,但是即使是原始Java开发人员(尤其是Josh Bloch)也同意这是一个错误的决定。
Voo

9
@jwenting“观点”是一个有趣的词;人们喜欢将其附加到事实上,因此他们可以无视它们。但是,无论是值得的还是约书亚·布洛赫(Joshua Bloch)的“观点”(参见:有效Java第17项),詹姆斯·高斯林(James Gosling)的“观点”(参见本采访),并且正如奥默·伊克巴尔(Omer Iqbal)的回答所指出,它也是安德斯·海斯伯格(Anders Hejlsberg)的“观点”。(尽管埃里克·利珀特(Eric Lippert)从来没有谈到过选择加入还是退出,但他显然也同意继承也很危险。)那么,谁在指的是?
Doval 2014年

33

正如罗伯特·哈维(Robert Harvey)所说,这就是您所习惯的一切。我发现Java 缺乏这种灵活性很奇怪。

也就是说,为什么首先要这样做?出于同样的原因,C#有publicinternal(也称“无”), ,protectedprotected internalprivate,但Java的只是有publicprotected什么都没有,和private。它可以对编码行为进行更精细的控制,但要牺牲更多的术语和关键字来跟踪。

newvs. 的情况下virtual+override,它是这样的:

  • 如果要强制子类实现该方法,请在子类中使用abstractoverride
  • 如果要提供功能但允许子类替换它,请在子类中使用virtualoverride
  • 如果要提供子类永远不需要重写的功能,则不要使用任何东西。
    • 然后,如果您有一个特殊情况的子类,其行为需要有所不同,请new在该子类中使用。
    • 如果你想确保没有子类可以永远地覆盖这些行为,使用sealed的基类。

举一个真实的例子:我处理过一个来自许多不同来源的电子商务订单的项目。有一个OrderProcessor具有大多数逻辑的基,每个源的子类都有某些abstract/ virtual方法可以覆盖。在直到获得一个具有完全不同的处理订单方式的新资源之前,我们一直工作得很好,因此我们不得不替换一个核心功能。在这一点上,我们有两个选择:1)添加virtual到基本方法中,并override在子方法中;或2)添加new到孩子中。

尽管任何一个都可以工作,但第一个可以使将来很容易再次重写该特定方法变得非常容易。例如,它会以自动完成方式显示。但是,这是例外情况,因此我们选择使用new。保留了“不需要重写此方法”的标准,同时允许在特殊情况下使用该方法。这是语义上的差异,使生活更轻松。

但是请注意,与此相关的行为差异,而不仅仅是语义差异。有关详细信息,请参见本文。但是,我从来没有遇到过需要利用这种行为的情况。


7
我认为故意使用new这种方式是一个等待发生的错误。如果我在某个实例上调用方法,那么即使将实例强制转换为基类,我也希望它总是做同样的事情。但这不是newed方法的行为。
svick 2014年

@svick-可能是的。在我们的特定情况下,这永远不会发生,但这就是为什么我添加了警告。
Bobson 2014年

newMVC框架中的WebViewPage <TModel>是的一种很好的用法。但是我也被涉及new几个小时的错误所困扰,所以我认为这不是一个不合理的问题。
pdr

1
@ C.Champagne:任何工具都可能使用不当,而这是一种特别锋利的工具-您可以轻松切割自己。但这不是从工具箱中删除工具并从更有才华的API设计人员中删除选项的原因。
pdr

1
@svick正确,这就是为什么它通常是编译器警告。但是具有“新的”能力可以覆盖边缘条件(例如给定条件),甚至更好,这使得在诊断不可避免的错误时确实很奇怪。“为什么是这个类,而只有这个类是越野车?啊,哈哈,一个“新”,让我们测试使用SuperClass的地方”。
deworde 2014年

7

Java的设计使得,只要有对对象的任何引用,对具有特定参数类型的特定方法名称的调用(如果允许的话)将始终调用同一方法。隐式参数类型的转换可能会受到引用类型的影响,但是一旦所有此类转换都已解决,则引用的类型就不相关了。

这简化了运行时间,但可能导致一些不幸的问题。假设GrafBase没有实现void DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3),而是将其GrafDerived实现为一个公共方法,该方法绘制一个平行四边形,其计算的第四个点与第一个相反。进一步假设更高版本的GrafBase实现了具有相同签名的公共方法,但其计算出的第四点与第二点相反。接收GrafBase到a的客户希望接收到a的引用,但他们GrafDerived希望DrawParallelogram以新GrafBase方法的方式来计算第四点,但是GrafDerived.DrawParallelogram在更改基本方法之前一直使用的客户将期望GrafDerived最初实现的行为。

在Java中,作者GrafDerived无法使该类与使用新GrafBase.DrawParallelogram方法的客户端共存(并且可能不知道GrafDerived甚至不存在),而不会破坏与定义该方法GrafDerived.DrawParallelogram之前使用的现有客户端代码的兼容性GrafBase。由于DrawParallelogram无法确定正在调用的是哪种客户端,因此在由各种客户端代码调用时,它必须具有相同的行为。由于两种客户端代码对行为的期望不同,因此GrafDerived无法避免违反其中一种的合法期望(即破坏合法的客户端代码)。

在C#中,如果GrafDerived未重新编译,则运行时将假定DrawParallelogram在类型引用上调用方法的代码GrafDerived将期望GrafDerived.DrawParallelogram()上次编译时的行为,但是在类型引用上调用该方法的代码GrafBase将是期望的GrafBase.DrawParallelogram(加入)。如果GrafDerived稍后在存在增强功能的情况下重新编译GrafBase,则编译器将发出嘶哑的声音,直到程序员指定他的方法是否旨在替代继承成员的有效替代GrafBase,或者其行为是否需要与type的引用绑定在一起为止GrafDerived,但不应这样做。替换类型为的引用的行为GrafBase

可能有人会合理地认为,采取一种与GrafDerived成员GrafBase具有相同签名的方法不同的方法将表明设计不好,因此不应予以支持。不幸的是,由于基本类型的作者无法知道可能将哪些方法添加到派生类型,反之亦然,因此,除非有可能,否则基本类和派生类客户对同名方法的期望不同的情况是不可避免的,除非不允许任何人添加别人可能还会添加的任何名称。问题不在于是否应该进行这样的名称重复,而是如何在名称重复时最大程度地减少危害。


2
我认为这里有一个很好的答案,但它正迷失在文字的墙上。请将此分成几段。
Bobson,2014年

什么是DerivedGraphics? A class using GrafDerived`?
C.Champagne

@ C.Champagne:我的意思是GrafDerived。我开始使用DerivedGraphics,但是感觉有点长。甚至GrafDerived还有些长,但是不知道如何最好地命名应该具有清晰的基/派生关系的图形渲染器类型。
超级猫

1
@鲍勃森:更好吗?
supercat

6

标准情况:

您是多个项目使用的基类的所有者。您想对上述基类进行更改,使其在1到无数个派生类之间切换,这些派生类在提供真实世界价值的项目中(一个框架最多提供一次删除的价值,没有真正的人类想要一个框架,他们想要在框架上运行的东西)。向派生类的忙碌所有者表示好运;“好吧,您必须进行更改,您不应该重写该方法”,而不必为那些必须批准决策的人员获得“框架:项目的延迟者和Bug的原因”的代表。

尤其是因为您没有声明它是不可覆盖的,所以您暗中声明了他们可以做阻止您更改的事情。

而且,如果您没有大量的派生类通过重写基类来提供真实世界的价值,那么为什么它首先是基类呢?希望是强大的动力,但也是结束未引用代码的一种很好的方法。

最终结果:您的框架基类代码变得异常脆弱和静态,并且您无法真正进行必要的更改以保持最新/高效。或者,您的框架会得到不稳定的代表(派生的类不断崩溃),人们根本不会使用它,因为使用框架的主要原因是使编码更快,更可靠。

简而言之,您不能要求忙碌的项目所有者推迟他们的项目以修复您所引入的错误,并且期望比“走开”更好的事情,除非您为他们带来了可观的收益,即使原始的“过失”是他们的,充其量是可以争论的。

最好不要让他们首先做错事,这是“默认情况下非虚拟”的来历。当有人带着很明确的理由来找您时,他们为什么需要这种特殊方法才能被覆盖,以及为什么它应该是安全的,您可以“解锁”它,而不必冒险破坏任何其他人的代码。


0

默认为非虚拟假定基类开发人员是完美的。以我的经验,开发人员并不完美。如果基类的开发人员无法想象一个方法可能被覆盖或忘记添加虚拟的用例,那么在扩展基类而不修改基类时,我将无法利用多态性。在现实世界中,修改基类通常不是一种选择。

在C#中,基类开发人员不信任子类开发人员。在Java中,子类开发人员不信任基类开发人员。子类开发人员负责该子类,并且应该(imho)有权扩展他们认为合适的基类(除非明确拒绝,并且在Java中,他们甚至可能会出错)。

这是语言定义的基本属性。这是对还是错,这是事实,不能改变。


2
这似乎并没有提供任何实质性的过度点进行,而且在以往5个回答解释
蚊蚋
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.