[投票] Error-Handling

189 views
Skip to first unread message

pongba

unread,
Sep 30, 2007, 4:42:44 AM9/30/07
to pon...@googlegroups.com
1. 在你经历的项目中,哪些情况被定义为错误,需要汇报的。(注意,这里的错误不包括编程错误(bug);而是指IO错误和文件错误这类。)给个模糊的答案即可:)

2. 你们使用的错误处理机制是error-code呢还是exception?

3. 你们如何确保error-safe的?也就是说,当错误发生的时候,如何确保能够正确clean up的?

4. 你们如何确保所有错误都被妥善(如果能handle的话)handle或者(如果不能handle的话)传播给上级的?

5. 你们的项目中,有多少错误是可以就地handle,有多少错误是就地没有足够上下文,需要传播到上层模块解决的?

感谢大家哈:-)

--
刘未鹏(pongba)|C++的罗浮宫
http://blog.csdn.net/pongba
TopLanguage
http://groups.google.com/group/pongba

Atry

unread,
Sep 30, 2007, 9:00:55 AM9/30/07
to pon...@googlegroups.com
在07-9-30,pongba <pon...@gmail.com> 写道:
1. 在你经历的项目中,哪些情况被定义为错误,需要汇报的。(注意,这里的错误不包括编程错误(bug);而是指IO错误和文件错误这类。)给个模糊的答案即可:)

绝大多数错误都是底层 API 传递上来的系统错误。自己产生的错误很少,基本上都是由非法的输入导致的。这类非法输入,可能是来自网络另一端、最终用户输入或者脚本调用。

2. 你们使用的错误处理机制是error-code呢还是exception?

有一些错误需要异步处理,因此不能用异常,只能用错误码,类似 Boost.Asio 里面的那种错误。
对于同步错误,Java 用异常,Lua 有 error + pcall , C++ 其实是返回值和异常混用,因为 C++ 用异常实在很危险。

3. 你们如何确保error-safe的?也就是说,当错误发生的时候,如何确保能够正确clean up的?

也是混用, goto 、 try-catch 、 RAII 都用。

4. 你们如何确保所有错误都被妥善(如果能handle的话)handle或者(如果不能handle的话)传播给上级的?

在调试中遇到的绝大多数错误其实都是因为编码错误产生的,剩下的极少数频繁出现的错误往往都是被期盼的,能很好的处理。
但是!剩下的极少产生,或者只有理论产生的错误往往根本不能被妥善处理,当极端情况发生,触发这些错误时,编码时根本就没有考虑到,这时,程序崩掉是最好的可能。
根本问题在于大多数语言没有像 Java 那样的强制异常处理,甚至根本没有异常声明。 比如 C++ 的异常声明在 VC 下不可用,而且本身存在极大的缺陷。同时,很多程序员又懒得去检查错误码返回值。

5. 你们的项目中,有多少错误是可以就地handle,有多少错误是就地没有足够上下文,需要传播到上层模块解决的?

我说过,在实际项目中,最常见到的两类错误,要么是编码错误导致的,要么是期待的。前者调试时解决,而后者即所谓 unhappy path , 往往解决办法都是一目了然的。剩下的偶发错误的解决办法往往是不解决。即使一个模块编写者,比如我,一本正经的把错误传递给上层,上层往往也是抛弃这些偶发错误。

Atry

unread,
Sep 30, 2007, 9:11:30 AM9/30/07
to pon...@googlegroups.com
就我看来,错误处理的关键在于接口定义。接口必须约定所有的错误,对一个函数来说,增加一个可能产生的错误就应该等同于接口变更。而用户则必须保证所有的错误都处理。
对于编程错误,我的观点是不应该产生错误,而应该让程序崩掉。
我非常不满操作系统 API 的错误定义方式。这些系统 API 根本不敢承诺只产生这些错误,搞得人提心吊胆。
而这些操作系统 API 产生的错误大半又都是可以用正确的编码保证不发生。这类错误就应该直接崩掉,或者触发调试断点。如果一定要报告给外层,需要的也只是一个人类可阅读的字符串理由,而不需要程序来解析的错误码。

在07-9-30,Atry <pop....@gmail.com> 写道:

pongba

unread,
Sep 30, 2007, 9:44:56 AM9/30/07
to pon...@googlegroups.com
感谢Atry的信息!:-)


编程错误的确应该直接崩掉,因为编程错误本就是应当在发布之前解决的,所以"C++ Coding Standard"提倡用assert来对付这类错误。

> 后者即所谓 unhappy path , 往往解决办法都是一目了然的
这个能举个例子么?


> 剩下的偶发错误的解决办法往往是不解决。即使一个模块编写者,比如我,一本正经的把错误传递给上层,上层往往也是抛弃这些偶发错误。
"剩下的偶发错误"其实也是属于unhappy path吧。


> 也是混用, goto 、 try-catch 、 RAII 都用。
try-catch是用来对付异常的抛出端和接收端的。RAII是用来在异常经过路径上保证strong guanrantee的,这些都不算混用。goto用在里面,或者error-code用在里面,就的确是混用了。
就我所知goto和error-code都是不被提倡的。见"C++ Coding Standard"的相应章节。Atry这么用想必有什么原因?

> 有一些错误需要异步处理,因此不能用异常,只能用错误码,类似 Boost.Asio 里面的那种错误。
这个,Atry能否稍加解释?:)

XXX123

unread,
Sep 30, 2007, 9:57:22 AM9/30/07
to TopLanguage
"根本问题在于大多数语言没有像 Java 那样的强制异常处理,甚至根本没有异常声明。 比如 C++ 的异常声明在 VC 下不可用,而且本身存在
极大的缺陷。"

汗一个。。。貌似JAVA的强制异常声明是无数大牛痛批的对象吧,掩耳盗铃的典范。

Atry 写道:

Atry

unread,
Sep 30, 2007, 3:06:52 PM9/30/07
to pon...@googlegroups.com
在07-9-30,pongba <pon...@gmail.com> 写道:
感谢Atry的信息!:-)


编程错误的确应该直接崩掉,因为编程错误本就是应当在发布之前解决的,所以"C++ Coding Standard"提倡用assert来对付这类错误。

编程错误应该报告给人类。报告的方式是崩掉然后显示错误信息,或者通知调试器。就算在发布以后仍然存在 assert ,把错误报告给用户也是没有坏处的。

> 后者即所谓 unhappy path , 往往解决办法都是一目了然的
这个能举个例子么?

比如读文件一直读到 eof ,这个 eof 就是一个 unhappy path ,某些接口会用异常来表现这个 eof 。

> 剩下的偶发错误的解决办法往往是不解决。即使一个模块编写者,比如我,一本正经的把错误传递给上层,上层往往也是抛弃这些偶发错误。
"剩下的偶发错误"其实也是属于unhappy path吧。

> 也是混用, goto 、 try-catch 、 RAII 都用。
try-catch是用来对付异常的抛出端和接收端的。RAII是用来在异常经过路径上保证strong guanrantee的,这些都不算混用。goto用在里面,或者error-code用在里面,就的确是混用了。
就我所知goto和error-code都是不被提倡的。见"C++ Coding Standard"的相应章节。Atry这么用想必有什么原因?

