为什么C#默认将方法实现为非虚拟方法?


105

与Java不同,为什么C#默认将方法视为非虚函数?它是否更可能是性能问题,而不是其他可能的结果?

我想起了安德斯·海斯伯格(Anders Hejlsberg)读的一段有关现有架构所带来的优势的文章。但是,副作用呢?默认情况下使用非虚拟方法真的不错吗?


1
提及性能原因的答案忽略了以下事实:C#编译器主要将方法调用编译为callvirt而不是call。这就是为什么在C#中,如果this引用为null,则不可能有行为不同的方法。有关更多信息,请参见此处
安迪(Andy)

真正!调用 IL指令主要用于对静态方法的调用。
RBT

1
C#的建筑师Anders Hejlsberg的想法在这里这里
RBT

Answers:


98

应该为继承设计类,以便能够利用它。virtual默认情况下拥有方法意味着可以将类中的每个函数插入并替换为另一个函数,这并不是一件好事。许多人甚至认为班级应该是sealed默认设置。

virtual方法也可能有轻微的性能影响。但是,这可能不是主要原因。


7
就个人而言,我怀疑这部分与性能有关。即使对于虚函数,编译器也能够很callvirt容易call地找到用IL代码中的简单内容替换的位置(或者在JITting时甚至更下游)。Java HotSpot也是如此。其余的答案就在眼前。
康拉德·鲁道夫

5
我同意性能不是最重要的,但是它也会对性能产生影响。它可能很小。当然,这不是选择此选项的原因。只是想提一下。
Mehrdad Afshari

71
密封班让我想打婴儿。
mxmissile

6
@mxmissile:我也是,但是好的API设计仍然很难。我认为默认情况下的密封对于您拥有的代码非常有意义。通常,您团队中的其他程序员在实现您需要的类时没有考虑继承,并且默认情况下密封会很好地传达该信息。
罗曼·斯塔科夫

49
很多人也是精神变态者,并不意味着我们应该听他们的话。Virtual应该是C#中的默认值,代码应该是可扩展的,而不必更改实现或完全重写它。过度保护自己的API的人通常最终会死掉或使用不全的API,这比有人滥用它的情况要糟得多。
克里斯·尼古拉

89

我感到惊讶的是,这里似乎达成了这样的共识,即默认非虚拟化是正确的处理方式。我要讲的是栅栏的另一面-我认为是实用的。

多数论据都像古老的“如果我们给您力量,您可能会伤害自己”这样的论点读给我听。来自程序员吗?

在我看来,像编码员一样,我不了解(或没有足够的时间)来设计自己的库以进行继承和/或可扩展性,是恰好产生了我可能必须修复或调整的库的编码员。覆盖功能最有用的库。

我不得不编写丑陋,绝望的变通方法代码(或者放弃使用并推出自己的替代解决方案)的次数,因为我无法覆盖太多,远远超过了我被咬过的次数(例如在Java中),以覆盖设计者可能没有考虑过的地方。

默认非虚拟使我的生活更加艰难。

更新: [正确地指出]我实际上没有回答这个问题。所以-很抱歉迟到了....

我有点想写一些像“ C#在默认情况下将方法实现为非虚拟方法,因为做出了一个错误的决定,使程序对程序的重视程度高于程序员”这样的东西。(我认为,根据该问题的其他一些答案(例如性能(过早的优化,有人吗?)或保证类的行为),这在某种程度上是合理的。)

但是,我意识到我只是在陈述自己的观点,而不是Stack Overflow想要的明确答案。当然,我认为,在最高层次上,确定的(但无济于事的)答案是:

默认情况下,它们是非虚拟的,因为语言设计者需要做出决定,而这正是他们选择的。

现在我想他们做出该决定的确切原因我们永远不会....噢,等等! 对话记录!

