为什么不建议用 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

13286 次点击
所在节点    Java
78 条回复
wysnylc
2019-11-16 16:06:17 +08:00
@scriptB0y #19 try-catch 是不建议滥用的例如用 try 包裹整个方法中的代码,但是这不是不用 try-catch 的接口不能因噎废食
这种 null 的检查 java8 中用 optional
工具是给人使用的不是放在头顶当神明敬畏的
kidlj
2019-11-16 16:12:02 +08:00
简单来说,Go 这种叫做 explicit error handling,try-catch 是 implicit error handling. 上面提到的那篇文章有对两种方式的利弊对比。当然,Go 当前提供的处理方式还有些简陋,但我认为方向是对的。这也是 Go 一贯的做事方式,再找到完整的解决方案之前,只提供最简单的设施,给设计者和社区足够的时间来找到最佳解决方案。error handling, go modules 以及范型都是这种处理方式。
wly19960911
2019-11-16 16:13:24 +08:00
3. 我并不喜欢。徒增难度而已,很多时候错误处理和 throw 的地方跨了好几个调用栈,你这意味着你每个调用栈都可以需要有处理来自 method 的 response。

我有一个业务是有 http 调用和 sql 调用,还有调用结束之后的 业务报错。 具体是 A -> B -> HTTP, A -> B -> SQL, 这个时候不管是 HTTP 或者 SQL 还是 B 里面业务逻辑,都可能会异常,但是这个异常都必须交给 A 来响应,说实话如果我写了 method response 的话,本身就是给自己增加烦恼,我还要在 B 里面区分 response 然后交给 A,这个流程代码会写的很冗长。
undeflife
2019-11-16 16:15:26 +08:00
try-catch 有没什么好争论的,使用返回值的问题是所有的引用都需要去判断返回值里是否含有错误信息,并一层层的向外返回,
java 里的 CheckException 是很好的设计,接口可以明确的告诉调用者有哪些问 Exeption 你需要处理,调用者也选择不处理直接 throw,可惜的是被滥用,甚至再 jdk 自身也有滥用的情况,才导致被人一直被人诟病,一度有人提出去掉所有的 Check Exception 只保留 RuntimeException ..
wysnylc
2019-11-16 16:15:38 +08:00
@wly19960911 #23 同样不喜欢 3,会导致方法的返回值结构完全被污染而且多层之间的定义可能完全不同,同一个标识为 1 的异常在不同层面的含义又不一样需要分开处理和转换
wysnylc
2019-11-16 16:16:23 +08:00
@undeflife #24 Check Exception 在很多时候确实没屁用......
wysnylc
2019-11-16 16:16:48 +08:00
@kidlj #22 java 在这里等着 go 来革命,加油!
DonaldY
2019-11-16 16:26:54 +08:00
感觉你想的就是 go 的机制。
lcdtyph
2019-11-16 16:27:58 +08:00
unhappy path 如果出现频率不可忽略的话,catch 段会耗费大量时间啊

比如某个 api 是接受用户输入的,输入简单处理之后交给内部的库去 parse,如果这个库的 pasererror 是 throw 出来的,那么这个接口只需要很少的代价就可以 dos
wysnylc
2019-11-16 16:36:34 +08:00
@lcdtyph #29
你想想看,假如没有 try catch,你每调用一次函数,都需要去判断执行结果,判断方式自然是 if else。
当程序中这些会出错误的函数少还好,但是假设你一段代码中有大量的程序要做这做判断,而且一般都是相关的代码放在一起的。这就意味着后面执行的逻辑会依赖你前面语句的执行情况,也就意味着你每调用一个可能会出现错误的函数的时候,都要判断是否成功,然后再继续执行后面的语句。导致你的这段代码中充斥着大量的 if else。
更极端一点,假设你的这段充满了 if else 判断的代码封装在某个函数里面,然后外层又有函数调用你这段函数,是否意味着外面这个函数也要去判断异常情况?你的错误可能会使用某个整数来作为错误代码,来表示不同的错误情况,可能会大大影响程序的可读性。而且每一层代码的错误处理都要和你的逻辑代码混在一起,写到最后你自己都会觉得恶心。

异常机制( try catch )就是用来解决这个问题的。
异常机制将所有的程序异常的情况和正常执行的代码分离开来,并提供统一的代码去处理不同的异常,而且针对不同类型的异常情况定义了不同的异常类,用于表示不同的异常情况,增加代码可读性。java 还提供了受检异常和非受检异常,受检异常会强制你去写 try catch 去处理异常情况,否则可能导致编译不通过,这对代码的健壮性很有帮助,避免人为的遗漏异常处理。
lcdtyph
2019-11-16 16:39:59 +08:00
@wysnylc
是的,这算是可读性与性能的 trade-off 吧,我只是想表达,在某些情况下 try catch 带来的优点可能没法掩盖它的问题,取决于需求
anonymous256
2019-11-16 16:41:23 +08:00
这是两种处理异常的不同思想,一种是使用错误状态码,一种是使用异常。

前者叫 LBYL (Look Before You Leap) ,中文的说法"三思而后行",在 Golang 很典型,大量的 if == nil.
后者叫 EAFP (Easier to Ask for Forgiveness than Permission),"宽恕比许可更容易?",在 Python 中最典型。

虽然 EAFP 也有不好的问题,但是 LBYL 有太多缺陷了:
1. 降低代码的可读性和清晰度,参考 golang.
2. 在操作本身,还需要冗余的检查工作。
3. 程序员可能会省略了必要的检查,导致隐藏的 bug。比如 golang 的 nil,slice out of index 等,会产生代码 bug。
4. 在执行检查和尝试操作之间,情况可能会发生变化.

LBYL 这种设计思想, 意味着你总是预见到了所有的错误, 并且在它发生前时, 已经做好了解决方案. 如果你没有预见到,
那就代码就会 bug 了。但是 EAFP,你也捕捉到非预期的错误,来避免程序整体性的崩溃。

如果你感兴趣, 看看这篇: https://eli.thegreenplace.net/2008/08/21/robust-exception-handling/
关于 go 的 ,https://blog.golang.org/errors-are-values
lcdtyph
2019-11-16 16:43:58 +08:00
@anonymous256
非预期的错误就不应该被捕获,直接 log 然后 abort 掉才对
wly19960911
2019-11-16 16:44:16 +08:00
@wysnylc 最主要的是问题还是一些人代码写的烂,不懂写过程和重构。(以下只针对不懂怎么写流程的新手)

通俗点说,多少人经历过业务逻辑自己来重构的?比如写一个过程,不少人从开始学习就以自己的角度来定义 interface 或者 private method,然后导致的问题就是自己并不是真正把一套流程看清楚然后编写的,比如一个业务流程被分到两个方法里面(当然,更多的还是流程写一个方法不重构的,偶尔写点 Utils 来处理下重复逻辑)。

再简单点说,一个方法分为业务 /流程代码, 以及数据处理代码。流程代码本来就只需要调用重构后的方法为主和业务判断处理就行了,结果一个方法写的和面条一样。最后连流程都不知道,还祈求这些人能进行流程上的 try catch 这个根本做不到...所以我带人的时候建议他们,别写一些自以为是的封装方法和 interface,写完了流程自己整理一下,然后根据流程重构一下,更别在流程代码里面写一些过长的数据处理。

这点我尤其要批评以 vue 和 angular 的部分前端开发, 一个组件下来连个有 return 都没有,连自己的流程都不清楚,还怎么控制流程,更不可能去做 try catch,做下来根本没法控制,只能引用更多的状态来控制多个方法的操作。本来不需要的东西结果越写越复杂。

所以还是强调我第一句,一些人写代码根本没有流程的概念。没有就多写一些面条方法然后自己多看一眼。怎么重构才对。

前端的话,我建议写方法养成一个习惯,一个只有一个异步调用的代码,分三个阶段,声明主要局部变量和把 this 上的数据 赋值 /浅拷贝 给局部变量,然后中间的所有流程不允许调用 this 上面的 data,然后赋值要等所有的操作再赋值(就像 react )。
但是比 react 多的步骤是局部变量,原因还是这块的话能对重构友好,首先对 debug 影响少(因为 debug 的时候你总会去看看谁调用了状态),第二是对重构友好,重构瞬间能产生一个没有副作用的 function。这样的话本身写出来的代码能简洁而且极易 debug 和维护。同时如果需要 try catch 也是很容易的。
wysnylc
2019-11-16 16:55:59 +08:00
@lcdtyph #31 32 楼说得非常明确了,希望你多阅读和理解
@anonymous256 #32 感谢回答和解惑,世界有你更精彩!
@wly19960911 #34 很多人开发的时候脑子里其实是个单线程,只会思考当前方法怎么写而不会思考上下级调用,兼容性,扩展性,导致代码一团糟,人菜不能怪语言
lcdtyph
2019-11-16 17:02:37 +08:00
@wysnylc
你还是不能正视 catch 段会带来效率损失这个事实。
wly19960911
2019-11-16 17:13:07 +08:00
@wysnylc 其实我这里也没有聊兼容性和扩展性,这个更多需要依赖业务和经验,可惜我也对这块也不是很熟悉。我工作经验还不是很久。

但是我能充分感受到的是,不用 try catch 最大的原因是因为看了一段代码之后,会开始思考究竟需要再哪个流程 try catch,那个流程怎么进行 try catch。但是往往面临的问题是我自己连 try catch 在哪里都不一定能确定下来

比如 一个 (流程 A) 里面执行了 (方法 B) ,B 执行了 (方法 C) ,A 结束之后执行 (流程 H),但是这个时候 C 里面 throw 了 error,我需要在 流程 A 里面处理,但是问题是我需要来自 B 的数据。这时候这个代码就写的一塌糊涂。这还是 3 层,如果是 4 层、5 层呢,怎么处理?


@lcdtyph 在运行效率面前我选择工作效率,如果舍弃了 try catch 还出了性能问题,那么已经是无力挽回了。有流程控制还能想办法优化。
ragnaroks
2019-11-16 17:19:38 +08:00
try-catch 好用但不应该滥用,我觉得 java 有点滥用的趋势,之前写了几个 MC 的插件,到处都是 try-catch
wysnylc
2019-11-16 17:21:11 +08:00
@wly19960911 #37 我已经不怎么想回答"try-catch 会有性能问题"这个问题了,java 从来就不以高效著称要追求极致效率请出门右转 C 语言,代码逻辑一塌糊涂还谈什么运行效率?
wysnylc
2019-11-16 17:21:57 +08:00
@ragnaroks #38 滥用是人的问题,不是 try-catch 的问题,而且不用 try-catch 更加可怕

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

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

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

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

© 2021 V2EX