因为 C++ 会混用很多不使用异常来报告错误的代码。就算标准 IO 流库里面也有选项是否在读写出错的时候抛出异常。往往程序员并不选择抛出异常,而使用if(!stream.good()) 这样的代码来处理。而那些 C 语言的接口就更没有异常可言了。

> 有一些错误需要异步处理,因此不能用异常,只能用错误码,类似 Boost.Asio 里面的那种错误。
这个,Atry能否稍加解释?:)

比如用一个 on_error 的回调函数来处理错误。把错误码传给这个 on_error 函数。因为发起 IO 请求的时候是立即返回,并不会产生错误。而是在处理这个请求的过程中可能有错误。

Atry

unread,
Sep 30, 2007, 3:07:23 PM9/30/07
to pon...@googlegroups.com
哪个大牛?我要去批判这个大牛。

在07-9-30,XXX123 <twj...@sina.com> 写道:

red...@gmail.com

unread,
Sep 30, 2007, 9:54:20 PM9/30/07
to pon...@googlegroups.com
pongba 写道:

> 1. 在你经历的项目中,哪些情况被定义为错误,需要汇报的。(注意,这里的
> 错误不包括编程错误(bug);而是指IO错误和文件错误这类。)给个模糊的答
> 案即可:)
说句套话, 这个应该是和应用密切相关的.

例如一个程序的通信部分, 分为底层 socket stream 部分和高层 RPC 部分的话,
非预期的tcp 连接中断, 对高层 RPC 的 call stub 来说, 可以视为是一个错误;
但是对于底层 socket stream 来说, 这只是一个普通的事件.


>
> 2. 你们使用的错误处理机制是error-code呢还是exception?
1. 接近 OS层的代码, 更多的是用 error-code, 接近业务概念的代码, 用
exception 较多. 例如, 对于底层io封装代码而言, 打开一个文件失败, 可以视作
一个常见的情况, 不值得惊奇; 但是对上层而言, 一个配置文件打不开, 就是一件
大事了.

2. 优化的考虑, 从一个数据结构之类删除一个 item, 参数不在数据结构中的处
理, 一般情况应该当作exception; 但是由于应用程序的特殊性, 总调用次数多,
出现的 "错误情形" 的比率也高, 而进行调用的点不多(检查 error -code 带来的
编程负担较小), 可能会作为 error-code. 这个要结合具体应用决定.

3. 库的通用性考虑: 如果作为 error-code 设计, 以后要改成 exception, 只需
要 wrap 一层 inline 函数即可, 反过来 wrap 的话, 性能代价已经付出, 所以写
一些较通用的库, 把不准这个库以后被用的情况下, 所谓"错误"情况的出现频率高
不高的时候, 会用 error-code.
>
> 3. 你们如何确保error-safe的?也就是说,当错误发生的时候,如何确保能够
> 正确clean up的?
主要是RAII, 为此宁愿写很多丑陋的函数 local 类, 强制这个方式之后, 程序员
容易掌握.

>
> 4. 你们如何确保所有错误都被妥善(如果能handle的话)handle或者(如果不
> 能handle的话)传播给上级的?
就是很一般的做法,
1. 每个scope 保证自己的资源不会泄漏, C++ 代码里面用 RAII (C 代码里面用
goto)
2. 对于有 error code 要检查的代码, 一定要检查 error code
3. 自己有一定的异常处理能力, 则 catch 相关的异常, 否则不要去 catch
4. 自己 catch 的异常, 根据是否处理完毕, 决定要不要 rethrow
>
> 5. 你们的项目中,有多少错误是可以就地handle,有多少错误是就地没有足够
> 上下文,需要传播到上层模块解决的?
没有留意这个数据.

red...@gmail.com

unread,
Sep 30, 2007, 10:13:39 PM9/30/07
to pon...@googlegroups.com
Atry 写道:
> 就我看来,错误处理的关键在于接口定义。接口必须约定所有的错误,对一个函
> 数来说,增加一个可能产生的错误就应该等同于接口变更。而用户则必须保证所
> 有的错误都处理。
> 对于编程错误,我的观点是不应该产生错误,而应该让程序崩掉。
> 我非常不满操作系统 API 的错误定义方式。这些系统 API 根本不敢承诺只产生
> 这些错误,搞得人提心吊胆。
> 而这些操作系统 API 产生的错误大半又都是可以用正确的编码保证不发生。这
> 类错误就应该直接崩掉,或者触发调试断点。如果一定要报告给外层,需要的也
> 只是一个人类可阅读的字符串理由,而不需要程序来解析的错误码。
看来你的应用场合和我目前的场合差很远, 你的希望我可是不能接受的 :)

我的应用中, 有一部分软件是执行网络包过滤任务的, 用 C/C++ 实现. 这里,
我宁愿程序来检查处理所有的错误, 拿网络帧来说, 从 ip 字段到 udp 字段到
PDU 内容, 每一个字段都需要检查数据是否可用, 有没有恶意伪造引发后续的检查
代码, 数据结构代码出错的可能性.

程序最好是可以连续运行很多年不会死机, 如果userspace 程序的heatbeat
没有被检测到, 进程会被软件 watchdog杀掉重新启动; 如果 kernel 部分程序的
heatbeat 没有被检测到(那么就是 OS or headware 出错), CPU板会被硬件
watchdog reset.

只要有 1ms 程序的网络服务不正常, 整个CPU板就会被网络板隔离, 然后监视
它什么时候恢复. 即使双电源供电都失效了, 网络板还能够保证将进出的网线直接
相连, 系统降级丢失过滤功能, 但是保留基本服务.

对我来说, OS 可能还是目前的做法好 :)


yq chen

unread,
Sep 30, 2007, 10:23:34 PM9/30/07
to pon...@googlegroups.com
1、在操作达不到预期条件和预期,都被定义为错误(作为error_code或者exception),需要汇报(向上层模块传递或者就地写日志)。
2、两者都有。对于确实应该有多个返回结果的调用(而且这多个结果都是正确的),通常采用error_code。其它的现在多采用exception来报告异常。比如系统初始化的时候,读不到配置文件或者访问数据库失败,就是一个异常,这个地方也不存在什么效率优化的问题了(因为后续工作都无法展开)。
3、大部分采用RAII、auto_ptr、shared_ptr这样的智能指针,然后还有一些是自己的wrapper。在模块或线程的边界,总是需要加上catch(...)。
4、具体的百分比不太清楚了。就地处理的通常相当少,一般都是汇集到一个事务层面,再来决定是失败还是重新提交。
 
在07-10-1,red...@gmail.com <red...@gmail.com> 写道:

yq chen

unread,
Sep 30, 2007, 10:26:47 PM9/30/07
to pon...@googlegroups.com
在处理很多COM相关的东西时,也是把HRESULT转化为Exception,一个判定为FAILED(hr)的调用结果,就被作为异常返回。

在07-10-1,red...@gmail.com <red...@gmail.com> 写道:

XXX123

