编程语言中错误处理机制的思考

举报
宇宙之一粟 发表于 2023/05/26 17:24:01 2023/05/26
【摘要】 似乎没有一种语言能正确处理错误当我们编写代码时,在调用其他函数时,函数内部会发生错误:fn f() {// Error can happen when b()// returns an error a = b() ...}由此产生的问题是:有时我们不想处理错误,只是从函数返回有时候我们想减轻错误有时候我们希望更晚处理错误-例如,处理其他错误。优选地,正常控制流继续。每种编程语言都找到了不同的...

似乎没有一种语言能正确处理错误

当我们编写代码时,在调用其他函数时,函数内部会发生错误:

fn f() {
// Error can happen when b()
// returns an error
 a = b()
 ...
}

由此产生的问题是:

  • 有时我们不想处理错误,只是从函数返回
  • 有时候我们想减轻错误
  • 有时候我们希望更晚处理错误-例如,处理其他错误。优选地,正常控制流继续。

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

Java 是第一批通过 Exceptions 提升到更高错误管理状态的大众语言之一。 b() 可以在错误时抛出异常。然后调用函数什么也不能做,在这种情况下,调用函数 f() 返回给它的调用者,并带有异常。或者它可以稍后通过将调用包装在try/catch 中来处理异常。Java 方法的缺点是在错误发生后我们不能有正常的控制流。要么我们处理,要么让它冒出来。

Java 异常机制的缺点之一是声明已检查的异常。如果我们的函数 f() 声明了它的异常,而函数 b() 抛出了不同的异常,我们需要以任何一种方式处理异常,因为它不会冒泡。

Rust 找到了一个解决方案,它有一种机制,可以自动将一个错误( b() )转换为另一个错误( f() )。这样我们就可以让错误冒出来而不处理它。Rust 使用 ? 符号:

fn f() {
 // Let function f() return
 // error autoconvert and bubble up
 a = b()?
 ...
}

一些编程语言通过在值旁边返回错误代码来处理这三个挑战。其中之一就是 Go :

a, err := b()

现在我们可以处理错误了

if err != nil { .... }

或者从我们的函数返回。我们可以在错误发生后有正常的程序流程-在错误情况下-除非我们想对一个操作:

a = a + 1

如果有错误并且 a 为nil,则不工作。

我们现在可以每次检查 a 的存在:

if a != nil { .... }

但这变得麻烦且快速不可读。

一些编程语言使用 Monads 处理错误后的控制流问题。

// a is of type Result<A,E>
a = b()

有了 Result Monad,我就可以处理方法的错误或返回。如上所述,对于返回 Rust 有一些特殊的语法:

a = b()?

带问号,函数将在 `b()` 返回错误时返回该行,并且错误会随着自动转换而冒泡。

我们也可以在错误的情况下使用正常的控制流,但仍然使用 a. 魔法!

a = b()
c = a.map(|v| v + 1)

...
// Deal with error later

在错误的情况下, c 也将是错误,否则 c 将包含 a 的值加1。这样,无论错误发生与否,我们都可以在错误发生后拥有相同的控制流。

这使得代码的推理更加容易。

Zig 通过用 ! 注释类型,有一个简短的 Result<A,E> 概念。

// Returns i32
fn f() i32 {
...
}

// Returns i32 or an error
fn f() !i32 {
...
}

Zig 还解决了 Java 中通过流分析声明异常的繁琐问题。它会检查你的函数 f() 并找出它可以返回的所有错误。然后,如果您检查调用代码中的特定错误,它会确保它是详尽的。

带有 ? 的 Rust 有一个特殊的语法来当场返回。Java 有特殊的语法 try/catch ,如果我们不写额外的代码,就不会当场返回并返回给函数的调用者。

问题是:我们经常做什么?返回错误或继续?我们经常使用的,应该有较少冗长的语法。对于 Rust 中的 ? case,我们应该需要一个 ? 来返回,还是需要一个 ? 来不返回?

a = b()?

? 可以表示“错误返回”。或者行为可以是,如果 b() 返回一个错误,而 ? 阻止了这个错误,那么总是当场返回。

这取决于发生的更频繁。

Golang可能会给予我们另一条线索。当函数返回时,它有特殊的清理语法:

f := File.open("my.txt")
// Make sure we close the file
// on exiting the function
defer f.close()

a, err = b()

if err != nil {
  // f.close() is called here
  return
}

Java 有一些不那么优雅的东西。看起来人们认为错误应该冒出来,在这种情况下,我们需要一些简单的清理。

从我的经验来看,我也怀疑我们会希望让大多数错误通过自动转换而出现,所以 ? 可能应该表示我们不希望函数返回,这与 Rust 正在做的相反。

Java 似乎是正确的,例外。没有语法意味着泡沫行为。它错过了自动转换和来自 Rust 的 Exception<V,E> ,以及一个本地的,简单的 defer ,如 Go,而不是非本地的,冗长的 Java 中 finally 。Java 没有解释如何正确使用异常,所以每个人都以错误的方式使用异常。

那么,一个假设的语言,像这样:

fn f() {
  // b() returns Result<V,E> or !V in Zig,
  // f() returns if b is an error
  // a is of type V
  a = b()

  // do not return on error but
  // a is of type Result<V,E> or !V
  a = b()!

  // compiles to a = a.map(|v| v + 1)
  a = a + 1

  // compiles to c = a.map(|v| v.c())
  // c is of type Result<C,E>
  c = a.c()
  ...
}

这具有更高的可读性。

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

// Does not work if d expects
// C as a parameter type
// and not Result<C,E>
d(c)

有些语言有一个特殊的语言语法来处理这个问题。Haskell 有 do ,Scala 有 for 但是你有特殊的代码围绕错误和特殊的上下文。这使得事情更难再读一遍,与本意相反。

所以最好抛出编译器错误。请记住,默认的方式是向上冒泡,并且 a 的类型是 V

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

a = b()
a = a + 1 // A is still Result<V,E>
if a instanceof Error {
 return
}
// A is now of type V
// because we checked for an error
d(a)

看起来每种编程语言都有一个最佳错误处理难题。从我所看到的,没有人成功过。

原文链接:Musings about error handling mechanisms in programming languages

【版权声明】本文为华为云社区用户翻译文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容, 举报邮箱:cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。