7f15c7ce39293e3b97819b7854a8dba2.gif

【CSDN 编者按】似乎没有一种编程语言能够正确处理错误!

本文翻译自https://www.amazingcto.com/best-way-to-handle-errors-for-a-programming-language/

未经授权,禁止转载!

作者 | Stephan Schmidt  译者 | 明明如月

出品 | CSDN(ID:CSDNnews)

当我们编写代码时,错误常常发生在我们调用其他函数的过程中:

fn f() {
// 当 b() 返回一个错误时,可能会出现错误
 a = b()
 ...
}

问题在于:

  • 有时我们希望直接从函数中返回,不处理错误

  • 有时我们希望减轻错误的影响

  • 有时我们希望延迟处理错误的时机,比如和其他错误一起处理,最好是用正常的控制流继续执行

每种编程语言都找到了一种不同的解决方案来应对这三个挑战。

b96dfeffbd0ef4b8bc26246111e33d04.png

Java

Java 是最早采用异常机制(Exceptions)来控制错误的编程语言之一。方法b() 可以在发生错误时抛出异常。调用函数如果什么都不做,此时调用函数 f() 就会将异常抛给它的调用者。当然,我们可以通过把调用包装在 try/catch 代码块中,以便稍后处理异常。

Java 方法的缺点是,一旦发生错误,就会打破正常的控制流程。我们要么处理它,要么让它传递上来。

Java 异常机制的一个缺点是声明检查异常。如果我们的函数 f() 声明了它的异常,而函数 b() 抛出了不同的异常,此时,我们就需要处理异常,因为它不能往上冒泡。

b5d1b59bea7a01ae1abd7a5ed6f5b845.png

Rust

Rust 通过一个机制找到了这个问题的解决方案,这个机制可以自动将一个错误——即 b() 的错误——转换为另一个错误——即 f() 的错误。这样我们又可以让错误传递上来,而不用处理它。

Rust 使用 ? 来实现这一点:

fn f() {
 // 让函数 f() 返回
 // 错误自动转换并传递上来
 a = b()?
 ...
}

a001341c9408134bd66ad10a6c927846.png

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 的特殊语法,如果我们不编写额外的代码,就不会立即返回并将错误信息返回给函数的调用者。

e92e407ca22a9efa8278247e60b02db7.png

我们应该使用较简洁的语法

问题的关键在于:我们更经常做什么?返回错误还是继续执行?我们更常做的事情应该使用较简洁的语法。

在 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()
  ...
}

这具有更高的可读性。

但是,当我们调用另一个方法时应该怎么办?

// 如果 d 需要的是 C 作为参数类型, 而不是 Result<C,E>,那么就不起作用
d(c)

一些语言有特殊的语法来处理这个问题。例如 Haskell 有 do,Scala 有 for。但是会存在很多处理错误的特殊代码和特殊上下文。这让事情变得复杂,代码变得难以阅读,和初衷相违背。

所以最好抛出一个编译错误。并记住,默认的方式是传递上去,a 是 V 类型。

我们可以通过控制流分析来减轻这种痛苦。一些编程语言,如 TypeScript,做了类似的事情

a = b()
a = a + 1 // A 仍然是 Result<V,E>
if a instanceof Error {
 return
}
// A 现在是 V 类型
// 因为我们检查了错误
d(a)
看起来每种编程语言都掌握了最佳错误处理谜题的一部分片段。

在我看来,似乎没有一种编程语言能够完美处理错误。

推荐阅读:

ChatGPT 之父推出加密货币钱包;Intel 被曝预算砍掉 10%、裁员 20%;Firefox 113 发布|极客头条

“请不要在 GitHub 上传我的代码!”

AI 读心术来了,准确率高达 82%?论文已刊登在 Nature!

271d504f667d21b03773a41260faad70.jpeg

Logo

20年前,《新程序员》创刊时,我们的心愿是全面关注程序员成长,中国将拥有新一代世界级的程序员。20年后的今天,我们有了新的使命:助力中国IT技术人成长,成就一亿技术人!

更多推荐