unread,
Sep 30, 2007, 10:45:14 PM9/30/07
to TopLanguage
恐怕你的愿望难以实现:)见sutter的《Exceptional C++ Style》13条,貌似Meyers也说过同样的话。

> > >http://groups.google.com/group/pongba- 隐藏被引用文字 -
>
> - 显示引用的文字 -

yq chen

unread,
Sep 30, 2007, 11:49:12 PM9/30/07
to pon...@googlegroups.com
Anders Hejlsberg对异常规格的态度:
 
1 、对Checked Exceptions 特性持保留态度

 

(译者注:在写一段程序时,如果没有用try-catch 捕捉异常或者显式的抛出异常,而希望程序自动抛出,一些语言的编译器不会允许编译通过,如Java 就是这样。这就是Checked Exceptions 最基本的意思。该特性的目的是保证程序的安全性和健壮性。Zee&Snakey(MVP) 对此有一段很形象的话,可以参见:

http//www.blogcn.com/user2/zee/main.aspBruce 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特性的美妙。但它某些方面的实现会带来一些问题。例如,从 JavaChecked Exceptions 的实现途径来看,我认为它在解决一系列既有问题的同时,付出了带来一系列新问题的代价。这样一来,我就搞不清楚 Checked Exceptions特性是否可以真的让我们的生活变得更美妙一些。对此你或许有不同看法。

Bruce EckelC#设计小组对 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异常。

    也就是说,在新版本中增加抛出的异常时,给用户的代码带来了破坏。在接口中使用方法时也有类似的问题。一个实现特定功能的接口一经发布,就是不可改变的,新功能只能在新版的接口中增加。换句话说,就是只能创建新的接口。在新版本中,你只有两种选择,要么建立一个新的方法foo2foo2可以抛出更多的异常,要么在新的 foo中捕获异常 D,并转化为原来的异常A B 或者C

Bill Venners :但即使在没有Checked Exceptions特性的语言中,(增加新的异常)不是同样会对程序造成破坏么?假如新版foo抛出了需要用户处理的新的异常,难道仅仅因为用户不希望这个异常发生,他写代码时就可以置之不理吗?

Anders Hejlsberg :不,因为在很多情况下,用户根本就不关心(异常)。他们不会处理任何异常。其实消息循环中存在一个最终的异常处理者,它会显示一个对话框提示你程序运行出错。程序员在任何地方都可以使用try finally 来保护自己的代码,即使运行时发生了异常,程序依然可以正确运行。对于异常本身的处理,事实上,程序员是不关心的。

很多语言的 throws语法(如 Java),没必要地强迫你去处理异常,也就是逼迫你搞清楚每一个异常的来源。它们要求你要么捕获声明的异常,要么将它们放入 throws语句。程序员为了达到这个要求,做了很多荒谬可笑的事情。例如他们在声明每个方法时,都必须加上修饰语:" throws Exception"。这完全是在搧这个特性的耳光,它不过是要求程序员多作些官样文章,对谁都没有好处。

Bill Venners :如此说来,你认为不要求程序员明确的处理每个异常的做法,在现实中要适用得多了?

Anders Hejlsberg :人们为什么认为(显式的)异常处理非常重要呢?这太可笑了。它根本就不重要。在我印象中,一个写得非常好的程序里,try finallytry catch语句数目大概是 10 1。在C# 中,也可以使用和类似try finallyusing语句(来处理异常)。

Bill Vennersfinally到底干了些什么?

Anders Hejlsbergfinally保证你不被异常干扰,但它不直接处理异常。异常处理应该放在别的什么地方。实际上,在任何一个事件驱动的(如现代图形界面)程序中,在主消息循环里,都有一个缺省的异常处理过程,程序员只需要处理那些没被缺省处理的异常。但你必须确保任何异常情况下,原来分配的资源都能被销毁。这样一来,你的程序就是可持续运行的。你肯定不希望写程序时,在 100个地方都要处理异常并弹出对话框吧。如果那样的话,你作修改时就要倒大霉了。异常应该集中处理,并在异常来临处保护好你的代码。

 

3Checked Exceptions 的扩展性

 

Bill Venners:那么 Checked Exceptions的扩展性又是如何呢?

Anders Hejlsberg 扩展性有时候和版本性是相关的。 在一个小程序里,Checked Exceptions 显得蛮迷人的。你可以捕捉FileNotFoundException 异常并显示出来,是不是很有趣?这在调用单个的API 时也挺美妙的。但是在开发大系统时,灾难就降临了。你计划包含45个子系统,每个子系统抛出 4 10个异常。但是(实际开发时),你每在系统集成的梯子上爬一级,必须被处理的新异常都将呈指数增长。最后,可能每个子系统需要抛出 40个异常。将两个子系统集成时,你将必须写 80 throw语句。最后,可能你都无法控制了。

很多时候,Checked Exceptions 都会激怒程序员,于是程序员就想办法绕过这个特性。他要么在到处都是写"throws Exception ",要么----我都不知道自己看到多少回了----写"try, da da da da da( 译者注:意思是飞快的写一段代码), catch curly curly( 译者注:即'{ }' )",然后说:"哦,我会回头来处理这些空的异常处理语句的。"实际上,理所当然的没有任何人会回头干这些事情。这时候, Checked Exceptions 已经造成系统质量的极大下降。

所以,你可能很重视这些问题,但是在我们决定是否将Checked Exceptions 的一些机制放入C#时,却是颇费了一番思量的。当然,知道什么异常可能在程序中抛出还是有相当价值的,有一些工具也可以作这方面的检查。我不认为我们可以建立一套足够严格而严谨的规则(来完成异常检查),因为(异常)还可能是编译器的错误引起的呢。但是我认为可以在(程序)分析工具上下些功夫,检测是否有可疑代码,是否有未捕获的异常,并将这些隐藏的漏洞给你指出来。

 



在07-10-1,XXX123 <twj...@sina.com> 写道:

Atry

unread,
Oct 1, 2007, 12:41:47 AM10/1/07
to pon...@googlegroups.com
Java 程序员就地吃掉异常的恶习的确很常见。不过,问题的关键在于,一个函数是否抛出异常,抛出哪些异常,这个信息是否应该是接口的一部分?如果可能抛出的异常发生改变,接口是否修改了?这对于 C++ 这样要求类型安全甚至到了 BT 的程度的语言来说尤其重要。

在07-10-1,yq chen < mephist...@gmail.com> 写道:

pongba

unread,
Oct 1, 2007, 1:47:20 AM10/1/07
to pon...@googlegroups.com
关于checked exception,我有一个考虑。

checked exception从本质上是行不通的,因为error-handling从本质上是一个cross-layer的东西,真正应该耦合的只应该是error发生端和error的接收端。然而如果使用checked exception的话,那么从发生端到接收端路径上的每一层调用都像串在一根绳子上的蚂蚱一样,被耦合住了。就算在中间的每一层都避开这些异常,由于checked exception的性质,也还是需要在接口上声明它们(接口耦合)。这些耦合是不必要的。

所有位于中间层的,想对异常保持透明的,因为checked exception的存在,都变得耦合起来了。

另一方面,Gosling也在一个访谈里面说到,且Java的不少关于异常的实践标准都提倡,如果一个异常是被预期到的,且能够并且必须被recover的,那就应该用checked exception。因为这样能够强制调用端考虑recover的问题。只可惜,你根本无法保证承担recover责任的一定是"立即"调用端,就算再紧急的异常,也有可能在调用栈上上浮一两层之后才得到解决。我认为在软件中这些对异常保持透明的中间层比比皆是,比如一个简单的辅助函数。而这些情况下一旦用了checked exception,这些中间层就无法无视这些exception,就必须要么吞掉异常(糟糕做法),要么再自己接口上声明这些checked exceptions(耦合做法)。后一种做法的问题在于,如果异常发生端版本改变,新添或者删减异常的话,本应不管这些异常的中间层也必然受到牵累。这就是Anders说的版本问题。

Anders还有一个观点,checked exception在小项目中是成功的,因为这些项目中往往没有严格的分层,异常往往立即就被处理掉。但到了multi-layer的应用中,需要对异常保持透明的中间层就出现了。用checked exception就会带来耦合了。

关键是,无论什么异常(即便是应该被recover的异常),异常的发生端和接收端都是一个cross layer的事情。我觉得这是错误处理的本质特性之一。它不像函数返回status-code来左右代码的控制流,后者肯定是两层紧邻栈之间的事情。checked exception的问题就在于强迫异常一定要在调用端处理----不能处理的话就耦合。。。

pongba

unread,
Oct 1, 2007, 3:38:19 AM10/1/07
to pon...@googlegroups.com
关于checked exception为什么是邪恶的,我再用一个具体的例子来说明一下:

checked exception一旦出现,只留给立即调用方(immediate caller)三个选择:
1. 立即解决这个exception(当然,不是吞掉,瞎吞异常以后总是要还的)
2. wrap成自己这个抽象层次上的新异常。比如一个SQLException被wrap成一个IOException。
3. 直接在自己的接口上声明底层传上来的异常,让异常往上自动传播。

例子:最常见的比如FileNotFoundException,在JDK中是一个checked exception。我们来分析以上三种做法的可行性:

首先假设这样一种场合,在这个场合,最底层代码遇到文件不存在异常时是不知道如何解决的,这应该是可能的。
1. 既然刚才说了,底层不知道如何对付这个异常(有可能到了高层会弹出对话框让用户提供一个文件路径,就像Adobe安装过程中找不到ArcoUpdate.msi文件一样) 。因此这个异常需要往上传,这就把我们逼到2、3两种可能。
2. 调用方可能根本不能算是一个抽象层,所以也不想wrap这个异常。这就只剩下一种可能----3.
3. 显然也不好,因为既然这层不处理异常,那干嘛让它的接口和这个异常耦合起来呢。前面我说过,耦合的只应该是发生端和接收端。

这个逻辑有漏洞吗?

Atry

unread,
Oct 1, 2007, 4:26:29 AM10/1/07
to pon...@googlegroups.com
你举的例子中,2和3都是值得提倡的。如果分层,就需要包装成另一个上层关心的异常(而不是那个底层异常)。如果不是分层,而是某个模块内部处理代码,那么简单的在这个内部函数中声明会抛出同样的异常就行了。

在07-10-1,pongba < pon...@gmail.com> 写道:

Atry

unread,
Oct 1, 2007, 4:32:15 AM10/1/07
to pon...@googlegroups.com
因为你说的 3 这种情况,一定是属于某个模块内部的函数,某个模块内部的函数考虑耦合做什么,若应该申明异常就坦白的声明就是了。
而 2 这种情况其实是最常见的。底层关心的异常的细节,未必是上层关心的,包装是必须的。问题在于,往往程序员在接口中偷懒,直接暴露底层异常,这是那个偷懒的程序员的错,而不能谴责语法特性给他偷懒造成了障碍。

在07-10-1, Atry <pop....@gmail.com> 写道:

pongba

unread,
Oct 1, 2007, 4:36:13 AM10/1/07
to pon...@googlegroups.com
对于2,如果这个层次根本不拥有自己的抽象语意呢?又或者这个层次是一个generic layer,只想做一些general的工作,不想关心哪些异常会穿过它呢?看这个:http://radio.weblogs.com/0122027/stories/2003/04/01/JavasCheckedExceptionsWereAMistake.html
举个C++的例子,std::for_each,这个generic components根本无法声明自己的异常,因为他根本就不知道他用到的那些对象和操作会抛出什么异常,就算知道,也不会做wrap,因为它应该是exception-transparent的。checked exception和generic components是本质上相悖的我认为。
对于3,简单的在这个函数的接口上声明会抛出的异常就等于将一个函数的接口与一个它根本不关心的异常耦合起来了。一旦底层新抛出其它它同样不关心的异常,这个夹在中间的函数就broke了。

Atry

unread,
Oct 1, 2007, 4:37:13 AM10/1/07
to pon...@googlegroups.com
Java 异常规范真正烦人的地方在于某些接口声明了一个异常,而这个异常实际上可以通过使用接口时的逻辑来保证这个异常一定不会产生。这种异常,正确的做法是包装成一个 AssertError ,就不用声明了。

在07-10-1,Atry < pop....@gmail.com> 写道:

pongba

unread,
Oct 1, 2007, 4:39:48 AM10/1/07
to pon...@googlegroups.com
On 10/1/07, Atry <pop....@gmail.com> wrote:
因为你说的 3 这种情况,一定是属于某个模块内部的函数,某个模块内部的函数考虑耦合做什么,若应该申明异常就坦白的声明就是了。
而 2 这种情况其实是最常见的。底层关心的异常的细节,未必是上层关心的,包装是必须的。问题在于,往往程序员在接口中偷懒,直接暴露底层异常,这是那个偷懒的程序员的错,而不能谴责语法特性给他偷懒造成了障碍。
 
为什么不要考虑耦合呢?如果后来修改底层代码,新添或者去掉某些异常的话,甚至于简单地修改一个异常类型的话,那么就会导致夹在中间的函数上的异常声明全都broke。

Atry

unread,
Oct 1, 2007, 4:40:44 AM10/1/07
to pon...@googlegroups.com
嗯,你说的那种 exception-transparent 应该用模板解决,for_each 的实现者可以用function_traits 之类的办法知道应该声明什么异常。

在07-10-1,pongba <pon...@gmail.com > 写道:

Atry

unread,
Oct 1, 2007, 4:42:52 AM10/1/07
to pon...@googlegroups.com
如果底层代码的接口发生改变,那么依赖这个接口的代码 broken 掉是理所当然的。反而是让它编译通过才危险。

在07-10-1,pongba <pon...@gmail.com> 写道:

pongba

unread,
Oct 1, 2007, 4:46:33 AM10/1/07
to pon...@googlegroups.com


On 10/1/07, Atry <pop....@gmail.com> wrote:
嗯,你说的那种 exception-transparent 应该用模板解决,for_each 的实现者可以用function_traits 之类的办法知道应该声明什么异常。

就算traits真能萃取出应该声明的异常,那也是workaround。关键是,我一开始说的,我认为,error-handling本质上是一个cross-layer的东西,只应该让错误发生端和接收端耦合起来,中间的层次应该保持透明。否则一旦发生端有分吹草动,中间的层次就会受到无辜牵连。

我现在关心的倒不是如何让中间的层次自动声明异常列表,而是,我说的这种"底层异常——中间层透明——上层处理"的情况在现实中到底存在得有多普遍?!我的感觉是,中间层出现一两层函数调用的情况是很多见的。多于一层的话,checked exception就掣肘了,因为它逼迫每一个根本只想保持透明的中间层都wrap异常或者声明下面传上来的异常。

Atry

unread,
Oct 1, 2007, 4:47:36 AM10/1/07
to pon...@googlegroups.com
不过用 function_traits 的缺点就是代码编写很繁琐。可以增加一个特性,一个函数模板可以自动声明异常。
类似这样:
template<typename Function>
void foo(Function f) throw(auto) {
  f();
}

这个关键字应该只允许用在模板函数上。

在07-10-1, Atry <pop....@gmail.com> 写道:

pongba

unread,
Oct 1, 2007, 4:49:19 AM10/1/07
to pon...@googlegroups.com


On 10/1/07, Atry <pop....@gmail.com> wrote:
如果底层代码的接口发生改变,那么依赖这个接口的代码 broken 掉是理所当然的。反而是让它编译通过才危险。
不,严格来说,是让接收端broke掉是理所当然的。中间层代码从语意上在任何情况下都只想对异常保持透明,让它们broke,是完全不必要的。但不check的话又不能让接收端broke掉,这是个矛盾。Anders提倡解决这个矛盾的办法是开发一个能够检查异常的自动工具。我觉得这才是真正的方案。

pongba

unread,
Oct 1, 2007, 4:53:53 AM10/1/07
to pon...@googlegroups.com


On 10/1/07, Atry <pop....@gmail.com> wrote:
不过用 function_traits 的缺点就是代码编写很繁琐。可以增加一个特性,一个函数模板可以自动声明异常。
类似这样:
template<typename Function>
void foo(Function f) throw(auto) {
  f();
}

我也想过这个做法。但这个方案最致命的地方就在于它必须要求foo的定义可见,试想这种情况:

// 1.cpp
void foo(void (*func)() ) throw(auto);
int main()
{
foo(); // 编译器在这里根本推导不出foo可能抛出哪些异常
}
// 2.cpp
void foo(void (*func)() ) throw(auto)
{
  ... // implementation
}

这个方案只对模板还算可行。

Atry

unread,
Oct 1, 2007, 4:57:15 AM10/1/07
to pon...@googlegroups.com
异常问题应该分成几种不同的情况。
1、中间层是一个通用算法,不关心异常,希望简单的传递异常。解决办法是throw(auto)
2、中间层和上层属于同一个模块,这里的中间层仅仅是模块内部的实现代码。这种情况下,中间层可以用 throw(auto) 之类的语法糖,也可以直接写明要抛的异常,可以用任何猥琐的技巧,只要有利于缩短代码。
3、中间层是一个定义良好的模块。这个模块可能抛出什么异常有一个良好的约定。这种情况下,中间层必须包装底层异常为上层的关心的异常。

在07-10-1,pongba <pon...@gmail.com> 写道:


On 10/1/07, Atry <pop....@gmail.com > wrote:
嗯,你说的那种 exception-transparent 应该用模板解决,for_each 的实现者可以用function_traits 之类的办法知道应该声明什么异常。

就算traits真能萃取出应该声明的异常,那也是workaround。关键是,我一开始说的,我认为,error-handling本质上是一个cross-layer的东西,只应该让错误发生端和接收端耦合起来,中间的层次应该保持透明。否则一旦发生端有分吹草动,中间的层次就会受到无辜牵连。

我现在关心的倒不是如何让中间的层次自动声明异常列表,而是,我说的这种"底层异常----中间层透明----上层处理"的情况在现实中到底存在得有多普遍?!我的感觉是,中间层出现一两层函数调用的情况是很多见的。多于一层的话,checked exception就掣肘了,因为它逼迫每一个根本只想保持透明的中间层都wrap异常或者声明下面传上来的异常。

Atry

unread,
Oct 1, 2007, 4:59:06 AM10/1/07
to pon...@googlegroups.com
throw(auto) 当然只对模板函数有效,但是一个实例化以后函数抛出的类型是明确的。

在07-10-1,pongba <pon...@gmail.com> 写道:

Atry

unread,
Oct 1, 2007, 5:05:22 AM10/1/07
to pon...@googlegroups.com
或者更明确地说,函数声明不可以 throw(auto) ,只有函数定义可以 throw(auto)

在07-10-1,Atry <pop....@gmail.com> 写道:

pongba

unread,
Oct 1, 2007, 8:11:07 AM10/1/07
to pon...@googlegroups.com
包括Effective Java Programming Language Guide在内的许多Java指南上说:用checked exception来报告可恢复的异常;用unckecked exception来报告编程错误,或不可恢复的异常。

然而,我觉得好玩的是,比如说我正在写一个最底层的函数,这个函数要Open一个文件,现在我知道可能会遇到FileNotFoundException,假设在我的项目中这个特定的文件找不到的异常是可以解决的(比如创建一个缺省的版本(如配置文件))。那么只有两种情况:

1. 让最底层的函数直接恢复:一个配置文件,不存在的话就创建一个缺省的。这种情况下根本不用任何exception。
2. 需要往上汇报。这就是以前许多文章建议的用checked exception的情况,因为这个exception是可以恢复的,只是不在这一层恢复,而checked exception便可以迫使调用方采取一些手段。但问题是,这层的编写者咋就知道这个异常一定能被紧贴着的上一层调用方解决呢?既然都需要往上汇报了,鬼知道汇报几层才得到解决,后者是调用方的事情,调用方完全可能会在中间插入一两层薄薄的调用的。所以这种情况下checked exception又不恰当。

综上,checked exception是废柴。实际上,还有一个例子就是JDK中的checked exception带来了很大的麻烦,一个FileNotFoundException,作为一个JDK,根本不知道调用它的应用能不能handle这个异常,所谓when in doubt, leave it out。用runtimeException报告再好不过了,用checked exception,遇到不能解决这个异常的时候,直接调用方很容易干脆就睁一只眼闭一只眼,用一个空的catch{}把它supress了。好一点的会catch(...){ log; exit; }。据说后来的Java框架Struts、Hibernet, Sprint普遍用Unchecked exception了。

On 10/1/07, Atry <pop....@gmail.com> wrote:
或者更明确地说,函数声明不可以 throw(auto) ,只有函数定义可以 throw(auto)


yq chen

unread,
Oct 1, 2007, 9:55:19 AM10/1/07
to pon...@googlegroups.com
果然问题又从最初的调查演变成为关于checked exception的辩论了。

在07-10-1,pongba <pon...@gmail.com> 写道:

Oxygen

unread,
Oct 1, 2007, 10:59:16 AM10/1/07
to TopLanguage
这个约束太严格了,通常绝大多数异常的处理都是集中到一个地方.做一下事务的回滚,写一下log文件,或者显示一下错误消息.只有少数异常是做其他处理
的.因此对于大多数的函数,只是简单的把异常继续往上抛.这个约束会产生很多不必要的异常申明.

On 10月1日, 下午6时05分, Atry <pop.a...@gmail.com> wrote:
> 或者更明确地说,函数声明不可以 throw(auto) ,只有函数定义可以 throw(auto)
>

> 在07-10-1,Atry <pop.a...@gmail.com> 写道:


>
>
>
> > throw(auto) 当然只对模板函数有效,但是一个实例化以后函数抛出的类型是明确的。
>
> > 在07-10-1,pongba <pon...@gmail.com> 写道:
>

Atry

unread,
Oct 1, 2007, 11:20:35 AM10/1/07
to pon...@googlegroups.com
2. 需要往上汇报。这就是以前许多文章建议的用checked exception的情况,因为这个exception是可以恢复的,只是不在这一层恢复,而checked exception便可以迫使调用方采取一些手段。但问题是,这层的编写者咋就知道这个异常一定能被紧贴着的上一层调用方解决呢?既然都需要往上汇报了,鬼知道汇报几层才得到解决,后者是调用方的事情,调用方完全可能会在中间插入一两层薄薄的调用的。所以这种情况下checked exception又不恰当。

就算不是在紧贴着的上一层解决,上一层增加一个异常声明浪费了你很多行代码吗?


Atry

unread,
Oct 1, 2007, 11:21:44 AM10/1/07
to pon...@googlegroups.com
这里有两个问题。
1. 在经过的每一层增加这个异常声明有没有用?
2. 增加这个异常声明会不会很麻烦?

在07-10-1,Atry <pop....@gmail.com> 写道:

pongba

unread,
Oct 1, 2007, 11:26:42 AM10/1/07
to pon...@googlegroups.com
On 10/1/07, Atry <pop....@gmail.com> wrote:
2. 需要往上汇报。这就是以前许多文章建议的用checked exception的情况,因为这个exception是可以恢复的,只是不在这一层恢复,而checked exception便可以迫使调用方采取一些手段。但问题是,这层的编写者咋就知道这个异常一定能被紧贴着的上一层调用方解决呢?既然都需要往上汇报了,鬼知道汇报几层才得到解决,后者是调用方的事情,调用方完全可能会在中间插入一两层薄薄的调用的。所以这种情况下checked exception又不恰当。

就算不是在紧贴着的上一层解决,上一层增加一个异常声明浪费了你很多行代码吗?

1. 需要声明的异常个数取决于被调用的下层子系统所传上来的异常个数,Anders称这个是个scalability问题。但Gosling说实际编码中需要声明的异常极少,一般就0、1、2个。

2. 我认为更主要的问题是耦合。异常抛出端不论是增加还是减少还是修改一下异常名字,中间层都需要被不必要的牵连(当然,重构工具也许可以搞定这一个问题)。

pongba

unread,
Oct 1, 2007, 11:29:47 AM10/1/07
to pon...@googlegroups.com
On 10/1/07, Atry <pop....@gmail.com> wrote:
这里有两个问题。
1. 在经过的每一层增加这个异常声明有没有用?
2. 增加这个异常声明会不会很麻烦?

1. 我认为没有用,因为似乎大家公认为这样的中间层(即exception-neutral,或称exception-transparent,whatever)并不鲜见;它们并不想关心哪些异常穿过它们,他们的目的是"异常中立——对所有(不管是现在还是将来)可能发生的异常保持中立,让他们透明地穿过"。

2. 也许Gosling是对的,许多时候,这并不麻烦,只需要声明很少几个异常即可。但(1),基于第一点的分析,这仍然是不必要的耦合,注意,我觉得不必要的耦合总是不好的。(2) 如果调用栈比较深的话,受牵连的层数就会更多。

pongba

unread,
Oct 1, 2007, 11:31:43 AM10/1/07
to pon...@googlegroups.com


On 10/1/07, Oxygen <Oxygen.J...@gmail.com> wrote:
这个约束太严格了,通常绝大多数异常的处理都是集中到一个地方.做一下事务的回滚,写一下log文件,或者显示一下错误消息.只有少数异常是做其他处理
的.因此对于大多数的函数,只是简单的把异常继续往上抛.这个约束会产生很多不必要的异常申明.
 

你说的这个是属于不可recover异常,目前Java的最佳实践已经提倡用RuntimeException来抛出这些异常,所以就不存在问题了:-)