因此,这里关于覆盖API的危险和显式设计继承的需求的答案和评论似乎在正确的轨道上,但是都缺少一个重要的时间方面:Anders的主要关注点是维护类或API的隐式跨版本合同。而且我认为他实际上更关心的是允许.Net / C#平台在代码下进行更改,而不是关注平台顶部的用户代码更改。(他的“务实”观点与我完全相反,因为他是从另一侧看的。)

(但是他们不能只是默认情况下选择了虚函数,然后在代码库中添加了“最终”字吗?也许那是不完全一样的。而且安德斯显然比我聪明,所以我要撒谎。)


2
完全同意。使用第三方api并想覆盖某些行为而无法执行时,非常沮丧。
安迪(Andy)

3
我热情地同意。如果要发布API(无论是在公司内部还是在外部),则可以并且应该确保您的代码是为继承而设计的。如果要发布该API以供许多人使用,则应该使该API良好且美观。与进行良好总体设计(良好的内容,清晰的用例,测试,文档)所需的精力相比,为继承进行设计确实还不错。而且,如果您不进行发布,则默认情况下虚拟化会减少耗时,并且您始终可以修复出现问题的一小部分。
重力

2
现在,如果Visual Studio中只有一个编辑器功能可以将所有方法/属性自动标记为virtual... Visual Studio加载项,有人吗?
kevinarpe 2012年

1
尽管我全心全意地同意您的意见,但这并不是答案。
克里斯·摩根

2
考虑到诸如依赖注入,模拟框架和ORM之类的现代开发实践,很明显,我们的C#设计人员似乎对此不满意。当您无法覆盖默认属性时,必须测试依赖项是非常令人沮丧的。
Jeremy Holovacs

17

因为很容易忘记某个方法可能被重写而不是为此目的而设计。C#使您在虚拟化之前进行思考。我认为这是一个很棒的设计决定。某些人(例如Jon Skeet)甚至说默认情况下应该密封类。


12

总结一下别人说的话,有以下几个原因:

1-在C#中,语法和语义上有很多东西直接来自C ++。C ++在默认情况下不是虚拟的方法会影响C#。

2-默认情况下,将每个方法虚拟化是性能问题,因为每个方法调用都必须使用对象的虚拟表。此外,这严重限制了即时编译器内联方法和执行其他类型的优化的能力。

3-最重要的是,如果默认情况下方法不是虚拟的,则可以保证类的行为。当它们默认为虚拟时(例如在Java中),您甚至无法保证简单的getter方法会按预期进行,因为它可能会被覆盖以在派生类中执行任何操作(当然,您可以并且应该使方法和/或最终课程)。

正如Zifre所提到的,您可能会想知道为什么C#语言没有更进一步,并且默认情况下将类密封。这是关于实现继承问题的整个辩论的一部分,这是一个非常有趣的话题。


1
我也将默认情况下密封首选类。那就是一致的。
安迪2014年

9

C#受C ++(及更多)的影响。默认情况下,C ++不启用动态分派(虚拟函数)。一个(好的?)论点是一个问题:“您多久实现一次属于类的成员的类?”。默认情况下避免启用动态调度的另一个原因是内存占用量。当然,没有指向虚拟表的虚拟指针(vpointer)的类比启用了后期绑定的相应类要小。

性能问题并不是那么容易说“是”或“否”。这样做的原因是Just In Time(JIT)编译,它是C#中的运行时优化。

关于“ 虚拟通话速度.. ”的另一个类似问题


由于JIT编译器,虚拟方法是否会对C#产生性能影响,我对此表示怀疑。这是JIT优于脱机编译的领域之一,因为它们可以内联在运行时之前“未知”的函数调用
David Cournapeau,2009年

1
实际上,我认为它比默认情况下执行C ++的受Java影响更大。
Mehrdad Afshari,2009年

5

原因很简单,除了性能成本外,还有设计和维护成本。与非虚拟方法相比,虚拟方法具有额外的成本,因为该类的设计者必须计划该方法被另一个类覆盖时发生的情况。如果您期望特定的方法更新内部状态或具有特定的行为,则这会产生很大的影响。现在,您必须计划派生类更改该行为时会发生什么。在这种情况下,编写可靠的代码要困难得多。

