默认非虚拟化
Bill Venners:在Java中,实例方法默认是虚拟化的,只有显式声明为final时,才可以在子类中被覆盖。相比之下,C#中实例方法默认为非虚拟化,要想声明虚拟化方法需要显式声明它为虚拟的。为什么要这样做?
Anders Hejlsberg:原因在于以下几点:
首先是性能。通过观察,我们发现用Java程序员在写代码时常常会忘记将方法声明为final,因此这些方法将是虚拟化的,所以性能并不好。相比于使用虚拟方法这样会带来性能上的提升。这是其一。
更重要的一点在于版本控制。关于虚拟方法主要有两派思想:一是学院派,他们认为“任何事物都应该是虚拟化的,因为我可能会在将来覆盖它”;务实派(从现实世界中获取开发应用的思路)认为“我们只需要关心有必要的虚拟化”。
当我们将平台中某物虚拟化时,对未来的变化做了过多的假设和保留。对于一个非虚拟化的方法,在你调用它时会发生x、y;而在API中发布一个虚拟化 的方法时,不仅在你调用该方法时会发生x、y。我们同样保证当你在覆盖这个方法时,会在这个特定的序列中调用它,注意到这些状态不变条件。
每当使用到API中的虚拟化时,你就是在创造一个回调的hook。作为一个OS或者API框架设计师,更需要注意这些。你不会希望用户在API任何部分重载和hook,因为你不一定能保证这些。同样,人们在虚拟化声明某物时也并不一定完全了解他们自己做的保证。
Incoming和Outgoing契约
Bill Venners:听起来像是说你不关心覆盖方法的人是否正确实现了对调用者的保证,你关心的是对覆盖了方法的人所做的保证。
Anders Hejlsberg:实际上,两者我都关心。虚拟化有两方面:Incoming和Outgoing。人们擅于思考Incoming的约束是什么,而对于Outgoing则没有清楚的认识。
Bill Venners:你所说的Incoming和Outgoing契约是指什么?
Anders Hejlsberg:Incoming契约是指调用方法时的约束。它保证我能在调用方法之前推导 出我需要的是什么,以及在该方法返回后该做什么。Outgoing契约是则是在覆盖方法是用到的。大多数API在覆盖虚拟化方法时,并不擅长记录你想做的 事:调用前的不变条件是什么?结果是什么才正确?那些方法你可以不必从自己的实现中调用?等等。我认为,默认所有方法都有合格的Incoming和 Outgoing契约。同样,也不是每个人都能为“何时去覆盖,此时不变条件是什么”写出优秀的文档。
我可以向你展示一个非常常见的版本控制问题,实际上这也我们是从Java的经验中发现的。每当一个新的Java版本库开发出来时都会带来破坏:当你 打算向基类引入新方法时,如果派生类有一个同名方法,并且返回的是不同的类型,那将不再能编译通过,而是出现覆盖异常。这是因为Java,C++也一样, 都没有考虑到程序员对于虚拟化的想法。
虚拟化的两个含义
Anders Hejlsberg:当你在说“虚拟化”的时候,这意味着两件事。如果你不集成同样标记的方法,这将是一个新的虚拟化方法。或者另外一种情况:它是继承的方法的覆盖。
从版本控制的焦点来看,程序员在声明一个方法为虚拟的时候,确定自己的想法非常重要。在覆盖一个已经存在的虚拟方法时,你必须说覆盖!
因此,C#并没有这些我上面提到的这样的版本控制问题,也就是我们之前介绍的基类和衍生类之间冲突的问题。也许你已经在你的类中声明了某个虚拟化方 法,现在我们引入了一个新的虚拟化方法。当然,这也没问题。现在有两个虚拟化方法了,有两个虚函数表(VTBL)槽。衍生的函数也隐藏在基函数中,但这也 没有问题。基函数甚至在写下衍生函数时就已经没有了,所以并不会隐藏新函数的时候出什么问题,一切都正常运行。
Bruce Eckel:你经常在实践中遇到这样的版本控制问题,所以想要解决它?我记得你在Delphi中也做过类似的事情。
Anders Hejlsberg:是的。
Bruce Eckel:你看待语言的角度和我采访过的其他人大不相同,非常务实。
Anders Hejlsberg:我也一直自认为是个务实的人。很有趣,因为版本控制最终会是语言设计的一大 要素。它体现于你如何覆盖C#虚拟方法。同样因为版本控制,我们C#里重载的解决方案也和我所知的其它方法不大一样。无论何时,在设计特别的功能特性时, 我们都会使用版本控制交叉检验。“在版本控制会如何改变它?从版本控制的角度来看,这个函数怎么样?”大多数以前的编程语言都没有考虑过这方面。
Bruce Eckel:你关心版本控制主要是因为DLL糟糕的问题?
Anders Hejlsberg:对,也因为我对编程语言多年来变化的观察。20-25年前,我们只有640k内存栅栏,每年都需要抛弃以前的代码,花一年时间重写一遍,正好赶上新版本发布的时间。所以你并不需要考虑重用?版本控制是什么?我们每年都需要重写一遍代码。
但是这都已经过去了,再也不会碰到这样的问题。根据摩尔定律,我们恐怕也不会用光所有的存储容量。现在我们花更少的精力就可以比原来得到更多的基础设施和应用、功能。而系统的寿命也在增长,所以版本控制变得越来越重要。