Anders Hejlsberg论为什么不在c#引入类似java的checked exceptions

类别:Java 点击:0 评论:0 推荐:
Anders Hejlsberg论为什么不在c#引入类似java的checked exceptions

 

这是Anders Hejlsberg发表在http://www.artima.com系列短文中的一篇。这个系列论述了他设计c#的思路,集中讨论了c#中借鉴java地方,读来颇有受益,我便擅自把它翻译成中文。

 

本篇中参与谈论的还有两人,一个是Bruce Eckel,Thinking系列的作者(以下简称BE);一个是Bill Venners,“Inside JVM”作者,www.Artima.com的主编(简称BV)。

 

 

BE:C#没有checked exceptions。你是如何决定不把它引入C#中的?

AH:我觉得checked exceptions带来两个问题:可扩展性和版本控制(scalability and versionability)。我知道你也写过关于checked exceptions的文章,你好像也同意我们的观点。[i]

BE:我曾经觉得checked exceptions是个很了不起的创意。

AH:没错。坦率的说,乍看起来,他们确实很棒,而且这个创意本身并没有错。我完全同意checked exceptions是个很好的特性。只是某些特定的实现(比如说,java的实现方式)方式会带来问题。在java中,你只是把一类问题转换成另一类,我不觉得它(java的方式)使我的生活变得简单,你只是把它变成另一类问题。

BE:C#设计小组中的其他成员对这个问题有不同的看法吗?

AH:没有。我想,我们对这个问题达成很广泛的一致。C#现在对这个问题基本上不作表态。一旦我们发现了更好的解决方式,(相信我,我们仍在思考这个问题),我们会把它公布出来。我一直都持这样的观点:如果你对一个问题没有准确的说法,没有一个完美的解决法案,那你干脆什么也不说,而不是试图去建立一个解决它的大框架。

如果你让初级程序员写一个日历控件,他们常常会这样对自己说,“天啊,我决定写一个全世界最棒的日历控件,它将针对不同的历法展现出多态行为。它会有各种花哨的特性。”他们只有两个月的时间来完成这个任务,却把几乎所有的时间用在搭建程序的框架上,而只花两天的时间来写真正有关日历控件的代码。然后,他们不得不自我安慰说,我会在下一版中做得更好些。

这样的设计方法从一开始就错了。而我却不断看到这种现象,一而再,再而三的发生。因此我坚信简单就是美。除非你能解决一个通用问题,否则不要把一个解决特定问题的方法变成一个通用的框架(framework),因为你并不知道该如何设计这个框架。

BE:极限编程说,“用最简单的方法来解决问题”。

AH:是啊,爱因斯坦不也说过,“用最简单的方法来解决问题,但不要过度简单”("Everything should be as simple as possible, but no simpler"),我对checked exceptions的担心是,它增加了程序员的负担。[ii]

当你看到程序员使用了带有throws子句的函数时,他们的代码变得多么扭曲,你就会意识到checked exceptions 其实并没有帮助他们。这实际上是武断的API设计者在告诉使用者,你该怎么怎么办,而不是使用者决定他该怎么怎么办,这根本就是本末倒置!

BV:你提到的可扩展性和版本控制(scalability and versionability)问题,你能进一步澄清一下吗?

AH:好的,我们先来看版本的问题,它比较容易理解。假设,我写了一个foo函数,他丢出三个异常A,B,C。在下一个版本中,我决定对foo增加新的功能,而它也将因此丢出一个新的异常D。这是一个带来麻烦的改变,因为已有的代码如果不作修改,根本无法处理这个新的异常D。也就是说新版本中新异常的出现会给使用它的程序员带来麻烦。这就好像往公有的接口添加一个新的方法一样。而实际上,一个公有的接口,一旦发布会后,就不应该改变的。如果你想添加新的方法,你应该发布一个新的接口。异常情况也类似:你要么写一个全新的方法foo2 丢出A,B,C,D,4个异常,要么在你的实现中捕获新的异常D,再将它转换成A,B,C其中的一种。

BV:但是,对于没有checked exceptions的编程语言不也同样存在这个问题吗?如果新版本的foo函数丢出了新异常,使用者不一样也得考虑如何处理他们?这不一样也给他们老代码带来麻烦吗?[iii]

AH:不是这样的。因为在很多情况下,人们不关心(这些异常)。他们也不打算处理这些异常。在他们的消息循环底部有段异常处理代码,这段代码仅仅弹出一个对话框告诉你,出了异常,然后一切继续运转。(实际情况就是这样)。程序员为了保护他们的代码会在所有地方写上try finally,这样当异常出现是他们能正常退出,但他们不关心如何处理这些异常。

