【CSDN 编者按】似乎没有一种编程语言能够正确处理错误!
本文翻译自https://www.amazingcto.com/best-way-to-handle-errors-for-a-programming-language/
未经授权,禁止转载!
作者 | Stephan Schmidt 译者 | 明明如月出品 | CSDN(ID:CSDNnews)当我们编写代码时,错误常常发生在我们调用其他函数的过程中:
fn f() {
// 当 b() 返回一个错误时,可能会出现错误
a = b()
...
}
问题在于:
有时我们希望直接从函数中返回,不处理错误
有时我们希望减轻错误的影响
有时我们希望延迟处理错误的时机,比如和其他错误一起处理,最好是用正常的控制流继续执行
每种编程语言都找到了一种不同的解决方案来应对这三个挑战。
Java
Java 是最早采用异常机制(Exceptions)来控制错误的编程语言之一。方法b() 可以在发生错误时抛出异常。调用函数如果什么都不做,此时调用函数 f() 就会将异常抛给它的调用者。当然,我们可以通过把调用包装在 try/catch 代码块中,以便稍后处理异常。
Java 方法的缺点是,一旦发生错误,就会打破正常的控制流程。我们要么处理它,要么让它传递上来。
Java 异常机制的一个缺点是声明检查异常。如果我们的函数 f() 声明了它的异常,而函数 b() 抛出了不同的异常,此时,我们就需要处理异常,因为它不能往上冒泡。
Rust
Rust 通过一个机制找到了这个问题的解决方案,这个机制可以自动将一个错误——即 b() 的错误——转换为另一个错误——即 f() 的错误。这样我们又可以让错误传递上来,而不用处理它。
Rust 使用 ? 来实现这一点:
fn f() {
// 让函数 f() 返回
// 错误自动转换并传递上来
a = b()?
...
}
Go
一些编程语言通过在返回值旁边返回一个错误码来处理这三个挑战。其中之一是 Go。
a, err := b()
接下来我们可以通过下面的方式处理错误:
if err != nil { .... }
或者选择从函数中返回。
除非我们想要对某些操作进行处理,否则在出现错误后,我们可以恢复正常的程序流程。
a = a + 1
如果出现了错误并且 a 是 nil,这就不起作用了。
现在我们每次都可以检查 a 是否存在:
if a != nil { .... }
但这会变得繁琐且难以阅读。
一些编程语言使用 Monad (Monads 是一种在函数式编程中使用的结构,它可以将程序函数和它们的返回值组合起来,并在一个类型中添加额外的计算)来处理错误后的控制流问题。
// a 是 Result<A,E> 类型
a = b()
有了 Result Monad,我就可以处理错误或从方法中返回。
如上所述,Rust 有一些特殊的语法用于返回:
a = b()?
有了问号,当 b() 返回错误时,函数将在那一行返回,并且错误会自动转换并传递上来。
我们也可以在错误的情况下执行正常的控制流,出现错误时,仍然可以使用 a ,非常神奇!
a = b()
c = a.map(|v| v + 1)
...
// 稍后处理错误
在出现错误的情况下,c 也会是一个错误,否则 c 将包含 a 加 1 的值。这样,无论错误是否发生,我们都可以在错误后有相同的控制流。
这使得对代码的推理变得更加容易。
Zig 通过对类型进行注释,以 ! 的形式简化了 Result<A,E> 的表示。
// 返回 i32
fn f() i32 {
...
}
// 返回 i32 或错误
fn f() !i32 {
...
}
Zig 还通过流分析解决了 Java 对异常声明的繁琐问题。它检查你的函数 f(),找出所有可能返回的错误。然后,如果你在调用代码中检查特定的错误,它会确保是详尽无遗的。
Rust 中的 ? 有一个特殊的语法,可以简化错误处理,出错时可立即返回。Java 有 try/catch 的特殊语法,如果我们不编写额外的代码,就不会立即返回并将错误信息返回给函数的调用者。
我们应该使用较简洁的语法
问题的关键在于:我们更经常做什么?返回错误还是继续执行?我们更常做的事情应该使用较简洁的语法。
在 Rust 中的 ? 的情况下,我们是否需要一个 ? 以便立即返回,或者用 ? 来阻止返回?
a = b()?
问号可以表示 “发生错误时返回”。或者,该行为可以是,如果 b() 返回错误,始终立即返回,而 ? 可以阻止这种情况。
这取决于哪种情况更常见。
Golang 可能会给我们另一个思路。
当函数返回时,它有一个特殊的语法用于执行一些清理操作:
f := File.open("my.txt")
// 退出函数时确保关闭文件
defer f.close()
a, err = b()
if err != nil {
// 这里调用 f.close()
return
}
Java 中的 finally 不太优雅。看起来人们认为错误应该传递上来,而我们需要在这种情况下进行简单的清理。
从我的经验来看,我也怀疑我们想让大多数错误自动转换后往上传递,因此 ? 可能应该表示我们不希望函数返回,Rust 却和该预期完全相反。
看起来 Java 在异常处理上是正确的。没有暴露向上传递错误的语法。但是它错过了自动转换和来自 Rust 的 Exception<V,E>,以及类似 Go 的本地、简单的 defer,而不是 Java 冗长的 finally。而且 Java 没有解释如何正确地使用异常,所以每个人都用错了。
假设有这样一种语言:
fn f() {
// b() 返回 Result<V,E> 或 Zig 中的 !V,
// 如果 b 是错误,f() 就返回
// a 是 V 类型
a = b()
// 错误时不返回,但 a 是 Result<V,E> 或 !V 类型
a = b()!
// 编译为 a = a.map(|v| v + 1)
a = a + 1
// 编译为 c = a.map(|v| v.c())
// c 是 Result<C,E> 类型
c = a.c()
...
}
这具有更高的可读性。
但是,当我们调用另一个方法时应该怎么办?