Atry

unread,
Oct 1, 2007, 9:11:18 PM10/1/07
to pon...@googlegroups.com
不可恢复异常和逻辑可避免的异常分别包装为RuntimeException和AssertError,而剩下的明确的 unhappy path 这类异常。我就是觉得需要明确声明,而且我觉得明确声明我编码不麻烦。你非要说异常中立的中间层很常见我就不同意,我就觉得中间层绝大多数都需要知道异常,就好像中间层也需要知道返回值一样,中间层非要不知道返回值就得用泛型。

在07-10-1, pongba <pon...@gmail.com> 写道:

pi1ot

unread,
Oct 6, 2007, 3:08:59 AM10/6/07
to TopLanguage
我一般是预料可能遇到的错误使用reutrn error code,比如fopen()失败,connect()超时之类。完全预料之外的使用
except,比如明明刚才已经fopen() succ,接下来fwrite()出错,就抛出except。
然后except如果发生在一个内部调用的接口,就只做自己范围内的清理工作之后继续throw,把最终决定权交给调用方或者顶层的
application来决定,最后如果是个工具性程序就terminated,如果是个service或者daemon就只log之然后忽略。


On 9月30日, 下午4时42分, pongba <pon...@gmail.com> wrote:
> 1.
> 在你经历的项目中,哪些情况被定义为错误,需要汇报的。(注意,这里的错误不包括编程错误(bug);而是指IO错误和文件错误这类。)给个模糊的答案即可:)
>

> 2. 你们使用的错误处理机制是error-code呢还是exception?


>
> 3. 你们如何确保error-safe的?也就是说,当错误发生的时候,如何确保能够正确clean up的?
>

> 4. 你们如何确保所有错误都被妥善(如果能handle的话)handle或者(如果不能handle的话)传播给上级的?
>

> 5. 你们的项目中,有多少错误是可以就地handle,有多少错误是就地没有足够上下文,需要传播到上层模块解决的?
>

> 感谢大家哈:-)
>
> --
> 刘未鹏(pongba)|C++的罗浮宫http://blog.csdn.net/pongba
> TopLanguagehttp://groups.google.com/group/pongba

pongba

unread,
Oct 6, 2007, 6:32:39 AM10/6/07
to pon...@googlegroups.com
Atry,

1. 为什么"3 这种情况,一定是属于某个模块内部的函数"?

2. 为什么模块内部的函数不用考虑耦合呢?(耦合只有两种,一种是不必要的,一种是必要的。如果是不必要的耦合,即便是在模块内部,我也认为是没有为好。而我认为3这种情况下的耦合就是不必要的,既然一个函数不处理异常,而这是任其向上穿过它的这层栈,那么其实这个函数对这个异常就是语意上透明的,强迫其在接口上声明这个异常不是不必要的耦合又是什么呢?)


