繁体   English   中英

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

[英]Why C# implements methods as non-virtual by default?

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

我想起了阅读 Anders Hejlsberg 的一段关于现有架构带来的几个优点的段落。 但是,副作用呢? 默认情况下使用非虚拟方法真的是一个很好的权衡吗?

应该为继承设计类以便能够利用它。 默认情况下使用virtual方法意味着可以插入类中的每个函数并替换为另一个函数,这实际上并不是一件好事。 很多人甚至认为课程应该默认sealed

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

我很惊讶这里似乎有这样的共识,即非虚拟默认是正确的做事方式。 我会在另一方面下台 - 我认为实用主义的一面。

大多数理由都像旧的“如果我们给你力量,你可能会伤到自己”这样的论点。 来自程序员?!

在我看来,编程人员不够了解(或有足够的时间)设计他们的库以进行继承和/或可扩展性的编码器是完全生成我可能需要修复或调整的库 - 完全是库中的覆盖能力最有用。

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

非虚拟默认让我的生活更加困难。

更新:有人指出[非常正确]我实际上没有回答这个问题。 所以 - 并为迟到而道歉......

我有点想写一些简洁的东西,比如“C#默认情况下将方法实现为非虚拟,因为做出了一个错误的决定,它比程序员更重视程序”。 (我认为根据这个问题的其他一些答案 - 比如性能(过早优化,任何人?),或保证类的行为,这可能有点合理。)

但是,我意识到我只是在陈述我的意见,而不是Stack Overflow所希望的那个明确的答案。 当然,我认为,在最高级别,最终(但没有帮助)的答案是:

默认情况下,它们是非虚拟的,因为语言设计者决定制作,这就是他们所选择的。

现在我想他们做出决定的确切原因我们永远不会......哦,等等! 谈话的成绩单!

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

(但他们不能只选择虚拟默认,然后通过代码库填写“最终”吗?也许这并不完全相同......而Anders显然比我聪明,所以我会让它撒谎。)

因为很容易忘记一个方法可能被覆盖而不是为此设计。 C#让你在虚拟之前思考。 我认为这是一个伟大的设计决定。 有些人(比如Jon Skeet)甚至说过应该默认密封课程。

总结其他人所说的,有几个原因:

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

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

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

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

C#受C ++(及更多)的影响。 默认情况下,C ++不启用动态分派(虚拟功能)。 对此的一个(好的?)论点是这样的问题:“你经常实现属于类层次结构成员的类吗?”。 默认情况下避免启用动态分派的另一个原因是内存占用。 没有指向虚拟表的虚拟指针(vpointer)的类比使用后期绑定的相应类小。

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

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

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

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

如果所有C#方法都是虚拟的,那么vtbl会更大。

如果类定义了虚方法,则C#对象只有虚方法。 确实,所有对象都具有包含vtbl等效项的类型信息,但如果没有定义虚拟方法,则只存在基础Object方法。

@Tom Hawtin:可能更准确地说C ++,C#和Java都来自C系列语言:)

来自perl背景我认为C#密封了每个可能想要通过非虚方法扩展和修改基类行为的开发人员的厄运,而不强迫新类的所有用户都知道可能在幕后细节。

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

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

表现。

想象一组覆盖虚拟基方法的类:

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

没问题? 没问题!

组装并不难理解:

  1. mov ecx, foo$ :这需要进入内存,并点击对象的虚拟方法表 (VMT) 的一部分以获取被覆盖的foo方法的地址。 CPU 将开始从内存中获取数据,然后继续:
  2. push 42 :将参数42压入堆栈以调用函数。 没问题,可以马上运行,然后我们继续:
  3. call [ecx]调用ClassB.func函数的地址。 ← 𝕊𝕋𝔸𝕃𝕃!!!

那是个问题。 尚未从 VMT 获取ClassB.func函数的地址。 这意味着 CPU 不知道下一步要去哪里。 理想情况下,它会跟随一个jump并继续执行指令,等待ClassB.func的地址从内存中返回。 但它不能; 所以我们等待。

如果幸运的话:数据已经在 L2 缓存中。 将值从 L2 缓存中取出到可以使用的地方需要 12-15 个周期。 CPU 不知道接下来要去哪里,而不必等待内存 12-15 个周期。

𝕋𝕙𝕖 ℂℙ𝕌 𝕚𝕤 𝕤𝕥𝕒𝕝𝕝𝕖𝕕 𝕗𝕠𝕣 𝟙𝟚-𝟙𝟝 𝕔𝕪𝕔𝕝𝕖𝕤

我们的程序在 12-15 个周期内无所事事。

CPU 内核有 7 个执行引擎。 CPU 的主要工作是让这 7 条管道充满要做的事情。 这意味着:

  • 将您的机器代码 JIT 转换为不同的顺序
  • 尽快开始从内存中提取,让我们继续做其他事情
  • 提前执行 100、200、300 条指令。 它将在您的循环中提前执行 17 次迭代,跨越多个函数调用和返回
  • 它有一个分支预测器来尝试猜测比较会走哪条路,这样它就可以在我们等待的同时继续执行。 如果它猜错了,那么它必须撤消所有这些工作。 但是分支预测器并不愚蠢——94% 的时间都是正确的。

您的 CPU 拥有所有这些功能和能力,而且它只是停滞了 15 个周期!?

这太可怕了。 这真糟糕。 每次调用virtual方法时都会受到这种惩罚——不管你是否真的覆盖了它。

我们的程序每次方法调用慢了 12-15 个周期,因为语言设计者让虚拟方法选择退出而不是选择加入。

这就是微软决定不让所有方法默认为虚拟的原因:他们从 Java 的错误中吸取了教训。

有人将Android移植到C#,而且速度更快

2012 年,Xamarin 人将所有 Android 的 Dalvik(即 Java)移植到 C#。 他们那里:

表现

当 C# 出现时,Microsoft 以几种重要的方式修改了该语言,使其更易于优化。 引入了值类型以允许小对象具有较低的开销,并且虚拟方法被选择加入,而不是选择退出,这使得更简单的 VM。

(强调我的)

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

有一种想法,你应该“设计继承或禁止它”。 这听起来像个好主意,直​​到你有一个严厉的商业案例来进行快速修复。 也许继承自你无法控制的代码。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM