为什么不建议用 try catch?

2019-11-16 12:17:38 +08:00
 wysnylc

不问是不是,就问为什么。这个问题看来需要从头说起。

一句话解释: try catch 机制非常好。那些觉得 try catch 不行的人,是他们自己的水平有问题,无法理解这种机制。并且这群人写代码不遵守规则,喜欢偷懒,这才造成 try catch 不好的错觉。

详细解释: 1.程序要健壮,必须要设计报错机制。 最古老,也是最常见的,比如: bool CreateFile( ); //如果创建文件失败就返回 false,否则返回 true。 这种报错方式,显然不好。因为它没有给出产生错误的具体原因。

2.改进:一个函数或过程,会因为不同的原因产生错误,报错机制必须要把这些错误原因进行区分后,再汇报。 比如: int CreateFile(): //如果创建成功就返回 1. //如果是因为没有权限,导致失败,返回-1。 //如果是因为文件已经存在,导致失败,返回-2。 //如果是因为创建文件发生超时,导致失败,返回-3。 这样看上去,比 [ 1 ] 要好些,至少指出了比较具体的失败原因,但是,还不够。

3.很多情况下,函数需要把详细的原因,用字符串的方式,返回: class Result { ....int State;//同 [ 2 ] ....string ErrorMessage;//如果失败,这里将给出详细的信息,如果有可能,应该把建议也写上去。 }

Result CreateFile(); //如果创建成功,返回的 Result,State 为 1,ErrorMessage 为 null。 //如果是因为没有权限,导致失败,返回的 Result,State 为-1,ErrorMessage 为"用户 [ guest ] 没有权限在 [ C:] 这个目录下创建该文件。建议您向管理员申请权限,或者更换具有权限的用户。"。 //如果是因为文件已经存在,导致失败,返回的 Result,State 为-2,ErrorMessage 为"文件 [ C:\abc.txt ] 已经存在。如果需要覆盖,请添加参数:arg_overwrite = true"。 //如果是因为创建文件发生超时,导致失败,返回的 Result,State 为-3,ErrorMessage 为"在创建文件时超时,请使用 chkdsk 检查文件系统是否存在问题。"。

4.我个人推崇上面这种方式,完整,美观。但是这种流程,容易与正常的代码混在一起,不好区分开。因此,Java、C#等设计了 try catch 这一种特殊的方式: void CreateFile() //如果创建成功就不会抛出异常。 //如果是因为没有权限,导致失败,会抛出 AccessException,这个 Exception 的 Msg 属性为"用户 [ guest ] 没有权限在 [ C:] 这个目录下创建该文件。建议您向管理员申请权限,或者更换具有权限的用户。"。 //如果是因为文件已经存在,导致失败,会抛出 FileExistedException,这个 Exception 的 Msg 属性为"文件 [ C:\abc.txt ] 已经存在。如果需要覆盖,请添加参数:arg_overwrite = true"。 //如果是因为创建文件发生超时,导致失败,会抛出 TimeoutException,这个 Exception 的 Msg 属性为"在创建文件时超时,请使用 chkdsk 检查文件系统是否存在问题。"。

可见,上述机制,实际上是用不同的 Exception 代替了 [ 3 ] 的 State。

这种机制,在外层使用时: try { ....CreateFile( "C:\abc.txt" ); } catch( AccessException e ) { ....//代码进入这里说明发生 [没有权限错误] } catch( FileExistedException e ) { ....//代码进入这里说明发生 [文件已经存在错误] } catch( TimeoutException e ) { ....//代码进入这里说明发生 [超时错误] } 对比一下 [ 3 ] ,其实这与 [ 3 ] 本质相同,只是写法不同而已。

5.综上,我个人喜欢 [ 3 ] 这类面向过程的写法。但很多喜欢面向对象的朋友,估计更喜欢 [ 4 ] 的写法。然而 [ 3 ] 与 [ 4 ] 都一样。这两种机制都是优秀的错误处理机制。

6.理论说完了,回到正题,题注问:为什么不用 try catch ? 答:这是因为,很多菜鸟,以及新手,他们是这样写代码的: void CreateFile( ) //无论遇到什么错误,就抛一个 Exception,并且也不给出 Msg 信息。 这样的话,在外层只能使用: try { ....CreateFile( "C:\abc.txt" ); } catch( Exception e ) { ....//代码进入这里说明发生错误 } 当出错后,只知道它出错了,并不知道是什么原因导致错误。这同 [ 1 ] 。

以及,即使 CreateFile 是按 [ 4 ] 的规则设计的,但菜鸟在外层是这样使用的: try { ....CreateFile( "C:\abc.txt" ); } catch( Exception e ) { ....//代码进入这里说明发生错误 ....throw Exception( "发生错误" ) } 这种情况下,如果这位菜鸟的同事,调用了这段代码,或者用户看到这个错误信息,也只能知道发生了错误,但并不清楚错误的原因。这与 [ 1 ] 是相同的。

出于这些原因,菜鸟的同事,以及用户,并没有想到,造成这个问题是原因菜鸟的水平太差,写代码图简单省事。他们却以为是 try catch 机制不行。

因此,这就导致了二逼同事,以及傻比用户,不建议用 try catch。 原文地址:https://www.zhihu.com/question/29459586

13326 次点击
所在节点    Java
78 条回复
hantsy
2019-11-17 00:36:08 +08:00
@wysnylc Java 8 Future, Java 9 Flow 代替不了 Rxjava。
1. Java 8 中相关接口,没有 Publisher/Subscriber 模式,没有 Back Pressure 处理能力。而这两种才是 Reactive Stream 的精华。
2. Java 9 中 Flow API 只是简单的 Copy 了 ReactiveStream JVM 接口,仅带一个简单的实现。实用性远不如 Rxjava 提供的 Single, Oberverable, Maybe, Flowable 丰富和易用。
hantsy
2019-11-17 00:43:48 +08:00
Java 异常本来就有两种分类, 大约 Spring 的人都忘记了 try、catch,一味的认为 RuntimeException 才是正确的( Spring 内部的异常它自己有 Exception Translator 进行转换,最终丢客户端)。其实不然, 异常也应该是系统设计上约束的一种。

比如你设计给一个服务给外部调用,必须强迫服务做什么检查步骤,用异常封装是最直接的表达方式,每一种异常路径用一个异常,比如,CreditCardExpiredException,UsernameWasTakeException,一目了然。
opengps
2019-11-17 00:57:58 +08:00
我的个人习惯,为了防止意外错误,开发期间避免 try catch,但是上线前一定都加上。开发期间加了容易隐藏问题。上线必须加是为了程序健壮,当然 catch 里必须记录日志。
我做 socket 服务端,任何异常都不允许中断程序运行,所以上线时候即使人为没错,也必须加一层以防万一
iwtbauh
2019-11-17 01:02:20 +08:00
@wysnylc #4

外星人可读的错误消息是认真的??

请看#2 第二句话

你这种设计会导致实现各种功能的函数和本地化机制紧密耦合。

正确的做法是返回(或通过异常)错误代码,或者某种定义良好的数据结构,然后通过错误代码或数据结构获取错误消息。这时候调用者可以选择最合适的国际化方式。也使本地化工作能更简单地完成。

例如 open 函数,如果使用 errno,则调用者负责国际化可以非常灵活,一种方法是

concat_string(_("open %s for %s failed, "), errorstr(errno))

在英文下:open /var/xxx for write failed, Permision denied
在中文下: 打开 /var/xxx 写入失败,拒绝访问

想象一下,如果 open 函数自己搞一套或者用某一套国际化机制,对于调用者来说则太重量级了,可能会很不舒服,调用者可能很难选择更好的方式来进行国际化。

考虑你需要调用库 a 和库 b,现在这两个库都返回人类可读的错误消息,然后分别搞了 2 套不同国际化机制,那个场面一定很好玩,而负责本地化的人也一定会向你抱怨。
MinQ
2019-11-17 01:05:04 +08:00
我们 python boy 就看着你们掐架,吃瓜 ing
soulzz
2019-11-17 01:10:39 +08:00
try{
//do sth
}catch(Exception e){
//错误处理代码
log.error(e.getMessage()+"代码区间 xxx")
}

通常快速迭代而没有反复推敲程序是否有问题时,配合上优秀的 log (必须是必要的 log,并且能够在日志中准确找到问题代码所在区块以及报错原因),下一个版本就能光速迭代
CosimoZi
2019-11-17 01:27:55 +08:00
monad 它不香吗?为什么要 try catch ?
wysnylc
2019-11-17 01:55:26 +08:00
@iwtbauh #64 这里在讨论 try-catch 和你本地化有啥关系,好好好就算你是举个例子想说明
那么有关系那么异常内容就不能做本地化了吗?你的想法很奇怪唉
charlie21
2019-11-17 07:57:18 +08:00
1 你理解问题的方式就是你处理问题的方式 ( 不同人把它理解成不同的啥样,就会把它处理成不同的啥样 )
2 这种能造成多种理解的片语 本身就有问题 ( 无论它被处理成不同的啥样 ) 。根本不值得深究
3 带着不同理解的人,不应该合作 ( 否则 在整体角度上 就出现混杂,在个人角度上 不合作是在降低耦合 )
4 在合作之前,应该统一好理解 ( 如果无法统一理解,那么 就降低耦合 )

异常处理 是有争议问题 背后是合作类问题,以上四步基本上是合作类问题的通解 。

-
wysnylc
2019-11-17 10:01:01 +08:00
@charlie21 #69 现在是两个派系的斗争而不是在所谓的合作,谁吃饱了撑的跟所有程序员合作啊
搞清楚场合再来显摆理论行不行?
nekoyaki
2019-11-17 13:51:24 +08:00
我不写 java,不过楼主说的大意我能领会。其他语言 /技术里,也常常会有一些机制,它们可能很强大,有一定的使用门槛,或是有一定的副作用,也容易造成滥用。
因此,就会有另一些人从避免滥用的角度出发,认为应当避免、甚至不应该使用这些机制。

对这类问题,我觉得是这样,如果我是小白,那么我会选择听从主流意见,避免使用这些机制; 但同时,我会去了解这些机制能带来什么好处,能解决什么问题,但相应地使用这些机制是否有风险和场景条件、带来的代价是什么。
如果我能够作出判断,认为我愿意支付这些代价和风险,来换取这些好处,我就会使用。

一味地说不要使用 XX,不是一个解决问题的思路。
godoway
2019-11-18 08:51:47 +08:00
@newtype0092
其实不如学 rust 的 match
match(rs) {
Ok(target) => dosomething,
Err(e)=> dosomething
}
solome
2019-11-18 10:55:40 +08:00
go\rust 的处理机制都是很优秀的思路。

个人还是很喜欢将异常也作为函数执行返回结果的一种。比如 js 中稍微封装下也会避免 滥用 try..catch... 的情况。

```
const noTryCatch = (func) => {
try {
const res = func()
return [res, null]
} catch (error) {
return [null, error]
}
}

const [res, err] = noTryCatch(() => /* 你的执行体 */)
```
newtype0092
2019-11-18 14:39:49 +08:00
@wysnylc #60 我不是想 return null,而是假设在函数的返回值里加入包含错误的 meta 数据,优先针对 meta 数据做错误处理,没有错误时才处理后续逻辑,说白了就是给 go 加个语法糖,多返回的那个 err 隐藏封装起来,不用写的那么痛苦。原理上还是和 try catch 一样,只是把范围限定到最小,防止什么不懂的人瞎 JB 一个大括号全扩起来 catch 下就不管了。。。
try catch 搞成大括号说明白了就是方便不用处理每一处可能的错误,把很多异常一次性处理了,这样是省事了,可是很多人放任自流的就越省越大了。。。
rovins
2019-11-23 03:22:39 +08:00
异常当然是要 handle 的,但是并不应该非 3 即 4,异常参与业务逻辑的流程控制的话,还是对性能有很大消耗的。
所以对业务逻辑的处理部分应不应该使用 checked exception,一直以来是有争议的。
可以参考 icyfenix 和 R 大他们以前的讨论: http://icyfenix.iteye.com/blog/857722

http://www.iteye.com/topic/2038
wysnylc
2019-11-23 10:14:58 +08:00
@rovins #75 关于性能的问题你的资料中有这么一段话,我猜你没仔细看

所以如果十分在意性能,是可以优化的 try-catch 的性能消耗是与 if-else 一致的,性能差距在于 fillInStackTrace
写出可读可维护的代码比这点性能要强太多
waterlaw
2019-12-08 11:32:54 +08:00
Spring 中配置 AOP 处理异常, 然后就不需要 try catch 了, 只要打印关键信息日志, 其他地方的话倒是得使用 try catch 并注意异常信息别丢失。
bronya0
188 天前
@guyeu go 完美的丢掉了这两点,既没有异常堆栈,对代码侵入也很大

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/620152

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX