这是自己转到.NET平台的时候,断断续续翻好到一个系列,在以前的blog上也贴出来过一部分,翻译的过程中阅读了大量的其他资料并且动手做了一些小程序,加深了对.NET平台的认识也逐渐熟悉了C#,这个访谈系列是03-04年发表在artima上的,似乎稍嫌陈旧,但是Anders Hejlsberg对于C#的设计过程以及C#在语言特性上的取舍,O-R Mapping,还有C#和Java,C++在范型实现上的不同等等这些内容的讲解并不算过时,贴在这里希望对其他朋友也有所帮助。原文地址http://www.artima.com/intv/choices.html
C#访谈系列
翻译:刘晓伟
Anders Hejlsberg,微软的一位杰出工程师,他领导了C#编程语言的设计团队。Hejlsberg首次跃上软件业界舞台是源于他在80年代早期为MS-DOS和CP/M写的一个Pascal编译器。不久一个叫做Borland的非常年轻的公司雇佣了他并且买下了他的编译器,从那以后这个编译器就作为Turbo Pascal在市场上推广。在Borland,Hejlsberg继续开发Turbo Pacal并且在后来领导一个团队设计Turbo Pascal的替代品:Delphi。1996年,在Borland工作13年以后,Hejlsberg加入了微软,在那里一开始作为Visual J++和windows基础类库(WFC)的架构师。随后,Hejlsberg担任了C#的主要设计者和.NET框架创建过程中的一个主要参与者。现在,Anders Hejlsberg领导C#编程语言的后续开发。
2003年7月30号,Bruce Eckel(《Thinking in C++》以及《Thinking in Java》的作者)和Bill Venners(Artima.com的主编)与Anders Hejlsberg在他位于华盛顿州Redmond的微软办公室进行了一次面谈。这次访谈的内容将分多次发布在Artima.com以及Bruce Eckel将于今年秋天发布的一张音频光碟上。在这次访谈中,Anders Hejlsberg谈论了C#语言和.NET框架设计上的一些取舍。
· 在 第一部分:C#的设计过程中, Hejlsberg谈论了C#设计团队所采用的流程,以及在语言设计中可用性研究(usability studies)和好的品味(good taste)相对而言的优点。
· 在第二部分:Checked Exceptions的问题中, Hejlsberg谈论了已检测异常(checked exceptions)的版本(versionability)问题和规模扩展(scalability)问题。
· 在第三部分: 委托、组件以及表面上的简单性里,Hejlsberg 谈论了委托(delegates)以及C#对于组件的概念给予的头等待遇。
· 在第四部分:版本,虚函数和覆写里,Hejlsberg解释了谈论了为什么C#的方法默认是非虚函数,以及为什么程序员必须显式指定覆写(override)。
- 在第五部分:契约和互操作性里,Hejlsberg谈论了DLL hell、接口契约、strong anmes以及互操作的重要性。
- 在第六部分:Inappropriate Abstractions里, Hejlsberg以及C#团队的其他成员谈论了试图让网络透明的分布式系统,以及试图屏蔽掉数据库的对象——关系映射。
- 在第七部分:比较C#、Java和C++的范型里,Hejlsberg比较了C#和Java的范型以及C++模板的实现方法,并且介绍了C#的constraints特性以及弱类型化和强类型化的问题。
- 在第八部分:CLR设计上的取舍里,Hejlsberg谈论了IL指令,非虚方法,不安全代码,值类型以及不变量。
Designing with a Living Agenda
Bruce Eckel: 我听说C#是由几个设计师在一个房间里创造出来的。
Anders Hejlsberg: 是的。四年都是同一个房间。每周一、三、五,我们仍然在那个房间里。
Bruce Eckel: 我非常想知道C#的设计过程。我直接或间接地参与过好几种不同语言的设计。比如,在Python的设计过程中,Guido van Rossum被戏称为仁慈的独裁者。
Anders Hejlsberg: 我差不多也处于这种境地。
Bruce Eckel: 你也是C#仁慈的独裁者?
Anders Hejlsberg: 我是仲裁器(tie breaker)。一旦我们就某个问题做了足够多的讨论,而且到了需要做一个非此即彼的决定的时候,那就由我来做这个决定。但是大多数情况下这个决定是显而易见的。
Bruce Eckel: 这和Turbo Pascal以及Delphi的设计类似吗?
Anders Hejlsberg: 那两种语言的设计没这么正规。我几乎是一个人设计的Turbo Pascal。设计Delphi的时候是与Chuck Jazdzewski和Gary Whizin一道,但是这是一个很小的团队,我们并不真的需要一个正规的流程。与此不同,对于C#来说,正规的流程起到了很大作用:每周一、三、五,从1:00到3:00,我们有一个定期的安排好的会议。我们有一个活动的议程表。问题像泡沫一样浮上来,我们把这些泡泡击碎。现在我们在因特网上有一个Wiki,上面有问题列表,对应的解决方案等等。
Bruce Eckel: 那些问题是怎么样像泡沫一样浮上来的?
Anders Hejlsberg: 嗯,它们就是那样浮上来的。我们有学多渠道可以让用户提出意见——软件设计复查(software design reviews),新闻组(news groups)——通过各种各样的渠道我们得到关于这门语言的反馈。这些反馈就引出了一些问题以及Bug、前后不一致(inconsistencies)、不规范(irregularities)等等。然后我们就知道哪些是我们该做的事情了。它们最终出现在列表上,然后我们从头到尾地解决。我们会看看其中一个问题并且问道,“就这个问题我们有新的想法吗?没有新的想法?好,这个问题已经呆在这里好几周了,让我们试试看花半个小时对它进行一下攻坚,看看能否有所收获。”
Bruce Eckel: 那么当发现有些东西糟糕透顶的时候。。。。。。
Anders Hejlsberg: 或者发现某些东西很棒,你想在下一个发布版本里针对它做点什么。但是我想这个流程只是保证你的努力不会成为泡影的一种方法。你把所有的东西都放进那个列表。有些可能会在上面呆很长一段时间,而且后来可能你决定不再对它进行什么改动了。但是至少它被捕获了,而且有办法可以再次找到它。它可能会发生,也可能不会,但是不会说找不到它。
Bill Venners: C#的设计团队里都有谁? 他们都扮演什么样的角色?
Anders Hejlsberg: 最初的C#设计团队是由Scott Wiltamuth, Peter Golde, Peter Sollich, Eric Gunnerson以及我自己组成的。C#2.0的设计团队包括Peter Hallam, Shon Katzenberger, Todd Proebsting和我自己。大多数有关泛型(generics)的功劳都要归功于来自微软研发部门的Don Syme和Andrew Kennedy。
可用性研究(Usability Studies)vs. 审美需要(Aesthetics)
Bill Venners: C#的设计有多少是基于可用性研究? 多少是基于市场的考虑?又有多少是出于审美需要?
Anders Hejlsberg: 归根结底,好的语言设计最终要归结于能否召集一群有品味的人。用你的话来说,它归结于编程上的审美需要。好的品味是非常主观的东西,也很难定义,但是你看到它的时候还是能够把它辨别出来。而且我认为再多的可用性研究也不能给你品味所带来的东西,因为可用性研究更倾向于纵向的东西(vertical)。一项(可用性)研究可以这么问,“你对这个特定的功能有什么看法?”但是很难这么问,“你认为这门语言怎么样?”你从哪里切入呢?你怎么可能在两个小时的可用性研究中解决这个问题呢?实际上没这个可能。
Bruce Eckel: 人们必须(对这门语言)有深入的了解。
Anders Hejlsberg: 使用一门编程语言更大程度上是一个深入其中的过程。人们只有在使用了数月之后才可能真正欣赏某种编程语言。然后他们可能逐渐意识到,“哇,这确实很爽。”这些你不可能一蹴而就。
That being said,我们做了大量的可用性研究,但是它们更多是纵向地针对于某些特定功能。
Bill Venners: 比如说呢?
Anders Hejlsberg: 大多数实际上都是关于IDE特性(feature)的可用性研究。我们可能会问,“人们能够明白通过单击右键就可以做这些事情吗?”我们也做过纯粹针对这门语言的语法的可用性研究——举个例子说,我记得我们是针对属性(properties)和事件(events)——但实际上这些是没必要的。
我觉得对于语言特性来说,很难像针对IDE特性的可用性研究那样,得到很高的回报。IDE的交互性是非常强的。你可以观察用户点击右键菜单,然后得到合适的反馈。对于编程语言来说,存在更多的问题,“从概念上来说,这个容易理解吗?”拥有一个用户咨询委员会、反馈委员会可以很好地解决这个问题。你就有地方可以陈述,“这是我们正在考虑针对这个特定的新特性要做的东西,你们都是怎么看的?”然后你明确地催促他们提尽可能多的意见,因为在加入这个新特性之前知道(这些意见)总比加入之后要好的多。所以说除非对某个语言特性像扣篮那样完全有把握,否则我们倾向于更多地倾听反馈委员会的意见。
第二篇: Checked Exceptions的问题[Top]
对已检测异常(checked exceptions)保持中立
Bruce Eckel: C#没有已检测异常(checked exceptions)。你是如何决定要不要把checked exceptions加入C#的?
Anders Hejlsberg: 我觉得checked exceptions有两个大问题:规模扩展(scalability)和版本(versionablility)。我知道你也写过一些关于checked exceptions的东西,而且你也倾向于我们的看法。
Bruce Eckel: 我曾经以为checked exceptions是非常棒的。
Anders Hejlsberg: 确切的说,乍看起来它们是非常棒的,这个点子并没有什么错。我完全同意checked exceptions是一个很不错的特性。可能有问题的只是那些特定的实现。比如,checked exceptions在Java里的实现方法,我认为你只是用这一堆问题替代另外一堆问题。最终我还是不清楚你是否真的让这一切变得更容易了。你只是换汤不换药罢了。
Bruce Eckel: 关于checked exceptions,在C#的设计团队里有很多不同意见吗?
Anders Hejlsberg: 没有,我想我们的设计团队就这个问题在很大程度上是一致的。对于checked exceptions这个问题,C#基本上是保持沉默的。一旦以后有了好的解决方案——相信我,我们还在考虑这件事情——我们会回头给它添加一些适当的东西。我深信,如果你不知道什么是正确的,或者不知道前进的方向,那你最好保持沉默和中立,而不是试图设计一个框架。
如果你要一个编程新手写一个日历控件,他们经常会在心中盘算,“哦,我要写一个世界上最好的日历控件!它应该根据日历的类型而展示出多态。它应该有displayers和mungers,应该有这个有那个,还应该有其它的东西。”他们需要两个月才能发布一个日历程序。他们把所有这些基础设施都放到控件里,然后花两天时间在它上面写一个蹩脚的日历程序。他们想,“下一个版本的程序里,我会做更多这样的事情。”
一旦开始考虑如何从实际上实现他们的抽象设计中其它需要具体化的东西,他们就会发现自己的设计彻头彻尾的错了。他们把自己把逼进了死胡同,他们必须把所有的东西都扔掉。我曾经一次又一次地目睹这种事情。我是个极简主义的信徒。除非你真的要解决通用问题,不要试图为了解决一个特定的问题而整出一个框架,因为你不知道那个框架看起来应该是什么样子。
Bruce Eckel: 极限编程的程序员们说,“尽可能地做简单的事情,能正常运行就可以了。”
Anders Hejlsberg: 是的,爱因斯坦说过,“尽可能地做简单的事情,但是不要过于简单。”关于checked exceptions,我所关心的是,它给程序员戴上了手铐。你会看到程序员们捡起这些包含Throws语句的API,然后你会发现他们的代码有多么的错综复杂,再然后你意识到checked exceptions根本没有帮上他们什么忙。这在一定程度上是因为这些独断专行的API设计者告诉你应该如何进行异常处理。他们不应该这么做。
Checked Exceptions的版本问题
Bill Venners: 你曾经提到了与checked exceptions的规模扩展(scalability)和版本(versioning)相关的问题。你是否可以就这两个问题阐明一下您的意思?
Anders Hejlsberg: 那就让我们从versioning开始吧,因为这个问题是显而易见的。比如说,我创建了一个叫做foo的方法,并且声明抛出异常A、B、C。在Foo的第二个版本里,我想要添加一组功能,现在foo可能会抛出异常D。我给那个方法的throws语句添加异常D导致了一个破坏性的变更,因为那个方法的既有调用者几乎肯定不会处理这个异常。
在throws语句新版本里添加一个新的异常打断了客户端代码。这有点像给一个接口添加一个方法。在你发布一个接口之后,从实际的角度来说你无论如何都不应该再改动它,因为它的任何一个实现都有可能包括你在下个版本里想要加入的方法。所以说你最好还是另外创建一个新的接口。异常与此类似,你要么创建一个全新的方法叫做foo2,让它抛出更多的异常,要么你在新的foo版本里捕获异常D,然后把D转换成A、B或者C中的一个。
Bill Venners: 但是即便是一门不支持checked exceptions的语言,这种情况下你不也是在打断他们的(客户端)代码么? 如果foo的新版本将要抛出一个新的异常,而客户端又有必要处理这个异常,但是事实上他们写代码的时候并没有预期到会有这个异常,这不还是要打断他们的代码么?
Anders Hejlsberg: 不,因为很多情况下人们并不关心(这些异常)。他们原本就没打算处理这些异常。在消息循环机制中,有一个最底层的异常处理器。这个异常处理器会弹出一个对话框说哪里出错了然后程序继续运行。程序员通过书写try finally语句来保护他们的代码,这样当出现异常的时候才能正常退出,但是他们并不真正想要处理这些异常。
至少Java实现throws语句的方法并不强制你处理异常,但是如果你真的不处理它们,它会强制要你明确指出哪些异常可以通过。它会要求你要么捕获已经声明的异常,要么把它们放进你自己的throws语句。为了满足这些条件,人们要做非常荒谬的事情。例如,他们给每个方法都加上,“throws Exceptions”。这恰恰彻底削弱了checked exceptions的特性,你只是让程序员写更多罗嗦的废话。这对谁都没好处。
Bill Venners: 也就是说你认为更为一般的情况是,相对一个通常来说深入调用堆栈的catch语句而言,调用者并不显示地以不同的方式处理异常?
Anders Hejlsberg: 很有意思的是,人们认为对于异常来说,重要的事情是如何处理它们。实际上这不是异常最重要的东西。在一个编写良好的应用程序里,以我的看法,try finally与try ctach的比例是10比1。或者在C#里,using staement (译注:参见《Applied Microsoft .NET Framework Programming》第19章) ,它跟try finally很像。
Bill Venners: finally语句里都有些什么东西呢?
Anders Hejlsberg: 在finally语句里,你保护自己免受exceptions的干扰,但是你并不真正处理它们。错误处理会放在其它的地方。在任何类型的事件驱动的程序里,诸如时髦的UI程序,你通常会围绕你的主消息循环泵放上一个异常处理器,只有当它们跃出那个范围你才处理异常。但是要让自己全身而退你得确信是通过释放已经占有的资源等等来达到目的的。谁都不想在一个程序的100个不同地方处理异常并且弹出错误对话框。假如说你想要改变一下弹出对话框的方式那可怎么办呢?这可糟透了。异常处理应该被集中在一个地方,而且当异常向外传递给它的处理程序的时候你只要保护好自己的程序就可以了。
Checked Exceptions的可伸缩性问题
Bill Venners: checked exceptions的scalability问题是指什么呢?
Anders Hejlsberg: Scalability问题多多少少和版本问题是有关系的。从小的方面来说,checked exceptions是非常有诱惑力的。只要用一个很小的例子,你就能展示给大家说你已经检验到捕获了FileNotFoundException异常,这不很棒么?是的,当你只是调用一个API的时候这样做没有问题。但是当你开始构建大的系统而需要和四五个不同的子系统打交道的时候问题就来了。每个子系统抛出4到10个异常。现在,每次当你沿着这个聚合而成的阶梯往上的时候,你的下面就有了指数级的异常层次(hierarchy),这些异常是你必须要处理的。最终你不得不定义40个可能抛出的异常。一旦你要把这个系统与另外一个子系统聚合在一起,在你的throws语句里就有了80个异常。异常的数量会失控般的激增。
从大的方面来说,checked exceptions变成了让人恼火的东西,人们试图完全绕过这个特性。他们要么到处写成,“throws Exception”;要么——我记不清已经见到过多少次了——写成,“try, da da da da da, catch curly(花括号) curly(花括号)”他们想,“以后我会回来处理这些空的catch语句”,当然他们再不会回来补上。在这些情况下,从大的方面来讲,checked exceptions确实降低了系统的质量。
所以说,当把以上这些问题都考虑在内的话,对我来说,在把类似于checked exceptions的机制加入C#之前,我们可得需要更多的考虑。但是尽管如此,知道哪些异常可以被抛出、以及用一些工作来检测这些异常,仍然毫无疑问有着巨大的价值。我不认为我们可以很快构建出牢靠和易用的规则,它要么是一个编译错误,要么不是。但是我想我们应当可以就分析工具做一些文章,侦测可疑的代码,包括未被捕获的异常,进而为你指出那些潜在的漏洞。
第三篇:委托、组件以及表面上的简单性[Top]
简单(Simplicity)vs.表面上的简单(Simplexity)
Bill Venners: C#与Java的一个不同之处是它向感兴趣的对象传播事件的方式。Java使用实现了listener接口的类(通常是内部类)。C#使用委托(delegates),更有点像函数指针。为什么要选择委托呢?
Anders Hejlsberg: 让我先说说通常我是如何看待简单性(simplicity)的。从来没有人争论说简单不好,但是人们给简单下的定义千差万别。有一种简单性(simplicity)我喜欢叫做“表面上的简单(simplexity)”。当你试图把一些极其复杂的东西包裹(wrap)到相对简单的东西里的时候,通常你只是隐藏了(shroud)复杂性。实际上你并不是在设计一个真正简单的系统。从某种意义上说你把它弄得更复杂了,因为现在用户必须弄明白你省略掉了哪些东西而说不定什么时候他们又需要用上。这就是“表面上的简单(simplexity)”。对我来说,简单性必须是真正意义上的,也就是说你越往下探究它就越简单。而不应该是你越深入探究它就越复杂。
委托(Delegates)vs.接口( Interfaces)
Anders Hejlsberg: 委托添加了一种类和接口所没有的表达方法,这也是为什么我认为它们是重要的。走在我们前面的那些编程语言意识到了它们的重要性。它们有许多名称:函数指针,成员函数指针。在LISP里它们被叫作闭包(closures)。当归结为一点的时候,它们就是函数式编程(functional programming)的全部。它们非常有用。
Bill Venners: 怎么有用呢?
Anders Hejlsberg: 实际上你可以用接口来做委托可以做的事情,但是你得做很多例行公事性质的事情。举个例子,让我们看看你在Java里和.NET世界分别是如何处理事件的。因为Java里没有委托,所以你最终选择使用接口。
你定义一个接口用以代表你的所有事件。这个接口可以声明一个、两个、三个、四个方法,或者更多。好了,现在有一个问题。如何结构化它们并不十分清楚。你要为自己所分发的(outgoing)事件准备多少个接口?你是要每个事件一个接口呢还是要所有事件都用一个接口?没有人提供明确的指导,而且有时候它会在某些地方陷入中间状态。现在,为了处理由组件(component)所产生的事件,你必须实现这个接口。当然如果你想要处理来自不同组件的同一组(set)事件,你必须得实现这个接口两次,而这是办不到的。这种情况下你需要创建一个适配器(adapter)。这么一来那些例行公事性质的事情就开始在你面前晃悠了。
使用内部类可以减少一些繁文缛节,但是尽管如此,使用接口还有一个问题就是事件的接受者必须知道他在接受事件。他必须显式地实现listener接口。与此形成对比的是,使用委托只要签名(signature)一致,你就可以把它们对接在一起(slot together)。处理事件的人不需要关心他是如何被调用的。这只不过是个方法(method)罢了。
Bruce Eckel: 有点弱类型化(weaker typing)的意思。
Anders Hejlsberg: 是的,实际上它就是。
Bruce Eckel: 也就是说它更灵活了。
Anders Hejlsberg: 是的,确实如此。它完全取决于你是否有一致的签名(signature),也就是参数列表。如果你有一致的参数列表,你就可以把它们捆绑在一起。而且从概念上它完全符合最终用户对于回调(call back)的期望,对吗?给我一些参数,然后我(用它们)写一些代码。对我来说听起来更像是一个方法。抱歉,我是想要给你一个关于那个方法的引用,这实际上就是委托。
Bruce Eckel: 而且最终你并没有丧失类型检测。类型检测是在运行时刻做的,对吗?
Anders Hejlsberg: 不,更多情况下是在编译时刻。当构造一个delegate的时候,你会得到C++程序员可能称之为绑定的成员函数指针的东西。Delegate引用特定对象的一个方法。如果那个方法是虚函数,你可以准确地找出需要的是哪个函数。所以从某种意义上来说,在delegate被构造的时候就你可以确定是哪个虚函数。通过delegate所完成的调用从字面上看只不过就是一个间接的调用指令。
Bruce Eckel: 其它地方并不需要间接调用了。
Anders Hejlsberg: 你说的对,你可以在构造delegate的时候一劳永逸地解析出虚函数表(VTBL)的间接调用,以后每次通过delegate调用的时候就可以直接找到那个方法。所以说,委托不仅仅比接口分派(interface dispatch)效率更高,而且它们也可能比常规的方法分派(method dispatch)效率高。
Bruce Eckel: C#还有组播委托(multicast delegate),也就是说一个delegate可以触发多个函数被调用。它是一个正交(orthogonal)特性吗?
Anders Hejlsberg: 组播(Multicast)是一个完完全全的正交特性。说实话,我对组播是否是非常好的东西持中立的态度。我认为确实有用得着它的地方,但是我也觉得我们应该保守一点认为所有的delegates都是单播的(single cast)。有些人发现多播非常重要,而且它在一些地方工作的很好,但是绝大多数情况下委托都是单播的。实际上,我们把系统构建成了只有在你使用多播的时候才需要为它付出一定的代价。
Bill Venners: 您刚才说的简单(simplicity)和表面上的简单(simplexity)如何适用于委托呢?简单性在哪里?表面上的简单又在哪里?
Anders Hejlsberg: 如果你用接口来模拟委托,那你最终会陷入繁文缛节和一大堆适配器里。实际上,看看任何捆绑JavaBeans的工具就知道了。它们产生一大堆适配器然后告诉你,“不要改动下面这写代码。我们会为你产生这些古怪的辅助类。”我认为,这就是表面上的简单。整个系统不是真正意义上的简单,实际上非常复杂,但是如果你只是瞄一眼的话看起来是很简单。
组件是头等的概念(Components are First-Class)
Bill Venners: 在公布在O’Reilly网络上的一篇访谈中,为了说明C#对于属性(properties)和事件(events)的支持是合理的,你说道,“现今开发者都在构建软件组件(components),而不是自成一体的应用程序或者类库。每个人都是在构建新组件,而新组建继承自由宿主环境所提供的某些基础组件。这些组件覆写(override)某些方法和属性,处理一些事件,然后再把组件放回去。把这些概念当作first classs是很重要的。”
我希望能更好地理解你这段话的意思,因为我一直认为自己是在构建类,而不是组件。你指的是为了让其他人以Delphi、Visual Basic或者JavaBeans的方式连接到一起而构建组件的人们?你说的组件是这个意思吗?
Anders Hejlsberg: 组件(component)这个词最大的好处就是你可以拿它来到处忽悠,而且它听上去也很棒,但是我们所思考的并不是同一个东西。最简单的形式,提到组件的时候我的意思是一个类再加一些东西。组件是一个自包含的(self-contained)软件单元,不仅仅是代码和数据。它是一个通过属性(properties)、方法和事件来暴露自己的类。它是一个有额外attributes与它关联的类,这些attributes的形式是元数据(metadata)或者naming patterns或者随便其它什么形式。这些attributes提供一些动态的额外信息,诸如组件是如何嵌入一个特定的宿主环境、它如何让自己持久化——所有你想通过元数据进行说明的额外的东西。通过元数据,IDE可以智能地推断出某个组件是干什么的然后显示给你关于它的文档。组件把所有这些都整合起来。
Bill Venners: 当使用Java的时候,我觉得是我是在设计类库,而不是组件库,可能是因为get和set太不优雅了。我确实使用了get和set,而且我的确也触发事件(fire events),但是我原本并没有打算让这些类在一个Bean构建器的IDE里使用。我想象中它们应该被那些手写代码的人使用。所以我仍然怀疑有多少人在实际上写类似于JavaBean的组件以及这是否是大势所趋,因为依我自己的经验我并没有见到很多。
Anders Hejlsberg: 现今主流的面向对象编程语言都是混血儿。它们有许多结构化的编程方法在里面。对象在很大程度上还是添加了一大堆方法和一个this指针的结构(structs)。我想,从概念上来说,当你想到一个对象或者一个组件的时候,它们绝对应该有属性也绝对应该有事件。给编程语言里的这些东西头等待遇可以让一切更简单。
人们可能会争论说C#对于属性和事件的支持只不过是语法糖衣(syntactic sugar)罢了,但是所有的东西不都是语法糖衣吗?对吗?对象也只是语法糖衣。我们可以自己弄一个虚函数表(VTBL),用C里面的宏(macros)就可以了,不是吗?毫无疑问,你可以用C写出面向对象的程序。只不过是非常复杂罢了。类似地,你可以用C++或者Java写组件,但是因为核心概念(core concepts)在这两种语言里没有作为第一等(first class)的东西,所以你最终还是得借助于别的东西。这里又说到了这个词(指first class)。就properties来说,它们并不是真正意义上的properties,它们是getBla和setBla。但是当它们在属性检视器里显示出来的时候,它们是bla。你只需要知道那里有一个映射(mapping)就可以了。
很明显,组件是一种趋势。我们就是以这种方式使用类的,但是在大多数我们与组件打交道的语言里,它们并没有作为头等的概念。我只是想说它们应该成为头等的概念。
我们谈论PME编程模型——属性(properties)、方法(Methods),事件(events)——已经有很长一段时间了,而且我们在日常编程中都使用过它们。为什么我们不实实在在地在编程语言里给它们头等的待遇呢?
第四篇:版本、虚函数和覆写[Top]
非虚方法(Non-Virtual)是默认的
Bill Venners: 在Java里,类的方法默认是虚的——只要不被显式声明成final,它们就可以在子类里被覆写(overidden)。与此不同,在C#里,instance方法默认都是非虚的。要想让一个方法成为虚方法,程序员必须显示把它声明为virtual。C#为什么要把非虚方法作为默认的呢?
Anders Hejlsberg: 有几个理由。一个是性能。我们观察到人们用Java写代码的时候,忘了给他们的方法加上final关键字。于是,这些方法就成了虚方法。因为它们是虚方法,它们的性能就不那么好。作为一个虚方法总会带来一些性能上的负担。这是问题之一。
更重要的一个问题是版本问题。关于虚方法有两派不同的意见。学院派认为,“所有的东西都应该是虚的,因为说不定什么时候我就想要覆写它。”实用主义学派(他们的观点来自于构建运行于现实世界的真实程序)认为,“对于要声明成virtual的东西我们应该格外小心。”
当我们在某个平台上把某些东西弄成virtual的时候,我们是在就它将来如何演化做出非常多的承诺。对于一个非虚方法,我们承诺当你调用这个方法的时候,x和y可以发生就可以了。当我们在一个API里发布一个虚方法的时候,我们不仅仅要保证当你调用这个方法的时候,x和y可以发生。我们还要保证当你覆写(override)这个方法的时候,我们会根据其它被覆写的函数按照特定的顺序来调用它,并且它的状态会放在某个不变量里。
每次当你在某个API里提到virtual的时候,你就是在创建一个回调的挂钩(call back hook)。作为一个操作系统或者API框架的设计者,你必须对这个问题格外谨慎。你不希望用户在一个API里的任意地方覆写并且实现挂钩程序,因为你没法做出那些承诺。而且人们在把某些东西弄成virtual的时候,可能没有完全明白他们需要为此做出的承诺。
进来的和出去的约定(Incoming and Outgoing Contracts)
Bill Venners: 听起来你并不十分关心覆写方法的人没有正确地实现你对于调用者所做出的承诺。你所担心的是你必须要对覆写这个方法的人所做出的种种承诺。
Anders Hejlsberg: 实际上两个我都关心。就这个问题来说,Virtual有两个方面:进来的(Incoming)和出去的(Outgoing)。对于思考进来的这方面所应遵循的约定(contract)大家都毫无问题。而对于出去的方面所应遵循的约定就非常差劲了。
Bill Venners: 进来的和出去的约定,你是指什么?
Anders Hejlsberg: 进来的约定是当我调用一个方法的时候需要用到的。它让我思考在调用一个方法之前需要做什么以及当方法返回以后都发生了些什么。出去的约定是当我覆写(override)一个方法的时候需要用到的。如果你看看大多数API,实际上它们对在你要覆写一个虚方法的时候需要做些什么所给出的文档是很差劲的:在你调用之前不变的是什么?调用之后哪些该为真(true)?哪些方法是不能通过你的实现来调用的?诸如此类。我想把所有方法都包括进来的和出去的方面作为默认情况是非常危险的。轻信每个人都会写清楚文档告诉你什么时候该覆写以及覆写的时候哪些是不变的,是非常危险的。
我可以示范给你一个真正现实世界中的版本问题,这是我们从Java的经验中真真切切看到的一个问题。无论何时他们发布一个Java类库的新版本,前后兼容性就被打破了。无论合适他们在基类里引入一个新方法,如果有人在派生类里有一个与之同名的方法,这个方法就被覆写了——除非它有一个不同的返回类型,否则编译就不会再通过了。问题是Java(以及C++)没有捕获程序员使用virtual的时候的意图。
Virtual有两层意思
Anders Hejlsberg: 当你说“virtual”的时候,你可能指的是两层意思中的一个。如果你没有继承一个具有同样签名的方法,那么这就是一个新的虚方法。这是第一层意思。否则它就覆写了被继承的方法。这是第二层意思。
从版本的角度来看,当声明一个方法为vritual的时候,让程序员说明他们的意图是很重要的。比如,在C#里,你必须显式说明自己想要使用的是virtual的哪一层含义。要声明一个新的虚方法,只要给它加上virtual就可以了。但是要覆写一个现有的虚方法,你必须给它加上override关键字。
事实上,C#并没有上述的版本问题,即便是在基类里引入一个已经在派生类里定义过的方法。在派生里里,假设你已经定义了foo为虚方法。现在我们(在基类里)引入了一个新的虚方法foo。好的,这没什么问题。现在有了两个虚的foo方法,但是虚函数表(VTBL)里也有两个入口。派生类的foo遮挡了基类的foo方法,但是这并没有什么问题。写派生类的foo方法的时候基类的foo方法还不存在呢,所以遮挡掉这个新的虚方法并没有什么问题。一切都按照原本预定的方式照常进行。
Bruce Eckel: 也就是说你在实践中看到这个版本问题多次发生,于是你就决定处理掉它?我记得你在Delphi里也做了类似的事情。
Anders Hejlsberg: 是的。
Bruce Eckel: 你对于编程语言的观点与我所交谈过的其他人有很大的不同。你的观点非常倾向于实用主义。
Anders Hejlsberg: 我经常把自己说成是一个实用主义者。很有意思,因为版本(versioning)最终成为支撑我们语言设计的柱石。如何在C#里覆写虚方法体现了这一点。另外,在C#里重载解析(overload resolution)的方式也区别于其它任何我所知道的语言,这也是出于versioning的原因。每当我们想要设计一个特性的时候,我们都会针对versioning反复斟酌一下。我们会问问,“versioning对此有什么影响?从versioning的角度来看它会如何运行?”事实上,以前大多数的语言设计很少从这个角度考虑问题。
Bruce Eckel: 你关心版本问题,主要是因为DLL hell问题吗?
Anders Hejlsberg: 是的,但是还源于我这些年的观察。十到十五年前,那时候我们的应用程序最多只有640K的内存可用,每年你都可以把所有的代码扔掉然后重写它们。重写大概需要一年的时间,这正好和下一个发布周期相吻合。所以你并不需要担心能否重用以前的东西。版本?那是什么东西?我们可是每次都从头开始。
很抱歉,我的回答打断了你的问题,但是我说的那个时代已经过去了。我们再也跟不上了,摩尔法则所带来的容量增长我们永远也用不完。现今,我们是通过越来越多地利用现有的基础设施和应用程序,得到更多的功能。随着系统的生命周期越来越长,版本问题就变得越发重要。
第五篇:契约和互操作性[Top]
DLL Hell以及契约理论
Bill Venners: 在多大程度上可以说“DLL Hell”在实际工作中完全是一个失败的接口约定? 对于某个特定DLL,如果每个人都完全理解并且遵循它的函数约定,那么更新这个DLL,从理论上来说就不会破坏任何代码,对么?
Anders Hejlsberg: 地狱里有很多酷刑。DLL Hell一方面的问题是你并没有遵循自己所承诺的语义上的约定。你做了一些和以前不一样的事情,然后就破坏了代码。实际上,这可能并不是我们所面临的最大问题。DLL Hell的真正问题是,我们并不允许你在一台机器上拥有某个特定DLL的多个版本。一旦你更新了这个DLL,你就更新了所有人的程序,这可是个巨猛无比的大锤子。
Bill Venners: 但是如果大家都遵守约定,难道最近的更新不是应该可以满足这个DLL的所有使用者么?
Anders Hejlsberg: 理论上说,是的。但是任何改动都是潜在的破坏性的改变。即使是修复一个Bug也可能会破坏一些人的代码,比如说这些代码依赖于这个bug。按照最严格的定义,你会发现一旦软件发布以后你就不能再做任何改动了。
引入版本(Versioning)是为了在正确的方向上放宽这些规则,并且有回旋的余地。勿庸置疑,能够保证不破坏任何东西的唯一方法就是什么都不改变。因此,针对同一功能的不同版本,在同一个box里甚至是同一进程内,支持并行的(side-by-side execution)执行是非常重要的。并行执行(side-by-side execution)是我们在.NET里支持的一个选项,而在老的DLL模型里我们是不支持这一点的。
Strong Names in .NET
Bill Venners: 我猜想你使用strong names是为了鉴别所需要的库的版本。Strong names是如何工作的?
Anders Hejlsberg: Strong names有一个逻辑部分和一个物理部分。逻辑部分由名字空间和类名称组成。实际上,在公共语言运行时库(CLR)看来,名字空间并不真正存在。我们可以假装名字空间存在于编程语言里,但是在CLR看来,一个类名是可以含有点号的。这就是类的逻辑名称。物理名称包括类代码所在的assembly的名称、版本号、locale、以及与之相关联的密钥。
你可以通过特定的版本号或者特定的strong key来指代特定assembly的特定类型。你可以完全保证要么准确地拿到你所编译的实现版本,要么什么也拿不到。或者,你可以放宽一部分strong name。你可以说,“我要比这个版本新的任何版本。”
强制性的语义约定
Bruce Eckel: 关于通过某些方法做强制性的语义约定,你是否有过一些有意思的想法?
Anders Hejlsberg: 我认为最有希望会成功的点子已经存在了,关于前置条件和后置条件、断言、不变量等等也有新的进展。微软研究院有几个正在进行的项目,我们经常会对这些项目进行评估。我们也看过一些非常具体的建议。最后我们意识到——这一点几乎对于任何真正重要的特性来说都是如此——你不可能仅仅从一门编程语言的角度来做强制性的契约编程。你必须把它弄进基础设施、CLR以及公共语言规范(Common Language Specification),这么一来,也就是说所有其它语言都要支持。如果对于某个接口的实现者来说,是否需要包含不变量的代码是可选的,那么把这个不变量放入接口还有什么意义呢?所以说,这个问题实际上更应该是类型系统级别,而不是编程语言级别的,但是这并不意味着我们就不打算考虑这个问题。下一个发布版本不会处理这个问题,但是它会是C#以后版本的一个头等重要的特性。
互操作性
Bill Venners: 你曾经说过,Java试图做到的是平台独立,而.NET所追求的却是互操作性。说到互操作性,你是指什么?
Anders Hejlsberg: 互操作性有好几个方面。首先,它指.NET里不同编程语言之间的互操作性。我们的设计目标是,.NET CLR应该是语言中立的,这样它就能支持多种编程语言。与此不同,Java仅仅是一门编程语言和针对这门编程语言的执行机器。CLR支持许多只在某些编程语言里才有的特性,比如指针。C#和managed C++的不安全代码支持指针,而Visual Basic和Jscript不支持。但所有关于指针的基础设施都在CLR里面。语言的互操作性基于我们在做了许多尝试之后认识到,我们不应该试图说服每一个人只使用一种编程语言来编程。老实说,业界之所以向前发展并且有所创新,要得益于新创造的这些编程语言。我们不但不会试图阻止这件事情,而且还会给予它更多的鼓励。
互操作性的另外一个方面是指与现有系统的互操作。我们以前摆弄Java的时候,对于我们的Java虚拟机我们试图提高的一件事情就是与已有代码的互操作性,因为我们觉得这种互操作性太重要了。现在我们还是这么认为的。因此,在CLR里,我们把与DLLs、COM、OLE自动化的互操作性看得极为重要,因为在.NET以前所有的代码都是基于这些技术写的。
这就又回到了我前面[In Part IV]所说的,现今最重要的是利用已有的东西。我们必须为程序员找到利用现有系统和代码的方法。要达到这个目标就意味着需要很强的互操作性,因为除此之外你还能怎样做到利用现有系统呢?这使得我们完全站到了Java观点的对立面,Java认为这个世界是“100%纯的”。而我认为我们所做的是正确的事情。我确实是这么想的。
如果你把从C#里调用DLL的感觉和从Java调用做一个比较,很快你就会明白我的意思。JNI用起来很复杂,我觉得很遗憾,他们没把这件事情做得更好,因为确实还有很多东西可以做。当你需要与其它系统进行互操作的时候,如果你处在一个更低级的世界,并且很可能比直接使用C或C++编程更为复杂,那就没有什么意义了。你必须得跑一大堆工具,产生一些头文件。还得记得调用这个调用那个。如果你想要给一个对象去引用(dereference),最好记着先给它加锁,否则程序运行的时候可能会有千分之一的情况,当垃圾收集器碰巧清除它的时候,它就被移动了。然后你就再也不能重新产生了。如果某个系统可以做垃圾收集和类型安全,以及更多的事情,为什么它不能帮你做互操作呢?看起来这是一件再合理不过的事情了。
Bill Venners: 以我的视角,从外部来看微软和Sun,我注意到两个公司有一种文化上或者说哲学上的不同,我想这种不同影响到了Java和.NET的设计。在微软,我感觉有种占主导地位的观点,就是写软件是用来给硬件(盒子,设备等等)编程的。我想,考虑到微软是如何赚钱的,这就是一种完全合理的观点。与此不同,尽管“网络就是计算机”是Sun的一个市场宣传口号,但是我发现这种态度在很大程度上确实存在于Sun的文化里面。网络(而不是盒子)提供服务,而Java就像是一个位于网络栈之上的面向对象层,它的目标是通过抽象去除连接到网络上的异构性。
Anders Hejlsberg: 有意思的是,现实世界中稍微有点来头的任何Java方案都有与特定平台相关的东西在里头。我没有听说过任何东西是不依赖于其它组件的纯的Java。Web上所有东西都依赖于Apache或者其它的Web服务器、数据库,以及与这些系统的某种形式的互操作。认为整个世界是纯Java是很搞笑的。整个世界是指把许多系统放到一起并且让它们跑起来。这也就是为什么web services让大家如此激动,因为它们有很棒的方法来进行互操作。我们认为互操作性有许多种形式,而我们应该尽我们所能使得系统之间可以更好和更容易地进行互操作。
Bill Venners: 实际上,你刚才所描述的情况正是我的Artima.com服务器所使用的东西。我用Tomcat跑JSPs,而Tomcat与Apache进行交互。
Anders Hejlsberg: 你大概还有个数据库。
Bill Venners: 是的,我的Java程序也与数据库进行交互。因为Java API的实现用到了JNI,从这种意义上说它确实有平台相关的东西在里面,但是我的程序代码都是Java。而且即使是Tomcat与Apache和数据库的连接器也都是纯Java的,因为它们使用sockets而不是JNI来进行互操作。
Bruce Eckel: Python有一点让我非常喜欢,如果你想要平台无关那就平台无关,如果你想与平台打交道的话,也可以。
Anders Hejlsberg: 是的,确实如此。是否平台无关应该由你来选择。
第六篇:不合适的抽象层次[Top]
松耦合的分布式系统
Bill Venners: 在O’Reilly网站上的一篇访谈里你说,“当一开始坐下来设计.NET框架的时候,我们往回退了一步,研究了一下Web上到底发生了哪些事情。它已经变成了松散连接的、分布性非常强的世界,我们试图弄明白这对于底层的编程模型有何影响。于是我们从头开始设计的时候就假定分布式程序是以松散连接的、无状态的(stateless)形式来构建的,这种形式能够给你极大的可伸缩性(scalability)。你只要在规模上进行伸缩,多拿一些部件把它们插上就可以了。 一旦你做了这个基本的假设,所有一切都随之改变了。”这个假设到底改变了什么呢?
Anders Hejlsberg: 5年前或者10年前,关于如何构建分布式系统,占统治地位的思想是CORBA、IIOP以及对象请求代理(object request brokers)。那时候甚嚣尘上的观点是把整个世界都看成是对象,特别地,再有一些基础设施(infrastructure)用以掩盖这些对象是分布在不同地方的这个事实。近乎异想天开的想法是,你只要写Object obj = CreateMeAnObject(),然后调用 obj.ThisMethod(), obj.ThatMethod(),而不用知道这个对象是在泰国还是隔壁房间,或者是在同一个进程里。这种编程方法的问题在于:在同一个进程里它跑得好极了;跨越多个进程也跑得不错;在一个小的intranet里也还行;但是在更大的空间里它就完全糟透了。
如果你隐藏掉消息穿越网络这个事实,并且不知道它们什么时候穿越网络,最后你得到的只能是唠唠叨叨的会话。于是在一瞬间,即使是以光速传递消息对你来说也成了个大问题。你不应该在一个对话里使用一个在纽约的对象,调用obj.LetMeGetX(),obj.LetMeGetY(), obj.LetMeGetZ()。不,你应该写成obj.LetMeGetXYAndZ(),然后在一个数据块(chunk)里返回所有东西。但是除非你能让大家明白他们是在构建分布式系统,否则你就不可能真正做到这一点。换句话说,你不应该试图假装一个远程对象就是本地对象,因为它们之间存在不同之处。这也正是web services值得称道的一个地方。
而且,web services是跑在我们知道可伸缩的已有的基础设施之上的。基于HTTP的web services使用的是和我们每天都要用到的浏览器完全一样的基础设计,只不过它是机器到机器的通信罢了。我们非常清楚应该如何对其进行规模上的伸缩。如果我们知道应该如何进行扩展,这就够了。那为什么不理用它呢?Web services就是这么做的。而另一方面,我们对于如何针对CORBA系统随着地理范围的增加进行规模扩展所知甚少。我们不会这么做。因为关于这方面的知识很少,而且我也从来没有听说过有谁做得特别成功。
无状态的方式(The Stateless Fashion)
Bill Venners: 刚才你说,“分布式程序都是以松散连接的无状态的方式构建的。。。。。。”,“无状态”是指什么呢?
Anders Hejlsberg: 如果你通过远程对象(remote-objects)的方式进行编程,无论什么时候初始化一个新对象,你都有可能最终得到一个指向远程对象的代理,而远程对象实际上在分布式系统的某台机器上的内存里被初始化。只要你在本机持有(远程对象)的引用,这就足以让远程对象所在的机器处于活动状态。这有可能让你进入非常长的事务处理——这些事务处理可能让远程的机器长时间内处于某种状态。这对于失效转移(failover)的情形来说是个问题,对于系统伸缩性来说也是个问题,因为你不可能很容易地把它们弄成分布式系统。一旦你连接了远端对象所在的机器,每次有调用进来的时候,你都要回到那台机器。
而通过HTTP,从某种意义上来说,你不得不解决HTTP本来就是无状态的这个问题。因为系统并不记录有关通道(channel)的信息——通道自己也没有状态——你不得不把自己的系统设计成每当有请求进来的时候它都可以被自由地路由给任何可以取到它的状态的CPU,然后这个CPU迅速处理一下再把它送回来。这样下次请求来的时候你就可以处理别的了。
Bill Venners: 对于服务器来说,并非一定的是无状态的。状态都被保存了。
Anders Hejlsberg: 服务器上有状态,但是分布式机制并不维持任何状态使其处于活动之中。
Bill Venners: 我不确定自己是否理解了两者的区别所在。两种情况下都存在状态。分布式机制不包含状态有什么好处呢?
Anders Hejlsberg: 很有意思。在某种意义上,web service替你初始化一个新的对象,然后持有它,并调用它的方法。Web service只是一个入口点。所有要进入内部的状态必须由你传入。所有要传出来的状态,由web service传回给你。然后它自己就把这个状态忘掉了。这与在本质上保持对象处于活动状态是不一样的。Web service对于会话(session)没有提及。
Bill Venners: 在标准里面是没有提及session。
Anders Hejlsberg: 是的。
Bill Venners: 但是通常会有一个会话位于服务器上。
Anders Hejlsberg: 当然。但是最终由你来决定这个会话如何工作。我们不会告诉你说必须有一个特定类型的会话概念,这个概念要求客户端初始化一个新的对象,只要它们还持有这个对象,在服务器上对象的状态就是活动的。你最好自己想办法让它工作。
对象-关系映射(Object-Relational Mappings)
Bruce Eckel: .NET框架是如何支持对象持久化的?
Anders Hejlsberg: 没有哪种对象持久化的方法可以满足所有人。有时候你想要持久化是因为你想把一个对象on the wire,把它送给另一个线程,然后马上把它从wire上拿掉。对于这种情况,你想要的只是二进制序列化,而且你大概也不会关心版本问题。对于更长期的持久化,你可能想要它对版本化支持更好一些。这样你就能够用1.0版本的程序写一个对象,然后用2.0版本的程序来读入并且获知这个对象。这种情况下,你会想要牺牲一些数据表示上的效率以解决版本问题。有时候你想要把对象存入数据库并且对它们进行查询。这时候,你真正想要的是对象——关系(O/R)映射。针对以上这些情形,在.NET里我们各提供了一种解决方案,并且它们还在不断改进之中。
Bruce Eckel: O/R mappings最大的问题是什么?
Anders Hejlsberg: 所有这些O/R mappings的命运通常取决于它们的缓存策略(caching policies)是否足够灵活,它们中的大多数都不够灵活。在.NET里,我们的确花了很大力气以便让缓存策略能够完全由你来控制,否则就不采用缓存策略。很多时候,你只是想要做一次查询并且记下查询结果。你会把结果当作对象来使用,但是你不想让基础设施帮你缓存这些结果,否则下次你在请求这个对象的时候,得到的还是同一个对象。很多系统只能以上述方式进行操作,导致的后果就是可怕的性能上的负担,而通常你并不需要这些。比如说,在一个中间层,大多数时候你并不关心缓存,因为你只是在处理一些进来的HTTP请求,而这些请求在被处理之后就立即消失了。那么为什么还要缓存呢?
Bruce Eckel: 也就是说缓存应该是在你需要的时候可以要求这么做,而不是默认情况下必须使用它。
Anders Hejlsberg: 是的。大多数O/R mappings或多或少都有这个问题,它们一上来就着手缓存和引用识别(referential identity)的问题。如果你请求要一个特定的Customer,然后得到一个Customer对象,下次你请求要那个Customer的时候得到的还是同一个对象。这可是个相当棘手的问题。这么做需要一个巨大的哈希表,用以存储你所用到过的所有对象。
Bill Venners: 为什么我要关心它们是否是同一个对象呢?
Anders Hejlsberg: 比如说你取到了一个CustID为100的Customer对象。在一个面向对象程序的内部,如果你在某个查询里请求这个Customer,并且随后在另外一个查询里也请求这个对象,第二次请求的时候,你期望要得到的是什么呢?
Bill Venners: 一个在语义上与我第一次得到的对象相等的Customer对象。
Anders Hejlsberg: 你期望得到的是同一个引用么?
Bill Venners: 我觉得这并不是我所关心的,只要它们在语义上是相等的就可以了。
Anders Hejlsberg: 真的吗? 因为它会导致你的程序以完全不同的方式工作。你是把Customer当作仅仅有一个实例的对象呢,还是把你所操作的那些对象当作从数据库来的拷贝呢? 大多数O/R mappings试图造成一种假相,那就是只有一个CustID为100的Customer对象,而且字面上看就是那个customer。如果你得到这个Customer并且给它的一个field设置一个值,那么现在你就改变了这个对象。与上述情况作为对照,你改变的是customer的这个(你自己的)拷贝,而不是另外一个拷贝。如果两个人都更新这个对象的两分拷贝,谁先更新(或者谁后更新),谁的更新就生效。
Bruce Eckel: 没错,如果你确实会碰到这些麻烦,那还不如透明些好。
Anders Hejlsberg: 很有意思。这让我想起了前面我们谈到的CORBA试图制造出一种假相,让分布式程序看起来并非是分布式的。我觉得也是同样的道理。你可能想要这种假相,假装数据并不在数据库里。你可以拥有这种假相,但是得付出一定的代价。
Bruce Eckel: 对于CORBA,他们试图制造一种假相,好像根本就没有网络存在。对于Jini,他们说,“不,网络的确存在。我们必须在某一层面上承认它的存在,否则一切就变得过于复杂。”对于设计者来说,关键是你在什么地方承认网络确实存在。你在哪个层面说,“这里就是我们经常必须要看到的边界。”我想类似的问题也存在于O/R mapping。挑战在于我们如何确定哪些是合适的抽象。
Eric Gunnerson: 有个大问题:你需要这些抽象吗? 在很多情况下其实并不需要。我们在当前.NET remoting的实现上(这个实现试图做到透明)也碰到了一些类似的问题。大多数人都说,“Yeah,我知道我正在做的是remoting。我知道对象位于远端。不要费尽心机试图把它弄得看起来像个本地对象。”
Anders Hejlsberg: 这当然会更好,因为这么一来使用者就会深入思考一下有可能会发生的事情。作为设计者,你应该尽力赋予用户这种能力。
Bruce Eckel: 也就是说你尽力把抽象放在合适的层次上,这样用户为了让程序跑起来,就不会因为错误的抽象而陷入巨大的麻烦。
Eric Gunnerson: 错误的抽象带来的问题是,你没办法摆脱它。在实践中,对于类设计者来说,即使是针对他们的设计可能会被应用的场景做出合理的猜测也是非常困难的,更不用说每种应用的相对频率了。你可能会以为你的用户想要透明性,因为这可以让他们做一些够酷的事情,于是你就实现了透明性。但是如果后来证明99%的用户从来都不关心透明性,猜猜看会发生什么?那些人可都是交了钱的。
Dan Fernandez: 另一个问题是,许多开发者都想像敲图章一样把同一种方法随处应用。人们会说,“好吧,既然有一个object-relational mapping的方法,那我们的程序里所有的东西毫无疑问都要用它。”在某些地方,它可能是有用的,但是另外一些变化幅度很大的东西——比如说股票交易系统——你可能不会真的想要一个持久层。但是因为你认为它们是解决问题的一个方法,最终你还是使用它。对于某些问题,object-relational mapping确实是好的解决方案,但是有时候人们想一劳永逸,认为它是所有问题的解决方案。这才是它真正给人们带来麻烦的地方。
Bruce Eckel: 但是你能理解这是为什么,对吧? 理由就是,我现在只需要学习一种持久化模型就可以到处使用了。
Dan Fernandez: 确实是这样.
Bruce Eckel: 解决这个问题的方法可能是使用某种类型的接口,然后根据它的使用情况选择不同的实现。通过这种方法,我只要学习一个接口,然后要么选择一个实现,要么让系统根据我所调用的方法帮我选择一个实现。
Eric Gunnerson: 当然可以,接口本身也是另外一种抽象。
第七篇:比较C#、Java以及 C++的范型[Top]
范型概述
Bruce Eckel: 能否就范型做一个简短的介绍?
Anders Hejlsberg: 范型的本质就是让类型能够拥有类型参数。它们也被称为参数化类型(parameterized types)或者参数的多态(parametric polymorphism)。经典的例子就是一个List集合类。List是一个方便易用的、可增长的数组。它有一个排序方法,你可以通过索引来引用它的元素,等等。现今,如果没有参数化类型,在使用数组或者Lists之间就会有些别扭的地方。如果使用数组,你得到了强类型保证,因为你可以定义一个关于Customer的数组,但是你没有可增长性和那些方便易用的方法。如果你用的是List,虽然你得到了所有这些方便,但是却丧失了强类型保证。你不能指定一个List是关于什么的List。它只是一个关于Object的List。这会给你带来一些问题。类型检测必须在运行时刻做,也就意味着没有在编译时刻对类型进行检测。即便是你塞给List一个Customer对象然后试图取出一个String,编译器也不会有丝毫的抱怨。直到运行时刻你才会发现他会出问题。另外,当把基元类型(primitive type)放入List的时候,还必须对它们进行装箱(box)。基于上述所有这些问题,Lists与Arrays之间的这种不和谐的地方总是存在的。到底选择哪个,会让你一直犹豫不决。
范型的最大好处就是它让你有了一个两全其美的办法(you can have your cake and eat it too),因为你可以定义一个List<T>[读作:List of T]。当使用一个List的时候,你可以实实在在地直到这个List是关于什么类型的List,并且让编译器为你做强类型检测。这只是它最直接的好处。接下来还有其它各种各样的好处。当然,你不会仅仅想让List拥有范型。哈希表(Hashtable)或者字典(Dictionary)——随便你怎么叫它——把键(keys)映射到值(values)。你可能会想要把Strings映射到Customrs,或者ints到Orders,而且是以强类型化的方式。
C#的范型
Bill Venners: 范型在C#中是如何工作的?
Anders Hejlsberg: 没有范型的C#,基本上你只能写class List {...}。有了范型,你可以写成class List<T> {...},这里T是类型参数。在List<T>范围内你可以把T当作类型来使用,当真正需要创建一个List对象的时候,写成List<int>或者List<Customer>。新类型是通过List<T>构建的,实际上就像是你的类型参数替换掉了原本的类型参数。所有的T都变成了ints或者Customers,你不需要做类型转换,因为到处都会做强类型检验。
在CLR(Common Language Runtime)环境下,当编译List<T>或者其它任何generic类型的时候,会像其它普通类型一样,先编译成中间语言IL(Intermediate Language)以及元数据。理所当然,IL以及元数据包含了额外的信息,从而可以直到有一个类型参数,但是从原则上来说,generic类型的编译与其它类型并没有什么不同。在运行时刻,当应用程序第一次引用到List<int>的时候,系统会查找看是否有人已经请求过List<int>。如果没有,它会把List<T>的IL和元数据以及类型参数int传递给JIT。而JITer在即时编译IL的过程中,也会替换掉类型参数。
Bruce Eckel: 也就是说它是在运行时刻实例化的。
Anders Hejlsberg: 的确如此,它是在运行时刻实例化的。它在需要的时候产生出针对特定类型的原生代码(native code)。从字面上看,当你说List<int>的时候,你会得到一个关于int的List。如果generic类型的代码使用了一个关于T的array,你得到的就是一个关于int的array。
Bruce Eckel: 垃圾回收机制会在某个时候来回收它么?
Anders Hejlsberg: 可以说会,也可以说不会,这是一个正交的问题。这个类在应用程序范围内被创建,然后在这个应用程序范围内就一直存在下去。如果你杀掉这个应用程序,那么这个类也就消失了,这点跟其它类一样。
Bruce Eckel: 如果我有一个应用程序用到了List<int>和List<Cat>,但是它从来没有走到使用List<Cat>的那个分支。。。。。。
Anders Hejlsberg:。。。。。。那么系统就不会实例化一个List<Cat>。现在让我说说一些例外的情况。如果你是使用NGEN在创建一个影像(image),也就是说你在直接产生一个native的影响,你可以提早产生这些实例。但是如果你是在通常的情况下运行程序,是否实例化是完全根据需要来确定的,而且推迟到越晚越好。
这之后,我们针对所有值类型(比如List<int>,List<long>,List<Double>, List<float>)的实例化做进一步的处理,创建可执行的原生代码的唯一拷贝。这样List<int>就有它自己的代码。List<long>也有它自己的代码。List<float>也是如此。对于所有引用类型(reference types),我们共享这些代码,因为它们所代表的东西是相同的。它们只是一些指针罢了。
Bruce Eckel: 你需要进行类型转换吧。
Anders Hejlsberg: 不,实际上并不需要。我们可以共享native image,但实际上它们有各自单独的虚函数表(VTables)。我只是想指出,当共享代码有意义的时候,我们会不遗余力的去做这件事情,但是当你非常需要运行效率的时候,我们对于共享代码会非常谨慎。通常对于值类型,你确实会关心List<int>元素的类型就是int。你不想把它们装箱(box)成Objects。对值类型进行装箱/拆箱,是可以用来进行代码共享的一种方法,但是这种方法代价过于昂贵。
Bill Venners: 对于引用类型,实际上也是完全不同的类。List<Elephant>和List<Orangutan>是不同的,但是它们确实共享所有的类方法的代码。
Anders Hejlsberg: 是的。作为实现上的细节来说,它们确实共享了相同的原生代码(native code)。
C#范型与Java范型的比较
Bruce Eckel: C#范型相比Java范型有什么特点?
Anders Hejlsberg: Java的范型实现是基于一个最初叫做Pizza的项目,这个项目是由Martin Odersky和其他一些人完成的。Pizza被重新命名为GJ,然后他成了一个JSR,并且最后被采纳进了Java语言。这个特定的范型proposal有一个关键的设计目标,就是它应该能够跑在不必经过改动的虚拟机上。不用改动虚拟机当然很棒,但是它也带来了一系列奇奇怪怪的限制。这些限制并不都是显而易见的,但是很快你就会说,“Hmm,这可有点怪。”
比如说,使用Java范型,实际上你就得不到任何刚才我所说得程序执行上的效率,因为当你在Java里编译一个范型类的时候,编译器拿掉了类型参数并到处代之以Object。List<T>编译好的影像文件(image)就像是一个到处使用Object(作为类型参数)的List。当然,如果你试图创建一个List<int>,那就的对所有用到的int对象进行装箱(boxing)。这就产生了很大的负担。此外,为了与老的虚拟机兼容,编译器实际上会插入各种各样的转换代码,而这些转换代码并不是由你来写的。如果是一个关于Object的List,而你试图把这些Objects当作Customers来对待,这些Objects必须在某些地方被转换成Customers,以便让verifier的验证能够通过。实际上它们的实现所做的就是自动为你插入那些类型转换。也就是说你得到了语法上的甜头,或者至少是一部分语法上的甜头,但是你并没有得到任何程序执行上的效率。这是我认为Java范型解决方案的第一个问题。
第二个问题是,我认为这可能是更大的一个问题,因为Java的范型实现依赖于去处掉类型参数,当到了运行时刻,你实际上并没有一个相对于运行时刻的可靠的范型表示。当你在Java里针对一个范型List使用反射(reflection)的时候,你并不知道这个List到底是关于什么的List。它只是一个List。因为你已经丢失了类型信息,对于任何动态代码生成(dynamic code-generation)的应用或者基于反射的应用,就没法工作了。这种趋势对我来说已经很明了了,(丢失类型信息的)情况越来越多。它根本没办法工作,因为你丢失了类型信息。而在我们的实现里,所有这些信息都是可获得的。你可以通过反射得到List<T>对象的System.Type表示。但这时候你还不能创建它的实例,因为你还不知道T是什么。但是你可以使用反射得到int的System.Type表示。然后你可以请求反射机制把这两个东西放在一起创建一个List<int>,这样你就得到了另外一个用以表示List<int>的System.Type。也就是说,从表示方法来说,任何你可以在编译时刻做到的事情,你也可以在运行时刻做到。
C#范型与C++模板的比较
Bruce Eckel: C#范型相比C++模板有哪些特点?
Anders Hejlsberg: 在我看来,理解C#范型与C++模板之间的差异最重要的一点就是:C#范型实际上就像是类,除了它们有类型参数。而C++模板实际上就像是宏(macros),除了它们看起来像是类。
C#范型与C++模板最大的不同之处在于类型检验发生的时间以及实例化的方式。首先,C#是在运行时刻实例化的,而C++ 是在编译时刻或者可能是在link的时候。但是不管怎样,C++模板实例化发生在程序运行之前。这是第一个不同之处。第二个不同之处在于,当你编译generic类型的时候,C#对它进行强类型检验。对于像List<T>这样未加限制的类型参数(unconstrained type parameter),类型T的值所能使用的方法仅限于Object类型所包含的方法,因为只有这些方法才是通常我们保证能够存在的方法。也就是说,在C#范型里,我们保证你所实施于类型参数的任何操作都会成功。
C++正好与此相反。在C++里,你可以对一个类型参数做任何你想做的事情。但是当你对它进行实例化的时候,它有可能通不过,而你会得到一些非常难懂的错误信息。比如,你有一个类型参数T以及两个T类型的变量,x和y,如果你写成x+y,那你最好事先定义了用于两个T型变量相加的+运算符,否则你会得到一些古怪的错误信息。所以从某种意义上说,C++模板实际上是非类型化的,或者说是弱类型化的。而C#范型则是强类型化的。
C#范型的constraints特性
Bruce Eckel: constraints在C#范型里是如何工作的?
Anders Hejlsberg: 在C#范型里,我们可以针对类型参数加一些限制条件(constraints)。还以List<T>为例,你可以写成,class List<T> where T: IComparable。意思是T必须实现IComparable接口。
Bruce Eckel: 有意思的是在C++里限制条件是隐含的。
Anders Hejlsberg: 是的。在C#里,你也可以让限制条件是隐含的。比如说我们有一个Dictionary<K,V>,它有一个add方法,以K为键(key)V为值(value)。Add方法的实现很可能需要把传入的键与Dictionary已有的键进行比较,而且它可能通过一个叫做IComparable的接口来做这个比较。一种方法是把key参数转换成IComparable,然后调用compareTo方法。当然,当你这么做的时候,你就已经针对K类型和key参数创建了一个隐式的限制条件。如果传入的key没有实现IComparable接口,你就会得到一个运行时错误。但是实际上你并没有在你的哪个方法里或者约定里明确表明key必须实现IComparable。而且你当然还得付出运行时刻类型检测的代价,因为实际上你所做的是运行时刻的动态类型检验。
使用constraint,你可以把代码里的动态检验提前,在编译时刻或者加载的时候对它进行验证。当你指定K必须实现IComparable,这就隐含了一系列的东西。对于任何K类型的值,你都可以直接访问接口方法,而不需要进行转换,因为从语义上来说,在整个程序里K类型要实现这个接口,这一点是得到保证的。无论什么时候你想要创建该类型的一个实例,编译器都会针对你给出的任何作为K参数的类型进行检验,看它是否实现了IComparable。如果没有实现,你会得到一个编译时错误。或者如果你是利用反射来做的话,会得到一个异常。
Bruce Eckel: 你说到了编译器以及运行时刻。
Anders Hejlsberg: 编译器会做检验,但是你也可能是在运行时刻通过反射来做的,这时候就由系统来做检验。如前所述,任何你在编译时刻可以做的事情,你都可以在运行时刻通过反射来做。
Bruce Eckel: 我是否可以写一个模板函数,或者换句话说,一个参数类型未知的函数?你们是在所做的是给容器加上更强的类型检验,但是我是否可以像在C++模板里那样得到弱类型化的东西呢?比如说,我是否可以写一个函数,它以A a和B b作为参数,然后我在代码里就可以写a+b?我是否可以不关心A和B是什么,只要它们有一个“+”运算符就可以了,因为我想要的是弱类型化。
Anders Hejlsberg: 你实际上问的是,通过constraints你到底能做到什么程度?与其它特性类似,如果把constraints发挥到极致,他可以变得异常复杂。仔细想想,其实constraints是一种模式匹配(pattern matching)的机制。你想要能指定,“该类型参数必须有一个接受两个参数的构造函数,并且实现了+运算符,要有某个静态方法,以及其它两个非静态方法,等等。”问题是,你想要这种模式匹配的机制复杂到哪种程度?
从什么也不做到功能全面的模式匹配,这是很大的一个范围。我们认为什么也不做太说不过去了,而全面的模式匹配又会变得非常复杂,所以我们选择了折衷的方式。我们允许你指定一个constraint,它可以是一个类、零个或者多个接口、以及叫做constructor constraint的东西。比如说,你可以指定“该类型必须实现IFoo和IBar接口,”或者“该类型必须继承自基类X。”一旦你这么做了,我们会在所有地方做类型检验以确认该constraint是否为真,包括编译时刻和运行时刻。任何由这个constraint所暗含的方法都可以通过类型参数的实例直接访问。
另外,在C#里,运算符都是静态成员函数。也就是说,一个运算符永远不可能成为一个接口的成员函数,因此一个接口限制条件(interface constraint)永远不可能让你指定一个“+”运算符。要指定一个“+”运算符,唯一的方法就是通过一个类限制条件(class constraint),这个类限制条件指定说必须继承自某个类,比如说Number类,因为Number有一个“+”运算符。但是你不可能把它抽象成:“必须有一个+运算符”,然后由我们来以多态的方式解析它的实际含义。
Bill Venners: 你是通过类型,而不是签名(signature)来实现限制条件的。
Anders Hejlsberg: 是的。
Bill Venners: 也就是说指定类型必须扩展某个类或者实现某些接口。
Anders Hejlsberg: 是的。本来我们可以走得更远。我们确实考虑过走得更远一些,但是那会非常复杂。并且我们不知道添加这些复杂性相对于你所获得的微不足道的好处,是否值得。如果你想做的事情没有被constraint系统直接支持,你可以借助于工厂模式(factory pattern)来完成。比如说,你有一个矩阵类Matrix<T>,在这个Matrix里你想定义一个标量积(dot product)方法。这当然意味着你最终需要理解如何把两个T相乘,但你不能把它表达成一个constraint,至少如果T是int、double或者float的时候这样做不行。但是你可以这么做:让Matrix接受一个Calculator<T>这样的参数,然后在Calculator<T>里声明一个叫做multiply的方法。你实现这个方法并把它传给Matrix。
Bruce Eckel: Calculator也是个参数化类型。
Anders Hejlsberg: 是的,它有点像factory模式。总之,是有办法来做这些事情的。可能不如你想要的那么棒,但是任何事情都是有代价的。
Bruce Eckel: 嗯,我感觉C++模板像是一种弱类型化(weak typing)的机制。当你开始在它上面添加constraints的时候,你是在从弱类型化转向强类型化(strong typing)。通常加入强类型化都会让事情更加复杂。这像是一个频谱。
Anders Hejlsberg: 你所意识到的类型化(typing)的问题,其实是一个拨盘(dial)。你把它拨的越高,程序员越觉得难受,但同时代码更安全了。但是在两个方向上你都有可能把它拨过头。
第八篇:CLR设计上的取舍[Top]
解释执行(Interpreting)和自适应优化( Adaptive Optimizations)
Bill Venners: Java的字节码(bytecode)与IL[Intermediate Language]的区别之一是,Java字节码把类型信息嵌入了指令当中,而IL并不这么做。例如,Java有好几个add指令:iadd用于两个int值相加,ladd用于两个long值型相加,fadd用于两个float值相加,以及dadd用于两个double值相加。IL的add指令用于两个数相加,add.ovf用于两个数相加并且捕获signed溢出,此外,add.ovf.un用于两个数相加并且捕获unsigned溢出。所有这些指令都是从栈顶部取下两个数,把它们相加,然后把结果如栈。但是对于Java来说,指令暗示了操作数的类型。fadd意味着两个浮点数位于栈的顶部。ladd意味着两个long型值位于栈的顶部。与此不同,CLR [Common Language Runtime]的add指令是多态的,尽管捕获signed溢出和unsigned溢出有所不同,但是add指令只是把位于栈顶的两个数相加,而不管它们是什么类型。基本上说,运行IL代码的引擎必须跟踪栈上这些值的类型信息,这样当碰到add指令的时候,它就可以知道应该采用哪种方法。
我读到过一些东西,说微软决定总是把IL代码编译执行,而从不解释执行。(Java)把类型信息嵌入指令当中是如何提高解释器的执行效率的?
Anders Hejlsberg:如果解释器可以不必跟踪栈顶上是什么东西,而完全根据指令来进行解释,那它就可以跑得更快一些。比如说当它看到一个iadd,它就知道这是两个integer相加。假设已经有人验证过栈上的数据是正确的,那么在这里省掉一些时间是安全的,而且对于一个解释器来说,这也是大家所关心的地方。对于我们来说,我们从来没有把CLR的目标设定在解释执行上。我们一直以来的打算就是JIT[Just-in-time compile],为了达到JIT的目的,我们需要跟踪类型信息。既然我们已经有了类型信息,再把它放到指令里就没有什么意义了。
Bill Venners: 许多现代的JVM [Java virtual machines]都采用了自适应优化,这一点是通过解释字节码入手来做的。它们在程序运行的时候进行profile,从而找到在80%到90%时间内所运行的10%到20%的代码,然后把这部分代码编译成native代码。尽管如此,它们并不需要实时(just-in-time)编译那些字节码。虽然它们被编译成native代码并且在后台进行了优化,但是一个方法的字节码仍然可以由解释器来执行。当native代码准备好了以后,就可以用它来代替字节码。CLR并不是针对解释执行的情形来进行设计的,那么在CLR里你们是不是完全排除了这种执行代码的方法?
Anders Hejlsberg: 不,我们并没有完全排除这种方法。我们仍然可以解释执行。只不过我们没有针对解释执行进行优化罢了。我们没打算写一个优化的高性能的解释器而让它仅仅用来解释执行。我们认为现在不会有人再这么做了。如果是针对10年前的一个机顶盒(top box),这么做可能还有点意思。但是现在不是了。JIT技术已经发展到了可以让你拥有多个JIT策略。你甚至可以想象使用一个快的JIT让它一个劲往下执行,然后当发现一直在执行一个特定的方法的时候,再使用另外一个JIT,让它多花些时间对代码进行更好的优化。采用JIT技术,你可以做的事情多了去了。
Bill Venners: 我在前面(In Part IV)曾经问过你,为什么在C#里缺省情况下方法是非虚的(non-virtual),你给出的一个理由是出于性能的考虑。你说:
We can observe that as people write code in Java, they forget to mark their methods final. Therefore, those methods are virtual. Because they're virtual, they don't perform as well. There's just performance overhead associated with being a virtual method.
对于进行自适应优化的JVM来说,另外一件事情就是它们会把虚方法调用做成内联函数,因为很多时候用到的仅仅是这个虚函数的一两个实现。
Anders Hejlsberg: 他们不可能做到把虚方法调用弄成内联函数。
Bill Venners: 我的理解是,这些JVM首先检查将要有虚方法被调用的对象的类型,看看它是否与预期的一两个类型相符,如果相符,它们就可以通过内联代码直接进行调用。
Anders Hejlsberg: 哦,是的。你可以针对上一次发生的情况进行优化,看看这次是否与上次是一样的,然后直接跳到上次调用的代码就可以了。但是这通常会带来一些额外的负担,尽管你可以把这些负担降到相当小的程度。
C#和CLR的不安全代码(Unsafe Code)
Bill Venners: 针对类似于指针运算等等不安全的行为,CLR有相应的IL指令,C#也有语法上的支持。与此不同,Java的字节码以及Java语言的语法并不支持不安全的行为。当你想要通JVM做一些不安全的事情,Java基本上要强迫你把它写成C代码然后使用Java Native Interface(JNI)。你们为什么决定要让IL和C#可以表示不安全代码?
Anders Hejlsberg: 具有讽刺意味的是,尽管有各种各样的争论和文章说C#如何(不该)有不安全代码以及“哦,上帝,这可糟透了”,有意思的是不安全代码比任何你通过JNI所写的代码都要安全的多。因为在C#里,不安全代码是和语言集成在一起的,每个人都知道发生的是什么。
首先让我们忘掉不安全代码存在安全漏洞这样的鬼话,因为不安全代码从来不在不受信任的(untrusted)环境里运行,正如JNI也从来不在不受信任的环境里运行一样。对不安全代码的正确看法应该是,它具有JNI所能够完成的功能,并且把它们集成到了编程语言当中。这使得在编写与外部世界打交道的代码的时候,变得更为简单,从而不容易出错,进而降低了不安全性。
Bruce Eckel: 把它叫作“不安全”你觉得遗憾么?
Anders Hejlsberg: 不。我认为应该直言不讳。它就是不安全的,对吗?
Bill Venners: 搞市场的那些人觉得遗憾么?
Anders Hejlsberg: 哦,是的。我们确实讨论过这个问题。他们说,“你难道不能把它叫作。。。。。。”
Bill Venners: 特殊代码。
Bruce Eckel: 总之听上去积极一点。
Anders Hejlsberg: 我们说不行。并且坚持道,“不,它是不安全的。我们就叫它不安全,”因为我们想让它单列出来。你应该尽可能避免书写不安全代码。有时候确实需要的话,我们希望在你的代码里这些地方是一目了然的。你总是可以通过在代码里查找unsafe关键字来找到所有这些地方。
Bill Venners: 你的观点是不安全代码更为安全,因为它比JNI方法更不容易出错。
Anders Hejlsberg: 是的,老实说,我想我们之所以这么做也是应为以往的经验。人们些JNI代码的时候碰到了很多问题。
值类型(Value Types)
Bill Venners: C#以及CLR支持值类型,它可以作为值存在于栈(stack)上,一可以作为对象存在于堆(heap)上。而Java有独立的基元类型(primitive types)和外覆类型(wrapper types)。在C#和CLR的设计种,值类型在多大程度上是出于运行效率的考虑,又在多大程度上是出于易用性的考虑?
Anders Hejlsberg: 处于运行效率方面的考虑是很明显的。有一种可能的解决方案是,“不存在值类型。所有类型都是在堆上分配的。这样我们就有了表示上的统一性,而且也完成了任务,对吗?”对是对,除了它运行起来像个废物。我们从Smalltalk知道这样做是不行的。Smalltalk采取的是类似的方法,所以需要有更好的方案。
长时间以来,我们看到两派不同的思想。要么你是高度的面向对象,这将付出性能上的代价,然后得到种种好处;要么你炮制一个分叉的类型系统,像Java和C++那样。在一个分叉的类型系统里,有基元类型,它被赋予特殊的能力,还有可以让用户扩展的类,而这些类不像基元类型那样有那些特殊的能力。没有针对所有类型的超类型(über-type)。这样看起来你就可以开始把任何数据都看成是对象。但是这真的很棒么?当你不能把int当作基元类型来对待的时候,你可以使用一个相应的外覆类型。是的,可以这么做,但是所有这些手动完成的外覆工作是非常烦人的并且它会妨碍到你做其它事情。
我们在C#和CLR里采用了不同的实现方法,我想我们因该有我们自己的两全其美的办法。只要是当作值来使用,值类型的效率就和Java或者C++的基元类型一样。只有当你试图把它们当作对象来使用的时候,它们才根据需要变成在堆上分配的对象,这一点是通过装箱(boxing)和拆箱(unboxing)来实现的。这种实现既美观又简单。
不变量(Immutables)
Bill Venners: 除了是C#和CLR的一个基础设施,值类型还是一个通用的面向对象编程概念。另外一个类似的概念是immutable类型。
当我在1996年参加第一次JavaOne大会的时候,针对C++又而Java没有的东西,每个人似乎都有抱怨,不同的人有不同的抱怨,但是看上去每个人至少都有一个抱怨。我的抱怨是const。我确实非常喜欢在C++里通过const可以做的事情,尽管在Java里没有const我也能对付。
你是否考虑过直接在C#和CLR里包含对immutable概念的支持?
Anders Hejlsberg: 这里包含两个问题。关于immutability,确实是需要慎重对待的,因为当你说某个东西是immutable的时候,你所说的是从外部视角来看的,这时候你并不能观察道任何mutation。这并不意味着它就不能有一个内部的缓存,用于提高它的效率。只是说从外部来看它是immutable的。对于编译器来说,这是很难弄清楚的。我们当然可以制定一条规则说,只能在构造函数里更改这个对象的成员变量。我们也可以保证它在实际应用中是可行的。但是实际上它会排除一些已经在使用的东西。所以我们没有把immutability作为一个强制性的规定,因为很难保证它的强制性。Immutable对象这个概念是非常有用的,但是这只对它的作者来说才是有意义的。
Bill Venners: Immutablity是类的语义的一部分。
Anders Hejlsberg: 是的。就const来说是这样的,这很有意思,因为我们也一直听到这个抱怨:“你们为什么没有const?”这个问题隐含的意思是,“你们为什么没有在运行时刻强制检验的const机制?”这实际上才是人们要问的,尽管他们不是直接这么说。
Const在C++里之所以能够工作是因为你可以把它转换成非const。如果不能把const转换为非const,那你就没法进行下去了。如果你定义了一个方法它接受一个const变量,你可以传给它一个非const变量。但是如果反过来就不行了。如果你定义了一个方法接受一个非const变量,你就不能传给它一个const变量。所以这时候你就没法往下进行了。逐渐你就需要所有非const的东西都要有一个const版本,最终这将导致所有的东西都像蒙上了一层阴影。在C++里,你可以行得通,因为正如C++ 里所有东西一样,它的所有特性都是可选得。如果你不喜欢constness,那你通过转换把它去掉就可以了。
反馈
对本文所描述的设计原则有自己的观点么?那么请到News&Ideas论坛讨论这篇文章,The C# Design Process.
资源
深入C#:微软主架构师Anders Hejlsberg访谈:
http://windows.oreilly.com/news/hejlsberg_0800.html
A Comparative Overview of C#:
http://genamics.com/developer/csharp_comparative.htm
Microsoft Visual C#:
http://msdn.microsoft.com/vcsharp/
Anders Hejlsberg不是Artima的采访对象中第一个提到品味的。Jim Waldo在他的访谈中针对构建一个由有品味的程序员组成的团队给出了几乎同样的评述:
http://www.artima.com/intv/waldo10.html
Ken Arnold’s的访谈有一整部分都是关于设计品味的——品味和美学(Taste and Aesthetics):
http://www.artima.com/intv/taste.html
浙公网安备 33010602011771号