使用非虚拟方法,您可以完全控制。出问题的是原始作者的错。该代码更容易推理。


这是一个非常古老的帖子,但是对于正在编写他的第一个项目的新手,我经常为我编写的代码的意外/未知后果感到烦恼。知道非虚拟方法完全是我的错,这让我感到非常安慰。
trevorc 2012年

2

如果所有C#方法都是虚拟的,则vtbl将更大。

如果类定义了虚拟方法,则C#对象仅具有虚拟方法。确实,所有对象都具有包括vtbl等效项的类型信息,但是如果未定义虚拟方法,则仅会出现基本的Object方法。

@Tom Hawtin:说C ++,C#和Java都是C语言族的,可能更准确:


1
为什么vtable更大会成为问题?每个类(而不是每个实例)只有1个vtable,因此它的大小并没有太大的区别。
重力

1

来自Perl的背景,我认为C#密封了每个可能想要通过非虚拟方法扩展和修改基类行为的开发人员的厄运,而这并没有迫使新类的所有用户都意识到潜在的隐患。细节。

考虑List类的Add方法。如果开发人员想要在“添加”特定列表时更新多个潜在数据库之一,该怎么办?如果默认情况下'Add'是虚拟的,则开发人员可以开发一个'BackedList'类来覆盖'Add'方法,而不必强迫所有客户端代码都知道它是'BackedList'而不是常规的'List'。出于所有实际目的,可以将“ BackedList”视为客户端代码中的另一个“ List”。

从大型主类的角度来看,这很有意义,该主类可能提供对一个或多个列表组件的访问,这些组件本身由数据库中的一个或多个模式支持。鉴于默认情况下C#方法不是虚拟的,所以由主类提供的列表不能是简单的IEnumerable或ICollection,甚至不能是List实例,而必须改为以“ BackedList”的形式发布给客户端,以确保新版本调用“添加”操作中的“更新”以更新正确的架构。


诚然,默认情况下,虚拟方法使工作更轻松,但这有意义吗?您可以采用许多方法来简化生活。为什么不只是在课堂上公开场?更改其行为太容易了。我认为,语言中的所有内容均应严格封装,僵化并具有弹性,以默认更改。仅在需要时更改它。仅是lil的哲学abt设计决策。续..
nawfal

...要谈论当前主题,仅当Bis为时才应使用继承模型A。如果B需要与之不同的东西A则不是A。我认为,将语言作为一种功能来覆盖是设计缺陷。如果您需要其他Add方法,则您的集合类不是List。试图说这是假的。正确的方法是组合(而不是伪造)。没错,整个框架都建立在最重要的功能之上,但是我只是不喜欢它。
nawfal

我想我理解您的意思,但是在提供的具体示例中:“ BackedList”可以仅实现接口“ IList”,而客户端仅知道该接口。对?我想念什么吗?但是,我确实了解您要提出的更广泛的观点。
Vetras

0

这当然不是性能问题。Sun的Java解释器使用相同的代码进行调度(invokevirtual字节码),而HotSpot则生成完全相同的代码final。我相信所有C#对象(但不是结构)都具有虚拟方法,因此您总是需要vtbl/ runtime类标识。C#是“类Java语言”的方言。暗示它来自C ++并不完全诚实。

有一个想法,您应该“为继承而设计或禁止继承”。直到您要解决一个严重的业务案例时,这听起来都是个好主意。也许从您无法控制的代码继承。


正是由于所有方法默认都是虚拟的,因此Hotspot不得不竭尽全力进行优化,这对性能产生了巨大影响。CoreCLR能够达到相似的性能,同时更加简单
Yair Halberstadt

@YairHalberstadt Hotspot需要能够出于各种其他原因而退出编译的代码。自从我研究了源代码已经有好几年了,但是final有效final方法之间的区别是微不足道的。还值得注意的是,它可以进行双态内联,即具有两种不同实现的内联方法。
汤姆·哈汀
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.