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

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

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)

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

相关推荐
HAPPY酷1 小时前
压缩文件格式实战速查表 (纯文本版)
python
invicinble1 小时前
spring相关系统性理解,企业级应用
java·spring·mybatis
祝余Eleanor1 小时前
Day 31 类的定义和方法
开发语言·人工智能·python·机器学习
背心2块钱包邮1 小时前
第6节——微积分基本定理(Fundamental Theorem of Calculus,FTC)
人工智能·python·机器学习·matplotlib
jiayong231 小时前
Spring IOC 与 AOP 核心原理深度解析
java·spring·log4j
larance1 小时前
修改jupyterlab 默认路径
python
卿雪1 小时前
Redis 线程模型:Redis为什么这么快?Redis为什么引入多线程?
java·数据库·redis·sql·mysql·缓存·golang
fish_xk2 小时前
c++基础扩展
开发语言·c++
lkbhua莱克瓦242 小时前
IO流练习(修改文件中的数据)
java·windows·学习方法·io流·java练习题·io流练习
阿沁QWQ2 小时前
C++继承
开发语言·c++