编程语言中的错误处理

2次阅读

共计 2686 个字符,预计需要花费 7 分钟才能阅读完成。

在日常的编程过程中,不可避免地需要处理错误的情况,而每一种编程语言都自有其错误处理逻辑,其背后的考量是什么?下面来探讨一下各编程语言中的错误处理,尝试总结出一些通用的方法与原则。
一、什么是异常
讨论一个问题之前,第一步就是要明晰下它所涉及的概念。
首先,标题所说的错误是广义的错误,它包括异常(Exception)与错误(Error)。下文中提到的『错误』均为狭义的区别与异常的错误。
程序中的异常(Exception)是指发生在程序执行过程中非频繁非正常的事件,它位于程序正常流程之外。异常大致可分为两类:

硬件异常:由 CPU 发起,它们可能是某些指令序列的执行导致的。比如除零或访问非法内存地址等。
软件异常:由应用程序或操作系统显式发起。例如系统可以检测到指定的参数非法值。

编程语言中的异常则属于软件异常。
而程序中的错误(Error),通常是指发生在程序执行过程中正常的事件,它就在程序正常流程范围之内。
二、正确区分异常与错误
与异常概念最容易混淆的就是错误。二者通常可以通过下面三个维度来区分:是否正常 / 可预期 / 终止程序

概念
是否正常
是否可预期
是否终止程序

异常


错误


前两个维度主要是对概念的描述,最后一个维度(是否终止程序,即是否可恢复)建议作为定义错误与异常的标准。如果一个事件它不可恢复应该定义为异常,及时终止程序退出,避免程序进入不可预知的状态(如造成数据不一致);如果一个事件可以预测出错误,那么就应该 check,并做一些相应的恢复处理。如 Golang 中的 Error 与 Panic 就是遵循该原则而设计。
三、错误处理
区分了异常与错误,下一步则是考虑针对错误的处理机制。
正确地区分了异常与错误的概念,我们就可以根据具体场景,正确地定义出异常与错误,以及安排相应的错误处理。
通常,编程语言中的错误处理可以分为两类:

check 式,检查返回值,以 C 语言为代表,Go 亦如此;
try/catch 式,目前大部分主流编程语言中的异常处理均采用类似方式,如 C ++/Java/PHP/JavaScript 等。

虽然目前主流编程语言中的异常处理均采用『try/catch』式的原则,但是大都数在写代码过程中都是双管齐下的,依据具体的场景,选择合适的处理方式(抛异常 or 检查返回值)。
3.1 check 式
最早的 C 语言是通过检查函数返回值 (通常零值 / 非空成功;非零值 / 空失败) 来进行错误处理的。如定义一个函数:
int foo() {
// <try something here>
if (failed) {
return 1;
}
return 0;
}
调用者则在进行下一步操作之前,需要判断 foo 函数返回值:
int err = foo();
if (err) {
// Error! Deal with it.
}

基于 C 或者底层级别的系统均是通过这种检查返回值的方式来处理错误的。如 Window 和 Linux 操作系统级别的调用(API)。
这种方式很简单,代码可读性也较好,但是写起来非常繁琐,这意味着你需要对每一个函数在调用之前的都需要手动 check 一下。而且,一旦忘记检查,很容易出现 bug。
Golang 则在 C 语言的基础上增加了更符合现代编程语言的语法和库。它允许函数有两个返回值,通常最后一个返回值为 Error 类型,调用者可以通过检查该类型返回值来检查函数返回情况,没有错误则使用第二个返回值,继续接下来的业务逻辑操作。如:
func foo()(int, error){
// <try something here>
if (failed) {
return -1, errors.New(“something error”)
}
return 0, nil;
}
调用:
if sum, err := foo(); err != nil {
// Deal with the error.
}
// do something with sum

Golang 的实现方式看起来比 C 语言更加优雅一些,但是频繁地检查返回值仍然不可避免。
C 语言在不使用 goto 语句的情况下,异常代码复用几乎不可能,Golang 也难以解决这个问题。
于是在后来发展起来的面向对象编程语言中,大部分都引入了类似 try/catch 式的异常处理机制。
3.2 try/catch 式
下面主要以 Java 语言举例说明
Java 中所有的错误处理均基于 Throwable 顶层父类,其下有两个子类:Error,它表示不希望被程序捕获或者是程序无法处理的错误。另一个是 Exception,它表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常。这是 Java 对异常与错误的划分,并且在此基础上为进一步提高程序健壮性,引入了 checked 异常与 unchecked 异常概念:针对那些除了 RuntimeException 与其子类,以及错误(Error),其他的都是需要编译时强制检查的异常,否则编译器会报错。
try
{
method();
}
catch (IOException ioe)
{
System.out.println(“I/O failure”);
}

// …

void method() throws IOException
{
throw new IOException(“some text”);
}
也正是 Java 的这种处理方式让人诟病:checked 异常容易让相关代码里充斥着大量的 try/catch,使代码同样变得晦涩难懂。同时,checked 异常所起到的作用也只是将捕获的异常,包装成运行时异常,然后再重新抛出。
正如,前文所言,每一门编程语言在设计之初都有自身的考量,且在进行实际的错误处理时均会同时考虑『check』与『try/catch』两种方式。
在 C# 中,并没有引入 checked 异常概念,而是把检查的义务又『还给』了开发者。
除此之外,try/catch 式异常处理通常会有很大的性能开销,故应当慎用。
四、『check』OR『try/catch』
即使发展至今,关于异常处理(『try/catch』)与检查返回值(『check』)这两种错误处理方式仍然争议不断。
check 式与 try/catch 式两种错误处理的方式,没有哪一种是绝对优势的,都有各自的优缺点,这有赖于语言设计者当时的权衡与抉择。但是不管哪种编程语言,基本衍生于这两类处理方式。
REFERENCES
http://www.cs.tut.fi/~popl/ny…http://joeduffyblog.com/2016/…https://www.zhihu.com/questio…https://nedbatchelder.com/tex…https://www.javaworld.com/art…

正文完
 0