当我们编写代码时,在调用其他函数时,函数内部会发生错误:
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)
看起来每种编程语言都有一个最佳错误处理难题。从我所看到的,没有人成功过。