On 10/1/07, Atry <pop....@gmail.com> wrote:
因为你说的 3 这种情况,一定是属于某个模块内部的函数,某个模块内部的函数考虑耦合做什么,若应该申明异常就坦白的声明就是了。
而 2 这种情况其实是最常见的。底层关心的异常的细节,未必是上层关心的,包装是必须的。问题在于,往往程序员在接口中偷懒,直接暴露底层异常,这是那个偷懒的程序员的错,而不能谴责语法特性给他偷懒造成了障碍。

在07-10-1, Atry <pop....@gmail.com> 写道:
你举的例子中,2和3都是值得提倡的。如果分层,就需要包装成另一个上层关心的异常(而不是那个底层异常)。如果不是分层,而是某个模块内部处理代码,那么简单的在这个内部函数中声明会抛出同样的异常就行了。

在07-10-1,pongba < pon...@gmail.com> 写道:
关于checked exception为什么是邪恶的,我再用一个具体的例子来说明一下:

checked exception一旦出现,只留给立即调用方(immediate caller)三个选择:
1. 立即解决这个exception(当然,不是吞掉,瞎吞异常以后总是要还的)
2. wrap成自己这个抽象层次上的新异常。比如一个SQLException被wrap成一个IOException。
3. 直接在自己的接口上声明底层传上来的异常,让异常往上自动传播。