在java中的实现,throws语句并不强迫你处理异常。如果你不处理他们,java强迫你必须确认有哪些异常会丢出。你或者捕获这些异常,或者把他们放到你的throws子句中。为了避免这样的麻烦,人们会做一些愚蠢的事,比如说他们会在每个函数后加上“throws Exception”,这完全违背了checked exceptions的初衷。当你看到人们些一大堆这样冗长的代码时,你就会意思到checked exceptions根本没有起到任何作用。

BV:所以你认为更多的情况是,人们知道最终会有通用catch块来处理异常,所以他们自己并不处理它们。(So you think the more common case is that callers don't explicitly handle exceptions in deference to a general catch clause further up the call stack?)

AH:有意思的是人们为什么会认为异常中重要的是处理它们?这其实并不重要的。我个人认为,一个编写良好的程序,try finally (或C#中的using,类似于try finally)对try catch比例应该是十比一。

BV:什么是finally?

AH:在finally中,你可以保护你的代码不受异常的影响,但你并不处理异常。异常处理的代码你可以放在其他的地方。所有事件驱动的程序,像任何一种现代的图像用户程序(modern UI),你会在你消息循环中放一段异常处理代码,只有在程序走到这块时才处理他们。但你必须确认释放了所有资源,做了所有必要的清理工作(在finally块中),这样你才能保护自己始终处在一个一致的,正确的状态。[iv]你不会想要一个程序有100不同的地方来处理异常,并弹出对话框,万一你想改变弹出对话框的方式呢?(那你将修改100处),这太可怕了。所以说处理异常的代码应该集中起来(The exception handling should be centralized)。你只需要在异常被处理前保护好你自己就行了。

BV:那checked exceptions带来的可扩展性问题(scalability)又是什么?

AH:scalability 和 versionability 有些关联。在小的程序中,checked exceptions是非常吸引人的。在一个小的程序中,你可以看到你真的捕获FileNotFoundException 异常,这多让人振奋啊!

当你只调用了一个API,确实是这样的。但当你开始构建一个由4,5子系统组成的大系统时,麻烦就出现了。每个子系统会丢出四到十个异常,这些丢出异常就像一把把梯子,让你爬得越来越高,你是不是开始觉得头晕目眩了?最终你可能碰到一段代码丢出40异常的情况。而当你把这个大系统和另外一个系统结合在一起时,你甚至会丢出80异常,这简直太可怕。

总的来说,当checked exceptions变得无法控制,人们就会采取一些极端的措施来逃避它。所以你会看到他们干脆就写"throws Exception"。我甚至无法告诉你,有多少次我看到人们这样写 :

try {

…}

catch() {}

catch() {}

他们会自我安慰说,等我有时间了再把catch的代码补上,而实际上他们根本就不会。这种情况下,checked exceptions 实际上降低了代码的质量。

所以,当把所有这些情况都考虑了之后,我决定我需要作更多的思考来决定是否把某种形式的checked exceptions加入C#中。当然,能事先知道什么样的异常会丢出,而且有某种工具来检验无疑是很有价值的。我不认为我们能定下什么严格而有效的规则让编译器来做这个工作,当然我们可以做很多分析工作来检测可疑的代码,推断出有哪些未捕获的异常,并把这些潜在的危险告诉用户。

 

 

[i] http://www.mindview.net/Etc/Discussions/CheckedExceptions

 

[ii] 这个问题,笔者在James Gosling访华时也曾当面问他,可能是由于当时会场的气氛,Gosling只是匆匆的回答说,他并不在意是不是给程序员增加了负担,他在意的是程序的正确性。有意思的是,James Gosling 也在www.artima.com发表他对checked exceptions见解:http://www.artima.com/intv/solid.html

 

[iii] 在“The Design and Evolution of C++”, Bjarne Stroustrup也提到,同样也基于对这个问题的考虑,c++没有“Static Checking”(checked exceptions)而是采用“Run time Checking”。而且Stroustrup建议,对于丢出新异常D,可以让它从已有的异常中继承,这样既不影响已有代码,新的代码也可以处理它。(这是Stroustrup在1990就作出的结论!)

[iv] 这其实Stroustrup一再强调的RAII。因为没有析构函数,不得不用finally来模拟。(C++有多少特性从来就没有人很好理解!又有多少人把C++当成C with Class, Sigh!)

本文地址:http://com.8s8s.com/it/it16905.htm