与 Java 不同,为什么 C# 默认将方法视为非虚函数?是否更有可能是性能问题而不是其他可能的结果?
我想起了阅读 Anders Hejlsberg 的一段关于现有架构带来的几个优点的段落。但是,副作用呢?默认情况下使用非虚拟方法真的是一个很好的权衡吗?
与 Java 不同,为什么 C# 默认将方法视为非虚函数?是否更有可能是性能问题而不是其他可能的结果?
我想起了阅读 Anders Hejlsberg 的一段关于现有架构带来的几个优点的段落。但是,副作用呢?默认情况下使用非虚拟方法真的是一个很好的权衡吗?
应该为继承设计类以便能够利用它。默认情况下拥有方法virtual
意味着类中的每个函数都可以被插入并被另一个函数替换,这并不是一件好事。许多人甚至认为类应该是sealed
默认的。
virtual
方法也可能对性能产生轻微影响。然而,这不太可能是主要原因。
我很惊讶这里似乎有这样的共识,即默认情况下非虚拟是做事的正确方法。我要站在栅栏的另一边——我认为是务实的。
大多数理由对我来说就像古老的“如果我们给你权力,你可能会伤害自己”的论点。来自程序员?!
在我看来,那些没有足够知识(或没有足够时间)来设计他们的库以实现继承和/或可扩展性的编码器正是那个产生了我可能不得不修复或调整的库的编码器——正是覆盖能力最有用的库。
我不得不编写丑陋、绝望的变通代码(或放弃使用并推出我自己的替代解决方案)的次数,因为我无法覆盖远远超过我被咬的次数(例如在 Java 中)通过覆盖设计者可能没有考虑到的地方。
Non-virtual-by-default 让我的生活更加艰难。
更新:有人指出[非常正确]我实际上并没有回答这个问题。所以 - 很抱歉迟到了....
我有点希望能够写一些精辟的东西,比如“C# 默认将方法实现为非虚拟的,因为做出了一个错误的决定,它比程序员更重视程序”。(我认为基于这个问题的其他一些答案,这在某种程度上是合理的——比如性能(过早的优化,有人吗?),或者保证类的行为。)
但是,我意识到我只是在陈述我的观点,而不是 Stack Overflow 想要的明确答案。当然,我认为,在最高级别上,明确的(但无益的)答案是:
默认情况下它们是非虚拟的,因为语言设计者需要做出决定,这就是他们选择的。
现在我猜他们做出这个决定的确切原因我们永远不会......哦,等等! 谈话记录!
因此,这里关于重写 API 的危险和显式设计继承的需要的答案和评论似乎都在正确的轨道上,但都缺少一个重要的时间方面:Anders 的主要关注点是维护一个类或 API 的隐式跨版本合同。而且我认为他实际上更关心的是允许 .Net / C# 平台在代码下进行更改,而不是关心用户代码在平台之上的更改。(而他的“务实”观点与我的完全相反,因为他从另一边看。)
(但他们不能只是默认选择虚拟,然后在代码库中添加“final”吗?也许这不完全一样……而且 Anders 显然比我聪明,所以我就让它撒谎吧。)
因为很容易忘记一个方法可能会被覆盖而不是为此而设计。C# 让您在将其变为虚拟之前进行思考。我认为这是一个伟大的设计决定。有些人(例如 Jon Skeet)甚至说默认情况下应该密封类。
总结其他人所说的,有几个原因:
1-在 C# 中,语法和语义中有很多东西直接来自 C++。C++ 中默认非虚拟的方法影响了 C#。
2-默认情况下每个方法都是虚拟的,这是一个性能问题,因为每个方法调用都必须使用对象的虚拟表。此外,这极大地限制了即时编译器内联方法和执行其他类型优化的能力。
3-最重要的是,如果默认情况下方法不是虚拟的,则可以保证类的行为。当它们默认为虚拟时,例如在 Java 中,您甚至不能保证一个简单的 getter 方法会按预期执行,因为它可以被覆盖以在派生类中执行任何操作(当然,您可以并且应该使方法和/或类final)。
正如 Zifre 所提到的,人们可能想知道为什么 C# 语言没有更进一步,默认情况下使类密封。这是关于实现继承问题的整个辩论的一部分,这是一个非常有趣的话题。
C# 受 C++(以及更多)的影响。C++ 默认不启用动态调度(虚函数)。对此的一个(好?)论据是问题:“您多久实现一次属于类层次结构的类?”。避免默认启用动态调度的另一个原因是内存占用。没有指向虚拟表的虚拟指针 (vpointer) 的类当然比启用后期绑定的相应类小。
性能问题不是那么容易说“是”或“不是”的。其原因是即时 (JIT) 编译,它是 C# 中的运行时优化。
另一个关于“虚拟通话速度.. ”的类似问题
原因很简单,除了性能成本外,还有设计和维护成本。与非虚拟方法相比,虚拟方法具有额外的成本,因为类的设计者必须计划当该方法被另一个类覆盖时会发生什么。如果您期望特定方法更新内部状态或具有特定行为,这会产生很大影响。您现在必须计划派生类更改该行为时会发生什么。在这种情况下编写可靠的代码要困难得多。
使用非虚拟方法,您可以完全控制。任何错误都是原作者的错。代码更容易推理。
如果所有 C# 方法都是虚拟的,那么 vtbl 会大得多。
只有在类定义了虚拟方法时,C# 对象才具有虚拟方法。的确,所有对象都具有包含等效 vtbl 的类型信息,但如果没有定义虚拟方法,则将仅存在基本 Object 方法。
@Tom Hawtin:说 C++、C# 和 Java 都来自 C 语言家族可能更准确:)
来自 perl 背景,我认为 C# 让每个可能想要通过非虚拟方法扩展和修改基类行为的开发人员的厄运,而不会迫使新类的所有用户意识到潜在的幕后细节。
考虑 List 类的 Add 方法。如果开发人员想要在“添加”特定列表时更新几个潜在数据库之一怎么办?如果默认情况下“添加”是虚拟的,则开发人员可以开发一个覆盖“添加”方法的“BackedList”类,而无需强制所有客户端代码知道它是“BackedList”而不是常规“List”。出于所有实际目的,“BackedList”可以被视为客户端代码中的另一个“列表”。
从可能提供对一个或多个列表组件的访问的大型主类的角度来看,这是有道理的,这些组件本身由数据库中的一个或多个模式支持。鉴于默认情况下 C# 方法不是虚拟的,因此主类提供的列表不能是简单的 IEnumerable 或 ICollection 甚至是 List 实例,而是必须将其作为“BackedList”广告给客户端,以确保新版本调用“添加”操作以更新正确的架构。
这当然不是性能问题。Sun 的 Java 解释器使用相同的代码来分派(invokevirtual
字节码),HotSpot 无论是否生成完全相同的代码final
。我相信所有 C# 对象(但不是结构)都有虚拟方法,所以你总是需要vtbl
/runtime 类标识。C#是“类Java语言”的一种方言。说它来自 C++ 并不完全诚实。
有一种想法是您应该“为继承而设计,否则就禁止它”。这听起来像是一个好主意,直到您有一个严重的业务案例需要快速修复。也许从您无法控制的代码继承。
表现。
想象一组覆盖虚拟基方法的类:
class Base {
public virtual int func(int x) { return 0; }
}
class ClassA: Base {
public override int func(int x) { return x + 100; }
}
class ClassB: Base {
public override int func(int x) { return x + 200; }
}
现在假设您要调用该func
方法:
Base foo;
//...sometime later...
int x = foo.func(42);
看看 CPU 实际要做的事情:
mov ecx, bfunc$ -- load the address of the "ClassB.func" method from the VMT
push 42 -- push the 42 argument
call [eax] -- call ClassB.func
组装并不难理解:
mov ecx, foo$
:这需要进入内存,并命中对象的虚拟方法表(VMT)的一部分以获取覆盖foo
方法的地址。CPU 将开始从内存中获取数据,然后继续:push 42
:将参数压42
入堆栈以调用函数。没问题,可以马上运行,然后我们继续:call [ecx]
调用ClassB.func
函数的地址。← !!!那是个问题。ClassB.func
尚未从 VMT 中获取函数的地址。这意味着 CPU 不知道下一步要去哪里。理想情况下,它会跟随 ajump
并在等待ClassB.func
从内存中返回的地址时继续执行指令。但它不能;所以我们等待。
如果幸运的话:数据已经在 L2 缓存中。将值从 L2 缓存中取出到可以使用的地方需要 12-15 个周期。CPU 不知道接下来要去哪里,而不必等待内存 12-15 个周期。
我们的程序在 12-15 个周期内无所事事。
CPU 内核有 7 个执行引擎。CPU 的主要工作是让这 7 条管道充满要做的事情。这意味着:
您的 CPU 拥有所有这些功能和能力,而且它只是停滞了 15 个周期!?
这太可怕了。这真糟糕。每次调用方法时都会受到这种惩罚virtual
——不管你是否真的覆盖了它。
我们的程序每次方法调用都慢了 12-15 个周期,因为语言设计者让虚拟方法选择退出而不是选择加入。
这就是微软决定不让所有方法默认为虚拟的原因:他们从 Java 的错误中吸取了教训。
2012 年,Xamarin 人将所有 Android 的 Dalvik(即 Java)移植到 C#。从他们那里:
表现
当 C# 出现时,Microsoft 以几种重要的方式修改了该语言,使其更易于优化。引入了值类型以允许小对象具有较低的开销,并且虚拟方法被选择加入,而不是选择退出,这使得更简单的 VM。
(强调我的)