例子:最常见的比如FileNotFoundException,在JDK中是一个checked exception。我们来分析以上三种做法的可行性:

首先假设这样一种场合,在这个场合,最底层代码遇到文件不存在异常时是不知道如何解决的,这应该是可能的。
1. 既然刚才说了,底层不知道如何对付这个异常(有可能到了高层会弹出对话框让用户提供一个文件路径,就像Adobe安装过程中找不到ArcoUpdate.msi文件一样) 。因此这个异常需要往上传,这就把我们逼到2、3两种可能。
2. 调用方可能根本不能算是一个抽象层,所以也不想wrap这个异常。这就只剩下一种可能----3.
3. 显然也不好,因为既然这层不处理异常,那干嘛让它的接口和这个异常耦合起来呢。前面我说过,耦合的只应该是发生端和接收端。

这个逻辑有漏洞吗?


Atry

unread,
Oct 6, 2007, 8:36:20 AM10/6/07
to pon...@googlegroups.com
在07-10-6,pongba <pon...@gmail.com> 写道:
Atry,

1. 为什么"3 这种情况,一定是属于某个模块内部的函数"?

如果是模块之间,那么模块间的接口应该是约定好的。这个接口可能抛出哪些异常也是约定好的,这样的话,实现代码应该包装异常而不是任凭异常通过。

2. 为什么模块内部的函数不用考虑耦合呢?(耦合只有两种,一种是不必要的,一种是必要的。如果是不必要的耦合,即便是在模块内部,我也认为是没有为好。而我认为3这种情况下的耦合就是不必要的,既然一个函数不处理异常,而这是任其向上穿过它的这层栈,那么其实这个函数对这个异常就是语意上透明的,强迫其在接口上声明这个异常不是不必要的耦合又是什么呢?)

在我的认知中,"低耦合" 的同义词就是"良好定义的接口"。一个模块内部实现代码与耦合无关。

写一个内部函数的时候考虑耦合是愚蠢的。内部函数质量高低的唯一标准是代码长度——代码越短越好。这一点正是前段时间" Linus 炮轰事件"给我的启示。"用 C 编码,用 C++ 实现"也是这个意思。模板这样的猥琐特性有利于缩短代码长度,但反而增加耦合。所以模板用在模块内部( cpp 文件中)就无所谓,用来约定接口就不妥。

Atry

unread,
Oct 6, 2007, 8:53:27 AM10/6/07
to pon...@googlegroups.com
当然,这些情况是有例外的,比如说确实存在异常透明的通用算法。这类情况应该用泛型解决。

在07-10-6,Atry <pop....@gmail.com> 写道:

pongba

unread,
Oct 6, 2007, 9:30:48 AM10/6/07
to pon...@googlegroups.com


On 10/6/07, Atry <pop....@gmail.com> wrote:

在我的认知中,"低耦合" 的同义词就是"良好定义的接口"。一个模块内部实现代码与耦合无关。

写一个内部函数的时候考虑耦合是愚蠢的。内部函数质量高低的唯一标准是代码长度——代码越短越好。这一点正是前段时间" Linus 炮轰事件"给我的启示。"用 C 编码,用 C++ 实现"也是这个意思。模板这样的猥琐特性有利于缩短代码长度,但反而增加耦合。所以模板用在模块内部( cpp 文件中)就无所谓,用来约定接口就不妥。

如果将修改代码的代价考虑进去呢?

// module 1
void g() throws E1, E2;
void h() throws E3;

// module2
void f() throws E1, E2, E3 {  g(); h(); }

// module3
void i() { try { f(); } catch(E1) { ... }  catch(E2) { ... } catch(E3) { ... } }

如果module1抛出的异常修改了,那么module2就受到无辜牵连(module3反正天生是挂了),需要重新部署。这也是Anders指出的问题之一。

yq chen

unread,
Oct 6, 2007, 9:37:07 AM10/6/07
to pon...@googlegroups.com
C++之所以不适合作为模块间的接口,我认为主要是C++缺乏必要的二进制兼容性,而不是说C++不能作为模块间的接口。
 
关于模块内的耦合,我认为也是尽量的少为秒。比如你写了一个函数作为模块对外的API,确实简单,十分好用。但是如果我是一个服务程序,而你的函数需要依赖于MFC,我就要考虑能不能了。同样的道理,如果你的library有很大,我只用了其中一个加密算法,你就要我把这个library的DLL全部带进去,我也不会愿意的。
 
MFC以前的一个问题就是,哪怕我用一个CString,我都要把MFC DLL全部带进去,我十分反感这样的做法(这是基于商业绑定的猥琐做法),所以我尽量就不用CString。

 
在07-10-6,Atry <pop....@gmail.com> 写道:

pongba

unread,
Oct 6, 2007, 9:52:22 AM10/6/07
to pon...@googlegroups.com


On 10/6/07, yq chen <mephist...@gmail.com> wrote:
C++之所以不适合作为模块间的接口,我认为主要是C++缺乏必要的二进制兼容性,而不是说C++不能作为模块间的接口。

正解!
而且,如果不基于字符串来调用虚函数的话,就算虚函数表的二进制布局再精确,也还是不能往发布的接口上添加函数,甚至调换函数的顺序也可能破坏二进制兼容性。
当然,按照原则,发布的接口根本不能添加函数。不过那是另一回事了。

关于模块内的耦合,我认为也是尽量的少为秒。比如你写了一个函数作为模块对外的API,确实简单,十分好用。但是如果我是一个服务程序,而你的函数需要依赖于MFC,我就要考虑能不能了。同样的道理,如果你的library有很大,我只用了其中一个加密算法,你就要我把这个library的DLL全部带进去,我也不会愿意的。
 
MFC以前的一个问题就是,哪怕我用一个CString,我都要把MFC DLL全部带进去,我十分反感这样的做法(这是基于商业绑定的猥琐做法),所以我尽量就不用CString。
 

呃.. 你举的这个例子是模块内耦合吗?感觉上还是有点出入。这是模块依赖于另一个模块吧?
我的看法是耦合只有必要和不必要的,不必要的不管模块内还是模块外,都不好。理由是耦合导致修改代码的困难,牵一发动全身。《敏捷软件开发:原则,模式和实践》以及《设计模式》还有其它关于设计的文章只有一个目的:解耦和。这个要解开的耦合,当然实践中主要是模块间的(取决于你把模块定义成什么,是一个类,还是一个DLL),但我认为模块内照样适用和重要,甚至更重要。
因为之所以要解耦和就是为了减小修改的代价!(模块内的修改显然要比接口修改频繁得多)

yq chen

unread,
Oct 6, 2007, 10:28:21 AM10/6/07
to pon...@googlegroups.com
呵呵,这个例子我说的不太清楚。
 
相对于仅仅只需要包含一个头文件就能引用的功能,CString需要我们引入整个MFC DLL,这是一个很大的代价。
 
对于我们系统来讲,对CString的依赖属于模块间的耦合。但是对于MFC来说,CString的实现是MFC模块内的耦合问题。如果CString不需要依赖于MFC中不必要的乱七八糟的东西,那我们的客户程序就可以依赖于一个粒度很小的模块。
 
 
我的意思是:如果模块内的耦合尽量少的话,那么它的客户就有可能只依赖于一个粒度很小的模块,否则可能就必须依赖于粒度很大的模块。一个模块的内部耦合同样也会影响使用它的模块。
 
在07-10-6,pongba <pon...@gmail.com> 写道:

Atry

unread,
Oct 6, 2007, 2:49:10 PM10/6/07
to pon...@googlegroups.com
对于这种模块间的情况,f() 不应该简单的暴露底层异常,而是某种上层关心的异常。这个上层关心的异常不会因为底层接口改变而改变。所以,修改 g() 和 h() 不需要修改 i() ,但是需要修改 f()

上层关心的异常是应该在设计 f() 的时候确定的,决不会因为底层实现改变而改变!

在07-10-6, pongba <pon...@gmail.com> 写道:

Atry

unread,
Oct 7, 2007, 2:24:47 AM10/7/07
to pon...@googlegroups.com
我再说一遍。"低耦合" 的同义词就是"良好定义的接口"。没有接口就没有重用,模块内的所谓"最小代码修改"只是你的幻想而已。

在07-10-6,yq chen < mephist...@gmail.com> 写道:

red...@gmail.com

unread,
Oct 7, 2007, 3:33:39 AM10/7/07
to pon...@googlegroups.com
"良好定义" 的确切含义是什么 ?

"接口" 是广义的还是狭义的 ?


Atry 写道:
> 我再说一遍。"低耦合" 的同义词就是"良好定义的接口"。没有接口就没有重
> 用,模块内的所谓"最小代码修改"只是你的幻想而已。
>

Atry

unread,
Oct 7, 2007, 6:01:16 AM10/7/07
to pon...@googlegroups.com
良好定义的接口是简单的,正交的,互相分离的,自描述的。测试代码可以用来验证一个接口是否合理。好的接口很容易写出测试代码。

在07-10-7,red...@gmail.com < red...@gmail.com> 写道:

Atry

unread,
Oct 7, 2007, 6:09:25 AM10/7/07
to pon...@googlegroups.com
接口从广义到侠义可以有 3 个含义:
最广义——一切界面。包括用户界面,协议,API 定义。
一般广义——只包括 API 定义,公开结构定义这些东西。
最狭义——特指 Java 中的 interface 或者其他语言对应的纯虚类。

我这里的接口是第二个含义。一个接口就对应一个模块的公开定义。

在07-10-7,red...@gmail.com <red...@gmail.com> 写道:

Huafeng Mo

unread,
Oct 7, 2007, 10:46:44 PM10/7/07
to pon...@googlegroups.com
1. 这个问题回答起来真的很模糊。因为,在实践中的很多应用软件,对错误处理非常草率。(多数软件企业对错误处理没有完整的要求)。所以,很多软件根本不对系统抛出的错误做任何处理,顶多用一个"internal error"对话框敷衍了事,让用户摸不清头脑。即便是编程中的错误也被作为系统错误抛出,比如未对指针或句柄正确初始化,引发的错误,甚至被认为是正常的系统错误抛出,而不是使用前做有效性检验。

2. 造成无法继续执行的错误,抛异常。比如sql语法错误。可恢复的状态变化,用返回值。比如到达结果集尾部。

3. 我一直用C++,那么最方便的办法还是RAII。标准容器、智能指针什么的。能不new,就不new了。即便用C++/CLI,也尽可能用栈语义,实现RAII。C#和Java的finalization,实在有些恼人。

4. 不十分明白这个问题的含义。我一般都首先确保每个指针、资源句柄什么的是有效的。然后根据所调用的服务的说明书,确定如何捕捉、分类、传递错误。不同的应用在这方面不完全一样。

5. 没有统计过。在服务性程序中,一般大多数错误都是直接打包扔出来,通过高级的函数捕捉,由专门的处理程序处理。这需要在设计时就必须系统地考虑错误处理,定义明确的低级错误的解析表。


在07-9-30,pongba < pon...@gmail.com> 写道:
1. 在你经历的项目中,哪些情况被定义为错误,需要汇报的。(注意,这里的错误不包括编程错误(bug);而是指IO错误和文件错误这类。)给个模糊的答案即可:)

2. 你们使用的错误处理机制是error-code呢还是exception?

3. 你们如何确保error-safe的?也就是说,当错误发生的时候,如何确保能够正确clean up的?

4. 你们如何确保所有错误都被妥善(如果能handle的话)handle或者(如果不能handle的话)传播给上级的?

5. 你们的项目中,有多少错误是可以就地handle,有多少错误是就地没有足够上下文,需要传播到上层模块解决的?

感谢大家哈:-)

--
刘未鹏(pongba)|C++的罗浮宫
http://blog.csdn.net/pongba
TopLanguage
http://groups.google.com/group/pongba





--
反者道之动,弱者道之用

pongba

unread,
Oct 8, 2007, 8:05:27 AM10/8/07
to pon...@googlegroups.com


On 10/6/07, pi1ot <pilo...@gmail.com > wrote:
我一般是预料可能遇到的错误使用reutrn error code,比如fopen()失败,connect()超时之类。完全预料之外的使用
except,比如明明刚才已经fopen() succ,接下来fwrite()出错,就抛出except。
然后except如果发生在一个内部调用的接口,就只做自己范围内的清理工作之后继续throw,把最终决定权交给调用方或者顶层的
application来决定,最后如果是个工具性程序就terminated,如果是个service或者daemon就只log之然后忽略。

fopen和connect的失败虽然是可以预料到的,然而我认为还是应该抛出异常,因为error-code如果不check的话,代码执行流将会继续往下。而异常不存在这个问题。不过如果错误总能在立即调用端恢复的话,也是可以用error-code的。
Reply all
Reply to author
Forward
0 new messages