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

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

java 复制代码
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 使用 ? 符号:

java 复制代码
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. 魔法!

java 复制代码
a = b()
c = a.map(|v| v + 1)

...
// Deal with error later

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

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

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

java 复制代码
// 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可能会给予我们另一条线索。当函数返回时,它有特殊的清理语法:

java 复制代码
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 没有解释如何正确使用异常,所以每个人都以错误的方式使用异常。

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

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,做的是这样的事情:

java 复制代码
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)

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

相关推荐
小白不太白9501 分钟前
设计模式之 模板方法模式
java·设计模式·模板方法模式
Tech Synapse3 分钟前
Java根据前端返回的字段名进行查询数据的方法
java·开发语言·后端
xoxo-Rachel10 分钟前
(超级详细!!!)解决“com.mysql.jdbc.Driver is deprecated”警告:详解与优化
java·数据库·mysql
乌啼霜满天24911 分钟前
JDBC编程---Java
java·开发语言·sql
肥猪猪爸21 分钟前
使用卡尔曼滤波器估计pybullet中的机器人位置
数据结构·人工智能·python·算法·机器人·卡尔曼滤波·pybullet
色空大师24 分钟前
23种设计模式
java·开发语言·设计模式
闲人一枚(学习中)25 分钟前
设计模式-创建型-建造者模式
java·设计模式·建造者模式
Bruce小鬼37 分钟前
QT文件基本操作
开发语言·qt
2202_7544215442 分钟前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言