一
1、C#设计过程
?
Bruce Eckel:我听说C#是一个工程师小组在一个屋子里设计出来的?
Anders Hejlsberg:是的。4年来,我们一直呆在这个屋子里。现在,每周一、三、五,我们仍然在这里会面。
Bruce Eckel:我很想了解一些关于C#设计过程的情况。我直接或间接参与过几种语言的设计工作,如Python。在Python设计过程中,Guido van Rossum被我们戏称为“仁慈的独裁者”。
Anders Hejlsberg:哦,Guido van Rossum就相当于我的位置。
Bruce Eckel:那么你是C#小组“仁慈的独裁者”么?
Anders Hejlsberg:我一般扮演最后拍板者的角色。比如,我们被一个问题困扰多时,到了非解决不可、只能作不二选择的时候,是由我来作最后决定的。当然大多数这样的情况下,正确的选择是显而易见的。
Bruce Eckel:C#的设计过程是不是和Turbo Pascal、Delphi十分相似?
Anders Hejlsberg:后面两者的设计过程不是那么规范的。因为Turbo Pascal主要由我一个人设计,而Delphi也是我和Chuck Jazdzewski、Gary Whizin等几个为数不多的人来完成,所以没有必要引入非常规范的设计过程。相反的,C#的设计过程则十分规范,每周一、三、五从1:00到3:00, 我们都会召开一个正式会议,会议议程也相当灵活,所有的问题都会拿到桌面上公开讨论、仔细推敲。我们还在互联网上建立了一个Wiki,这些问题及其解决方案,以及其他一些相关的东西都被发布在上面。
Bruce Eckel:那你们是如何发现这些的问题呢?
Anders Hejlsberg:呵呵,我们有一套行之有效的方法。我们可以通过很多途径来得到用户对语言设计的反馈意见——如软件设计咨询会、网络新闻组。这些反馈意见包括:疑问、软件Bugs、不一致、不规范问题等。这样我们就能有的放矢了。最后我们将这些问题整理成表,并一一重现它们。对于每个问题,我们都会认真对待,询问自己:“我们对这个问题有新的想法吗?真的没有吗?这个问题已经搁置好几个星期了,我们立即花30分钟集中精力研究一下,看这次是否能有所斩获。”
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,以及我自己。微软研究院的Don Syme和Andrew Kennedy承担了大部分一般性研究工作。
?
?
2、语言可用性研究和语言美学
?
Bill Venners:在C#的设计中,可用性研究、市场策略和语言美学的侧重是如何权衡的?
Anders Hejlsberg:一般而言,好的语言设计过程体现了对设计小组成员的美学品味取向的综合,也就是你刚才所说的语言美学观。美学品味带有极大的主观性,很难定论,只有产品出来后,你才能仔细体味它。我认为任何程度的可用性研究都不能取代语言美学的价值,因为可用性研究是极有针对性、非常具体的。可能有人问你:“你认为这部分功能如何?”这个问题很难回答。“你对这个语言有什么看法?”你从何谈起呢?你怎么可能花两个小时就解决掉所有可用性问题?绝无可能。
Bruce Eckel:人们必须深入理解这个问题。
Anders Hejlsberg:使用一种编程语言会经历一个感觉微妙变化的过程。只有使用几个月之后用户才能真正喜欢上它。他们会逐渐发现:“哦,它给人的感觉很舒服嘛。”你不能急于求成。
开始说过,我们在可用性研究上也做了大量工作,但主要是针对特定的功能。
Bill Venners:可以举个例子么?
Anders Hejlsberg:我们将可用性研究的重点放在了IDE功能实现上。我们会问自己:“用户是否知道在这里点击右键会有什么结果?”在纯语言功能部分,我们也考虑了一些可用性问题——例如在一些属性和事件上——不过没什么必要,真的。
我想在可用性研究上,语言特性不可能带来和IDE特性一样高的收益。你可以看到用户点击一个菜单项立即得到正确的反馈信息。而对语言来说,问题要复杂一些。例如:“它的概念容易理解么?”我们通过用户咨询会、留言板,比较好的解决了这些问题。用户需要有个说话的地方,“对于这个新特性,我有这样一些想法,你们是如何考虑的呢?”这样的问题提得越多、尖锐越好,因为你肯定是最希望在产品出来以前就能知道用户的想法,而不是产品推出以后。在一个语言特性被完全敲定前,我们通常都会考虑用户的建议和意见的。
?
二
1、对Checked Exceptions特性持保留态度
?
(译者注:在写一段程序时,如果没有用try-catch捕捉异常或者显式的抛出异常,而希望程序自动抛出,一些语言的编译器不会允许编译通过,如Java就是这样。这就是Checked Exceptions最基本的意思。该特性的目的是保证程序的安全性和健壮性。Zee&Snakey(MVP)对此有一段很形象的话,可以参见:
http://www.blogcn.com/user2/zee/main.asp。Bruce Eckel 也有相关的一篇文章(《Does Java need Checked Exceptions》),参见:
http://www.mindview.net/Etc/Discussions/CheckedExceptions)
Bruce Eckel:C#没有Checked Exceptions,你是怎么决定是否在C#中放置这种特性的么?
Anders Hejlsberg:我发现Checked Exceptions在两个方面有比较大的问题:扩展性和版本控制。我知道你也写了一些关于Checked Exceptions的东西,并且倾向于我们对这个问题的看法。
Bruce Eckel:我一直认为Checked Exceptions是非常重要的。
Anders Hejlsberg:是的,老实说,它看起来的确相当重要,这个观点并没有错。我也十分赞许Checked Exceptions特性的美妙。但它某些方面的实现会带来一些问题。例如,从Java中Checked Exceptions的实现途径来看,我认为它在解决一系列既有问题的同时,付出了带来一系列新问题的代价。这样一来,我就搞不清楚Checked Exceptions特性是否可以真的让我们的生活变得更美妙一些。对此你或许有不同看法。
Bruce Eckel:C#设计小组对Checked Exceptions特性是否有过大量的争论?
Anders Hejlsberg:不,在这个问题上,我们有着广泛的共识。C#目前在Checked Exceptions上是保持缄默的。一旦有公认的更好的解决方案,我们会重新考虑,并在适当的地方采用的。我有一个人生信条,那就是——如果你对该问题不具有发言权,也没办法推进其解决进程,那么最好保持沉默和中立,而不应该摆出一个非此即彼的架势。
假设你让一个新手去编一个日历控件,他们通常会这样想:“哦,我会写出世界上最好的日历控件!我要让它有各种各样的日历外观。它有显示部分,有这个,有那个……”他们将所有这些构想都放到控件中去,然后花两天时间写了一个很蹩脚的日历程序。他们想:“在程序的下一个版本中,我将实现更多更好的功能。”
但是,一旦他们开始考虑如何将脑海中那么多抽象的念头具体实现出来时,就会发现他们原来的设计是完全错误的。现在,他们正蹲在一个角落里痛苦万状呢,他们发现必须将原来的设计全盘抛弃。这种情况我不是看到一次两次了。我是一个最低纲领主义者。对于影响全局的问题,在没有实际解决方案前,千万不要将它带入到整个框架中去,否则你将不知道这个框架在将来会变成什么样子。
Bruce Eckel:极限编程(The Extreme Programmers)上说:“用最简单的办法来完成工作。”
Anders Hejlsberg:对呀,爱因斯坦也说过:“尽可能简单行事。”对于Checked Excpetions特性,我最关心的是它可能给程序员带来哪些问题。试想一下,当程序员调用一些新编写的有自己特定的异常抛出句法的API时,程序将变得多么纷乱和冗长。这时候你会明白Checked Exceptions不是在帮助程序员,反而是在添麻烦。正确的做法是,API的设计者告诉你如何去处理异常而不是让你自己想破脑袋。
?
2、Checked Exceptions的版本相关性
?
Bill Venners:你提到过Checked Exceptions的扩展性和版本相关性这两个问题。现在能具体解释一下它们的意思么?
Anders Hejlsberg:让我首先谈谈版本相关性,这个问题更容易理解。假设我创建了一个方法foo,并声明它可能抛出A、B、C三个异常。在新版的foo中,我要增加一些功能,由此可能需要抛出异常D。这将产生了一个极具破坏性的改变,因为原来调用此方法时几乎不可能处理过D异常。
??? 也就是说,在新版本中增加抛出的异常时,给用户的代码带来了破坏。在接口中使用方法时也有类似的问题。一个实现特定功能的接口一经发布,就是不可改变的,新功能只能在新版的接口中增加。换句话说,就是只能创建新的接口。在新版本中,你只有两种选择,要么建立一个新的方法foo2,foo2可以抛出更多的异常,要么在新的foo中捕获异常D,并转化为原来的异常A、B或者C。
Bill Venners:但即使在没有Checked Exceptions特性的语言中,(增加新的异常)不是同样会对程序造成破坏么?假如新版foo抛出了需要用户处理的新的异常,难道仅仅因为用户不希望这个异常发生,他写代码时就可以置之不理吗?
Anders Hejlsberg:不,因为在很多情况下,用户根本就不关心(异常)。他们不会处理任何异常。其实消息循环中存在一个最终的异常处理者,它会显示一个对话框提示你程序运行出错。程序员在任何地方都可以使用try finally来保护自己的代码,即使运行时发生了异常,程序依然可以正确运行。对于异常本身的处理,事实上,程序员是不关心的。
很多语言的throws语法(如Java),没必要地强迫你去处理异常,也就是逼迫你搞清楚每一个异常的来源。它们要求你要么捕获声明的异常,要么将它们放入throws语句。程序员为了达到这个要求,做了很多荒谬可笑的事情。例如他们在声明每个方法时,都必须加上修饰语:“throws Exception”。这完全是在搧这个特性的耳光,它不过是要求程序员多作些官样文章,对谁都没有好处。
Bill Venners:如此说来,你认为不要求程序员明确的处理每个异常的做法,在现实中要适用得多了?
Anders Hejlsberg:人们为什么认为(显式的)异常处理非常重要呢?这太可笑了。它根本就不重要。在我印象中,一个写得非常好的程序里,try finally和try catch语句数目大概是10:1。在C#中,也可以使用和类似try finally的using语句(来处理异常)。
Bill Venners:finally到底干了些什么?
Anders Hejlsberg:finally保证你不被异常干扰,但它不直接处理异常。异常处理应该放在别的什么地方。实际上,在任何一个事件驱动的(如现代图形界面)程序中,在主消息循环里,都有一个缺省的异常处理过程,程序员只需要处理那些没被缺省处理的异常。但你必须确保任何异常情况下,原来分配的资源都能被销毁。这样一来,你的程序就是可持续运行的。你肯定不希望写程序时,在100个地方都要处理异常并弹出对话框吧。如果那样的话,你作修改时就要倒大霉了。异常应该集中处理,并在异常来临处保护好你的代码。
?
3、Checked Exceptions的扩展性
?
Bill Venners:那么Checked Exceptions的扩展性又是如何呢?
Anders Hejlsberg:扩展性有时候和版本性是相关的。 在一个小程序里,Checked Exceptions显得蛮迷人的。你可以捕捉FileNotFoundException异常并显示出来,是不是很有趣?这在调用单个的API时也挺美妙的。但是在开发大系统时,灾难就降临了。你计划包含4、5个子系统,每个子系统抛出4到10个异常。但是(实际开发时),你每在系统集成的梯子上爬一级,必须被处理的新异常都将呈指数增长。最后,可能每个子系统需要抛出40个异常。将两个子系统集成时,你将必须写80个throw语句。最后,可能你都无法控制了。
很多时候,Checked Exceptions都会激怒程序员,于是程序员就想办法绕过这个特性。他要么在到处都是写“throws Exception”,要么——我都不知道自己看到多少回了——写“try, da da da da da(译者注:意思是飞快的写一段代码), catch curly curly(译者注:即‘{ }’)”,然后说:“哦,我会回头来处理这些空的异常处理语句的。”实际上,理所当然的没有任何人会回头干这些事情。这时候,Checked Exceptions已经造成系统质量的极大下降。
所以,你可能很重视这些问题,但是在我们决定是否将Checked Exceptions的一些机制放入C#时,却是颇费了一番思量的。当然,知道什么异常可能在程序中抛出还是有相当价值的,有一些工具也可以作这方面的检查。我不认为我们可以建立一套足够严格而严谨的规则(来完成异常检查),因为(异常)还可能是编译器的错误引起的呢。但是我认为可以在(程序)分析工具上下些功夫,检测是否有可疑代码,是否有未捕获的异常,并将这些隐藏的漏洞给你指出来。
?
?
三
1、Simplicity和Simplexity
?
Bill Venners:C#和Java传递对象事件的方式有所不同。Java使用类(通常是内部类(inner classes),它实现监听接口(listener interfaces)。C#使用了委托(delegates。译者注:VJ++6.0就引入了delegates),它有点儿类似函数指针。为什么要采用委托方式呢?
Anders Hejlsberg:请允许我首先谈谈对于一般意义上的Simplicity的看法。没有任何人怀疑简单的正确性,但是在如何实现简单的问题上则千差万别。有一种简单,我想称之为Simplexity。你做了一个很实际上复杂的东西,当你将它包装为一个简单的东西时,通常是将它的复杂性隐藏起来。所以实际上,你并不是在设计一个真正简单的系统。这样的一个包装过程,从某些角度上看,系统可能被你搞得更复杂了,因为用户有时候需要知道被隐藏地东西。这就是我说的Simplexity。
对我而言,简单必须是真正的简单,也就是说,当你将来某个时候需要钻研系统内部结构时,它应该显得更加简单,而不是比它表面那个样子更复杂。
?
2、委托和接口
?
Anders Hejlsberg:委托提供了与类和接口无关的实现方式,这是我认为最重要的地方。过去的很多编程语言已经认识到了这种方式的重要性。这种方式有很多名字,如:函数指针、成员函数指针,在Lisp语言中,被称为closures, 它是非常有用处的。
Bill Venners:那么这是如何实现的呢?
Anders Hejlsberg:使用接口的确可以完成委托具有的所有功能,但是你会被迫面对烦杂的“家务管理”。我们可以对比一下Java和.NET处理事件的过程。因为在Java中没有委托,所以最终必须使用接口。
接口是对应于事件的,一个接口可以定义1、2、3、4个甚至更多的方法。这点就已经产生了问题,因为这个“对应”没有明确的规范。到底应该定义多少个接口来处理事件呢?是每个事件对应一个接口还是所有的事件用一个接口?让你难以取舍。好了,先胡乱选择其一去处理组件的事件。接下来,你必须实现这些接口。理所当然的,如果处理两个组件的同样的事件,你就必须将接口实现两次——你当然不愿意这么干,所以在这种情况下,你还需要创建一个适配器。这样,烦杂的“家务管理”就跑到你面前来了。
内部类在处理家务事上能帮点小忙,但最终,有些问题你是逃避不了的——事件接收者必须知道它什么时候接收事件。这样你又必须明确的实现一个监听接口。与此相反,如果你使用委托,只要信息是兼容的,你就可以将所有事件放在一起处理。这家伙并不关心自己是怎么被调用的,它仅仅是一个(处理事件的)方法。
Bruce Eckel:看来,委托是非常精瘦的。
Anders Hejlsberg:对,的确如此。
Bruce Eckel:它也更加灵活。
Anders Hejlsberg:的确是这样。它仅仅依赖于信息的兼容性,如参数是否一致。如果是兼容的,你就可以将多个事件放在一起处理。从概念上说,这也完全满足了用户对一个回调的结果期望,对吧?只要给我一些参数,我就可以编写程序了。听起来很像一个方法吧,那么我给定该方法的一个引用,这个引用就是我所说的委托了。
Bruce Eckel:最后你也不会丢掉类型检查。类型检查是在运行时进行的么?
Anders Hejlsberg:不,大多数都是在编译时进行。你创建一个委托的实例后,它就和程序员在C++中使用的成员函数指针差不多。委托指向了某对象的一个方法。如果是虚拟的方法,你也能准确地判断委托的指向。所以从某种意义上说说,你可以在实例化委托时就解决虚拟问题。通过委托实现的调用可以看作一个间接的(方法)调用指令。
Bruce Eckel:除此之外,就不再需要其他的间接支持了。
Anders Hejlsberg:是的,构造一个委托的时候,你就可以一次性解决虚拟函数表(VTBL)和委托的指向问题;通过委托实现的调用都可以直接准确的得到它对应的方法。所以,委托比接口派遣更有效率,即使和标准的方法派遣相比,它的效率也要高一些。
Bruce Eckel:C#中也有Multicast类型的委托(译者注:Multicast即多点传送。是指一个委托可以对应多个方法;委托被调用时,就可以引起多个方法的调用。更详细的说明可以参考:http://msdn.microsoft.com/vjsharp/productinfo/
visualj/visualj6/technical/articles/general/delegates/), 它能够使多个函数被调用。 这是一个orthogonal特性吗?
Anders Hejlsberg:Multicast是一个彻头彻尾的orthogonal特性。老实说,在Multicast是否重要这个问题,我也是持保留态度的。我承认它有它的用处,但保守地讲的话,我认为所有的委托都是single cast的。有些人觉得Multicast十分重要,使用它有很多优点,但是在大多数情况下委托恰恰都是single cast的。事实上,我们构造的(C#)系统是使用single cast的,只要你(在这个系统里)不使用Multicast,就不会为它付出什么代价的。
Bill Venners:委托是怎么来体现你前面所说的 Simplicity和 Simplexity的呢?它们都体现在哪些地方?
Anders Hejlsberg:如果你仿效接口来实现委托,那么你最终无可避免地要面对“家务管理”和适配器问题。其实,我们可以观察任何一个捆绑了JavaBeans的开发工具,它们都会生成一些适配器并告诉你:“你不要修改下面的代码,我会分发一些非常瘦小的帮助类给你。”这对于我来说就太复杂了。它并不是一个真正简单的系统,它实际上非常复杂,不过是貌似简单而已。
?
3、组件概念在C#中的至高地位
?
Bill Venners:O'Reilly网站发布过对于你的一次采访,当时你这么评价C#对于属性和事件的支持:“现在,程序员人员每天都在开发大量的软件组件。他们不是在开发彼此孤立的应用程序和类库。每个人都在开发继承自环境提供的组件的新组件。这些新组件覆盖了父组件的一些方法、属性,处理一些事件,然后将这些新组件放回去(到组件库)。这是一个首先要树立的概念。”
我希望能更好理解你的谈话精神,因为我一直认为我是在开发类而不是组件。你是说很多人都在为别人开发在Delphi、VB和Java中使用的组件么?你说的“组件”到底是什么呢?
Anders Hejlsberg:“组件”一词包含的最重要的意思是组件可以很好的移植。这听起来十分美妙,但我们可能对此有不同的理解。在一个最简单的form中,一个组件可能就等同于一个附加了一些数据的类。组件是一个独立的软件部件,并不仅仅包含代码和数据。它是一个通过属性、方法和事件来实现自我暴露的类;是一个包含了元数据、命名模式等很多附加特征的类。这些特征可以给特定的开发环境提供动态信息,如:组件怎么使用,组件如何持久化自己的数据。开发环境使用组件的元数据就能够实现组件功能的智能解释并给出相应的说明文档。“组件”包含了如上所述的全部(内容)。
Bill Venners:我使用Java作开发时想到的是,我是在开发类库而不是组件库,可能是因为我觉得get/set太笨重了。在激发事件的时候,我也使用get/set,但我没有打算将这些类拿到集成开发环境中去使用,我一直想象这些类就是给那些那些纯编码的人们使用的。所以我很想知道现在到底有多少人在开发类似JavaBean那样的组件,面向组件开发是否是未来的趋势,因为在我的职业生涯中,和组件打的交道太少了。
Anders Hejlsberg:当今,主流的面向对象编程语言实际上都是混血儿。其中有大量的结构化编程,对象基本上也不过是一个包含了一系列方法和一个this指针的结构体。当你想到一个对象或者组件时,我想,从概念来说,你应该意识到它是有属性和事件的。如果编程语言能给予这些概念头等待遇的话,理解它就要容易一些。
有人可能说,C#对于属性和事件的支持不过就是个“甜果”(译者注:原文为syntactic sugar。Peter Landin发明的一个术语,意思是“增加到语言中、让人们感觉更舒服的特性”)而已。其实它的确就是个甜果,对不对?哼,对象本来就是个甜果嘛。我们不过就是在那些虚拟函数表(VTBL)上打转,你用C语言的宏也能实现,对吧?真的,你可以用C语言来面向对象编程,不过它的纷繁复杂能让你坠入地狱。同样的,你可以在C++或者Java里编写组件,但是因为这些语言没有给予组件概念足够重要的地位,所以要痛苦得多。还得说明一下的是,属性,并不是真的是一个属性,而是getBla和setBla(两个方法)。在属性观察器中,你看到的是Bla,但你应该知道它内部映射到了get/set。
很明显,组件导向是大势所趋。我们就是通过组件来使用我们的类的,但是在大多数语言中,组件并没有成为最重要的概念。我想强调的是,组件应该拥有头等地位。对于PME编程模式——属性、方法、事件——的谈论已经持续了很长的时间,我们也都在日复一日的使用这些东西来编程,那为什么不在编程语言中给予它应有的至高待遇呢?
?
?
附注:Anders Hejlsberg简历
??? Anders Hejlsberg是Microsoft公司卓越的软件工程师,领导设计了C#(发音为“C Sharp”)编程语言。Hejlsberg于上个世纪80年代初投身软件事业,为MS-DOS和CP/M平台开发了Pascal编译器。成立不久的一家小公司——Borland——很快聘用了Hejlsberg并收购了他的编译器,然后改名为Turbo Pascal。Hejlsberg接下来领导开发了Turbo Pascal的替代产品:Delphi。1996年,Hejlsberg在为Borland工作13个春秋后,加盟Microsoft公司(译者注:因为Borland公司内部矛盾和Microsoft的殷勤)。
1、C#设计过程
?
Bruce Eckel:我听说C#是一个工程师小组在一个屋子里设计出来的?
Anders Hejlsberg:是的。4年来,我们一直呆在这个屋子里。现在,每周一、三、五,我们仍然在这里会面。
Bruce Eckel:我很想了解一些关于C#设计过程的情况。我直接或间接参与过几种语言的设计工作,如Python。在Python设计过程中,Guido van Rossum被我们戏称为“仁慈的独裁者”。
Anders Hejlsberg:哦,Guido van Rossum就相当于我的位置。
Bruce Eckel:那么你是C#小组“仁慈的独裁者”么?
Anders Hejlsberg:我一般扮演最后拍板者的角色。比如,我们被一个问题困扰多时,到了非解决不可、只能作不二选择的时候,是由我来作最后决定的。当然大多数这样的情况下,正确的选择是显而易见的。
Bruce Eckel:C#的设计过程是不是和Turbo Pascal、Delphi十分相似?
Anders Hejlsberg:后面两者的设计过程不是那么规范的。因为Turbo Pascal主要由我一个人设计,而Delphi也是我和Chuck Jazdzewski、Gary Whizin等几个为数不多的人来完成,所以没有必要引入非常规范的设计过程。相反的,C#的设计过程则十分规范,每周一、三、五从1:00到3:00, 我们都会召开一个正式会议,会议议程也相当灵活,所有的问题都会拿到桌面上公开讨论、仔细推敲。我们还在互联网上建立了一个Wiki,这些问题及其解决方案,以及其他一些相关的东西都被发布在上面。
Bruce Eckel:那你们是如何发现这些的问题呢?
Anders Hejlsberg:呵呵,我们有一套行之有效的方法。我们可以通过很多途径来得到用户对语言设计的反馈意见——如软件设计咨询会、网络新闻组。这些反馈意见包括:疑问、软件Bugs、不一致、不规范问题等。这样我们就能有的放矢了。最后我们将这些问题整理成表,并一一重现它们。对于每个问题,我们都会认真对待,询问自己:“我们对这个问题有新的想法吗?真的没有吗?这个问题已经搁置好几个星期了,我们立即花30分钟集中精力研究一下,看这次是否能有所斩获。”
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,以及我自己。微软研究院的Don Syme和Andrew Kennedy承担了大部分一般性研究工作。
?
?
2、语言可用性研究和语言美学
?
Bill Venners:在C#的设计中,可用性研究、市场策略和语言美学的侧重是如何权衡的?
Anders Hejlsberg:一般而言,好的语言设计过程体现了对设计小组成员的美学品味取向的综合,也就是你刚才所说的语言美学观。美学品味带有极大的主观性,很难定论,只有产品出来后,你才能仔细体味它。我认为任何程度的可用性研究都不能取代语言美学的价值,因为可用性研究是极有针对性、非常具体的。可能有人问你:“你认为这部分功能如何?”这个问题很难回答。“你对这个语言有什么看法?”你从何谈起呢?你怎么可能花两个小时就解决掉所有可用性问题?绝无可能。
Bruce Eckel:人们必须深入理解这个问题。
Anders Hejlsberg:使用一种编程语言会经历一个感觉微妙变化的过程。只有使用几个月之后用户才能真正喜欢上它。他们会逐渐发现:“哦,它给人的感觉很舒服嘛。”你不能急于求成。
开始说过,我们在可用性研究上也做了大量工作,但主要是针对特定的功能。
Bill Venners:可以举个例子么?
Anders Hejlsberg:我们将可用性研究的重点放在了IDE功能实现上。我们会问自己:“用户是否知道在这里点击右键会有什么结果?”在纯语言功能部分,我们也考虑了一些可用性问题——例如在一些属性和事件上——不过没什么必要,真的。
我想在可用性研究上,语言特性不可能带来和IDE特性一样高的收益。你可以看到用户点击一个菜单项立即得到正确的反馈信息。而对语言来说,问题要复杂一些。例如:“它的概念容易理解么?”我们通过用户咨询会、留言板,比较好的解决了这些问题。用户需要有个说话的地方,“对于这个新特性,我有这样一些想法,你们是如何考虑的呢?”这样的问题提得越多、尖锐越好,因为你肯定是最希望在产品出来以前就能知道用户的想法,而不是产品推出以后。在一个语言特性被完全敲定前,我们通常都会考虑用户的建议和意见的。
?
二
1、对Checked Exceptions特性持保留态度
?
(译者注:在写一段程序时,如果没有用try-catch捕捉异常或者显式的抛出异常,而希望程序自动抛出,一些语言的编译器不会允许编译通过,如Java就是这样。这就是Checked Exceptions最基本的意思。该特性的目的是保证程序的安全性和健壮性。Zee&Snakey(MVP)对此有一段很形象的话,可以参见:
http://www.blogcn.com/user2/zee/main.asp。Bruce Eckel 也有相关的一篇文章(《Does Java need Checked Exceptions》),参见:
http://www.mindview.net/Etc/Discussions/CheckedExceptions)
Bruce Eckel:C#没有Checked Exceptions,你是怎么决定是否在C#中放置这种特性的么?
Anders Hejlsberg:我发现Checked Exceptions在两个方面有比较大的问题:扩展性和版本控制。我知道你也写了一些关于Checked Exceptions的东西,并且倾向于我们对这个问题的看法。
Bruce Eckel:我一直认为Checked Exceptions是非常重要的。
Anders Hejlsberg:是的,老实说,它看起来的确相当重要,这个观点并没有错。我也十分赞许Checked Exceptions特性的美妙。但它某些方面的实现会带来一些问题。例如,从Java中Checked Exceptions的实现途径来看,我认为它在解决一系列既有问题的同时,付出了带来一系列新问题的代价。这样一来,我就搞不清楚Checked Exceptions特性是否可以真的让我们的生活变得更美妙一些。对此你或许有不同看法。
Bruce Eckel:C#设计小组对Checked Exceptions特性是否有过大量的争论?
Anders Hejlsberg:不,在这个问题上,我们有着广泛的共识。C#目前在Checked Exceptions上是保持缄默的。一旦有公认的更好的解决方案,我们会重新考虑,并在适当的地方采用的。我有一个人生信条,那就是——如果你对该问题不具有发言权,也没办法推进其解决进程,那么最好保持沉默和中立,而不应该摆出一个非此即彼的架势。
假设你让一个新手去编一个日历控件,他们通常会这样想:“哦,我会写出世界上最好的日历控件!我要让它有各种各样的日历外观。它有显示部分,有这个,有那个……”他们将所有这些构想都放到控件中去,然后花两天时间写了一个很蹩脚的日历程序。他们想:“在程序的下一个版本中,我将实现更多更好的功能。”
但是,一旦他们开始考虑如何将脑海中那么多抽象的念头具体实现出来时,就会发现他们原来的设计是完全错误的。现在,他们正蹲在一个角落里痛苦万状呢,他们发现必须将原来的设计全盘抛弃。这种情况我不是看到一次两次了。我是一个最低纲领主义者。对于影响全局的问题,在没有实际解决方案前,千万不要将它带入到整个框架中去,否则你将不知道这个框架在将来会变成什么样子。
Bruce Eckel:极限编程(The Extreme Programmers)上说:“用最简单的办法来完成工作。”
Anders Hejlsberg:对呀,爱因斯坦也说过:“尽可能简单行事。”对于Checked Excpetions特性,我最关心的是它可能给程序员带来哪些问题。试想一下,当程序员调用一些新编写的有自己特定的异常抛出句法的API时,程序将变得多么纷乱和冗长。这时候你会明白Checked Exceptions不是在帮助程序员,反而是在添麻烦。正确的做法是,API的设计者告诉你如何去处理异常而不是让你自己想破脑袋。
?
2、Checked Exceptions的版本相关性
?
Bill Venners:你提到过Checked Exceptions的扩展性和版本相关性这两个问题。现在能具体解释一下它们的意思么?
Anders Hejlsberg:让我首先谈谈版本相关性,这个问题更容易理解。假设我创建了一个方法foo,并声明它可能抛出A、B、C三个异常。在新版的foo中,我要增加一些功能,由此可能需要抛出异常D。这将产生了一个极具破坏性的改变,因为原来调用此方法时几乎不可能处理过D异常。
??? 也就是说,在新版本中增加抛出的异常时,给用户的代码带来了破坏。在接口中使用方法时也有类似的问题。一个实现特定功能的接口一经发布,就是不可改变的,新功能只能在新版的接口中增加。换句话说,就是只能创建新的接口。在新版本中,你只有两种选择,要么建立一个新的方法foo2,foo2可以抛出更多的异常,要么在新的foo中捕获异常D,并转化为原来的异常A、B或者C。
Bill Venners:但即使在没有Checked Exceptions特性的语言中,(增加新的异常)不是同样会对程序造成破坏么?假如新版foo抛出了需要用户处理的新的异常,难道仅仅因为用户不希望这个异常发生,他写代码时就可以置之不理吗?
Anders Hejlsberg:不,因为在很多情况下,用户根本就不关心(异常)。他们不会处理任何异常。其实消息循环中存在一个最终的异常处理者,它会显示一个对话框提示你程序运行出错。程序员在任何地方都可以使用try finally来保护自己的代码,即使运行时发生了异常,程序依然可以正确运行。对于异常本身的处理,事实上,程序员是不关心的。
很多语言的throws语法(如Java),没必要地强迫你去处理异常,也就是逼迫你搞清楚每一个异常的来源。它们要求你要么捕获声明的异常,要么将它们放入throws语句。程序员为了达到这个要求,做了很多荒谬可笑的事情。例如他们在声明每个方法时,都必须加上修饰语:“throws Exception”。这完全是在搧这个特性的耳光,它不过是要求程序员多作些官样文章,对谁都没有好处。
Bill Venners:如此说来,你认为不要求程序员明确的处理每个异常的做法,在现实中要适用得多了?
Anders Hejlsberg:人们为什么认为(显式的)异常处理非常重要呢?这太可笑了。它根本就不重要。在我印象中,一个写得非常好的程序里,try finally和try catch语句数目大概是10:1。在C#中,也可以使用和类似try finally的using语句(来处理异常)。
Bill Venners:finally到底干了些什么?
Anders Hejlsberg:finally保证你不被异常干扰,但它不直接处理异常。异常处理应该放在别的什么地方。实际上,在任何一个事件驱动的(如现代图形界面)程序中,在主消息循环里,都有一个缺省的异常处理过程,程序员只需要处理那些没被缺省处理的异常。但你必须确保任何异常情况下,原来分配的资源都能被销毁。这样一来,你的程序就是可持续运行的。你肯定不希望写程序时,在100个地方都要处理异常并弹出对话框吧。如果那样的话,你作修改时就要倒大霉了。异常应该集中处理,并在异常来临处保护好你的代码。
?
3、Checked Exceptions的扩展性
?
Bill Venners:那么Checked Exceptions的扩展性又是如何呢?
Anders Hejlsberg:扩展性有时候和版本性是相关的。 在一个小程序里,Checked Exceptions显得蛮迷人的。你可以捕捉FileNotFoundException异常并显示出来,是不是很有趣?这在调用单个的API时也挺美妙的。但是在开发大系统时,灾难就降临了。你计划包含4、5个子系统,每个子系统抛出4到10个异常。但是(实际开发时),你每在系统集成的梯子上爬一级,必须被处理的新异常都将呈指数增长。最后,可能每个子系统需要抛出40个异常。将两个子系统集成时,你将必须写80个throw语句。最后,可能你都无法控制了。
很多时候,Checked Exceptions都会激怒程序员,于是程序员就想办法绕过这个特性。他要么在到处都是写“throws Exception”,要么——我都不知道自己看到多少回了——写“try, da da da da da(译者注:意思是飞快的写一段代码), catch curly curly(译者注:即‘{ }’)”,然后说:“哦,我会回头来处理这些空的异常处理语句的。”实际上,理所当然的没有任何人会回头干这些事情。这时候,Checked Exceptions已经造成系统质量的极大下降。
所以,你可能很重视这些问题,但是在我们决定是否将Checked Exceptions的一些机制放入C#时,却是颇费了一番思量的。当然,知道什么异常可能在程序中抛出还是有相当价值的,有一些工具也可以作这方面的检查。我不认为我们可以建立一套足够严格而严谨的规则(来完成异常检查),因为(异常)还可能是编译器的错误引起的呢。但是我认为可以在(程序)分析工具上下些功夫,检测是否有可疑代码,是否有未捕获的异常,并将这些隐藏的漏洞给你指出来。
?
?
三
1、Simplicity和Simplexity
?
Bill Venners:C#和Java传递对象事件的方式有所不同。Java使用类(通常是内部类(inner classes),它实现监听接口(listener interfaces)。C#使用了委托(delegates。译者注:VJ++6.0就引入了delegates),它有点儿类似函数指针。为什么要采用委托方式呢?
Anders Hejlsberg:请允许我首先谈谈对于一般意义上的Simplicity的看法。没有任何人怀疑简单的正确性,但是在如何实现简单的问题上则千差万别。有一种简单,我想称之为Simplexity。你做了一个很实际上复杂的东西,当你将它包装为一个简单的东西时,通常是将它的复杂性隐藏起来。所以实际上,你并不是在设计一个真正简单的系统。这样的一个包装过程,从某些角度上看,系统可能被你搞得更复杂了,因为用户有时候需要知道被隐藏地东西。这就是我说的Simplexity。
对我而言,简单必须是真正的简单,也就是说,当你将来某个时候需要钻研系统内部结构时,它应该显得更加简单,而不是比它表面那个样子更复杂。
?
2、委托和接口
?
Anders Hejlsberg:委托提供了与类和接口无关的实现方式,这是我认为最重要的地方。过去的很多编程语言已经认识到了这种方式的重要性。这种方式有很多名字,如:函数指针、成员函数指针,在Lisp语言中,被称为closures, 它是非常有用处的。
Bill Venners:那么这是如何实现的呢?
Anders Hejlsberg:使用接口的确可以完成委托具有的所有功能,但是你会被迫面对烦杂的“家务管理”。我们可以对比一下Java和.NET处理事件的过程。因为在Java中没有委托,所以最终必须使用接口。
接口是对应于事件的,一个接口可以定义1、2、3、4个甚至更多的方法。这点就已经产生了问题,因为这个“对应”没有明确的规范。到底应该定义多少个接口来处理事件呢?是每个事件对应一个接口还是所有的事件用一个接口?让你难以取舍。好了,先胡乱选择其一去处理组件的事件。接下来,你必须实现这些接口。理所当然的,如果处理两个组件的同样的事件,你就必须将接口实现两次——你当然不愿意这么干,所以在这种情况下,你还需要创建一个适配器。这样,烦杂的“家务管理”就跑到你面前来了。
内部类在处理家务事上能帮点小忙,但最终,有些问题你是逃避不了的——事件接收者必须知道它什么时候接收事件。这样你又必须明确的实现一个监听接口。与此相反,如果你使用委托,只要信息是兼容的,你就可以将所有事件放在一起处理。这家伙并不关心自己是怎么被调用的,它仅仅是一个(处理事件的)方法。
Bruce Eckel:看来,委托是非常精瘦的。
Anders Hejlsberg:对,的确如此。
Bruce Eckel:它也更加灵活。
Anders Hejlsberg:的确是这样。它仅仅依赖于信息的兼容性,如参数是否一致。如果是兼容的,你就可以将多个事件放在一起处理。从概念上说,这也完全满足了用户对一个回调的结果期望,对吧?只要给我一些参数,我就可以编写程序了。听起来很像一个方法吧,那么我给定该方法的一个引用,这个引用就是我所说的委托了。
Bruce Eckel:最后你也不会丢掉类型检查。类型检查是在运行时进行的么?
Anders Hejlsberg:不,大多数都是在编译时进行。你创建一个委托的实例后,它就和程序员在C++中使用的成员函数指针差不多。委托指向了某对象的一个方法。如果是虚拟的方法,你也能准确地判断委托的指向。所以从某种意义上说说,你可以在实例化委托时就解决虚拟问题。通过委托实现的调用可以看作一个间接的(方法)调用指令。
Bruce Eckel:除此之外,就不再需要其他的间接支持了。
Anders Hejlsberg:是的,构造一个委托的时候,你就可以一次性解决虚拟函数表(VTBL)和委托的指向问题;通过委托实现的调用都可以直接准确的得到它对应的方法。所以,委托比接口派遣更有效率,即使和标准的方法派遣相比,它的效率也要高一些。
Bruce Eckel:C#中也有Multicast类型的委托(译者注:Multicast即多点传送。是指一个委托可以对应多个方法;委托被调用时,就可以引起多个方法的调用。更详细的说明可以参考:http://msdn.microsoft.com/vjsharp/productinfo/
visualj/visualj6/technical/articles/general/delegates/), 它能够使多个函数被调用。 这是一个orthogonal特性吗?
Anders Hejlsberg:Multicast是一个彻头彻尾的orthogonal特性。老实说,在Multicast是否重要这个问题,我也是持保留态度的。我承认它有它的用处,但保守地讲的话,我认为所有的委托都是single cast的。有些人觉得Multicast十分重要,使用它有很多优点,但是在大多数情况下委托恰恰都是single cast的。事实上,我们构造的(C#)系统是使用single cast的,只要你(在这个系统里)不使用Multicast,就不会为它付出什么代价的。
Bill Venners:委托是怎么来体现你前面所说的 Simplicity和 Simplexity的呢?它们都体现在哪些地方?
Anders Hejlsberg:如果你仿效接口来实现委托,那么你最终无可避免地要面对“家务管理”和适配器问题。其实,我们可以观察任何一个捆绑了JavaBeans的开发工具,它们都会生成一些适配器并告诉你:“你不要修改下面的代码,我会分发一些非常瘦小的帮助类给你。”这对于我来说就太复杂了。它并不是一个真正简单的系统,它实际上非常复杂,不过是貌似简单而已。
?
3、组件概念在C#中的至高地位
?
Bill Venners:O'Reilly网站发布过对于你的一次采访,当时你这么评价C#对于属性和事件的支持:“现在,程序员人员每天都在开发大量的软件组件。他们不是在开发彼此孤立的应用程序和类库。每个人都在开发继承自环境提供的组件的新组件。这些新组件覆盖了父组件的一些方法、属性,处理一些事件,然后将这些新组件放回去(到组件库)。这是一个首先要树立的概念。”
我希望能更好理解你的谈话精神,因为我一直认为我是在开发类而不是组件。你是说很多人都在为别人开发在Delphi、VB和Java中使用的组件么?你说的“组件”到底是什么呢?
Anders Hejlsberg:“组件”一词包含的最重要的意思是组件可以很好的移植。这听起来十分美妙,但我们可能对此有不同的理解。在一个最简单的form中,一个组件可能就等同于一个附加了一些数据的类。组件是一个独立的软件部件,并不仅仅包含代码和数据。它是一个通过属性、方法和事件来实现自我暴露的类;是一个包含了元数据、命名模式等很多附加特征的类。这些特征可以给特定的开发环境提供动态信息,如:组件怎么使用,组件如何持久化自己的数据。开发环境使用组件的元数据就能够实现组件功能的智能解释并给出相应的说明文档。“组件”包含了如上所述的全部(内容)。
Bill Venners:我使用Java作开发时想到的是,我是在开发类库而不是组件库,可能是因为我觉得get/set太笨重了。在激发事件的时候,我也使用get/set,但我没有打算将这些类拿到集成开发环境中去使用,我一直想象这些类就是给那些那些纯编码的人们使用的。所以我很想知道现在到底有多少人在开发类似JavaBean那样的组件,面向组件开发是否是未来的趋势,因为在我的职业生涯中,和组件打的交道太少了。
Anders Hejlsberg:当今,主流的面向对象编程语言实际上都是混血儿。其中有大量的结构化编程,对象基本上也不过是一个包含了一系列方法和一个this指针的结构体。当你想到一个对象或者组件时,我想,从概念来说,你应该意识到它是有属性和事件的。如果编程语言能给予这些概念头等待遇的话,理解它就要容易一些。
有人可能说,C#对于属性和事件的支持不过就是个“甜果”(译者注:原文为syntactic sugar。Peter Landin发明的一个术语,意思是“增加到语言中、让人们感觉更舒服的特性”)而已。其实它的确就是个甜果,对不对?哼,对象本来就是个甜果嘛。我们不过就是在那些虚拟函数表(VTBL)上打转,你用C语言的宏也能实现,对吧?真的,你可以用C语言来面向对象编程,不过它的纷繁复杂能让你坠入地狱。同样的,你可以在C++或者Java里编写组件,但是因为这些语言没有给予组件概念足够重要的地位,所以要痛苦得多。还得说明一下的是,属性,并不是真的是一个属性,而是getBla和setBla(两个方法)。在属性观察器中,你看到的是Bla,但你应该知道它内部映射到了get/set。
很明显,组件导向是大势所趋。我们就是通过组件来使用我们的类的,但是在大多数语言中,组件并没有成为最重要的概念。我想强调的是,组件应该拥有头等地位。对于PME编程模式——属性、方法、事件——的谈论已经持续了很长的时间,我们也都在日复一日的使用这些东西来编程,那为什么不在编程语言中给予它应有的至高待遇呢?
?
?
附注:Anders Hejlsberg简历
??? Anders Hejlsberg是Microsoft公司卓越的软件工程师,领导设计了C#(发音为“C Sharp”)编程语言。Hejlsberg于上个世纪80年代初投身软件事业,为MS-DOS和CP/M平台开发了Pascal编译器。成立不久的一家小公司——Borland——很快聘用了Hejlsberg并收购了他的编译器,然后改名为Turbo Pascal。Hejlsberg接下来领导开发了Turbo Pascal的替代产品:Delphi。1996年,Hejlsberg在为Borland工作13个春秋后,加盟Microsoft公司(译者注:因为Borland公司内部矛盾和Microsoft的殷勤)。
关键字词: