错误处理#
错误处理一直是我们语言设计的核心。接下来我们将解释 MoonBit 中的错误处理。我们假设您对 MoonBit 有一些了解,如果没有,请查看 MoonBit 之旅。
错误类型#
在 MoonBit 中,所有的错误类型都可以用 Error
,一个通用的错误类型,来表示。
但是,Error
不能直接构造。必须定义一个具体的错误类型,形式如下:
suberror E1 Int // 错误类型 E1 具有一个构造器 E1,并带有一个 Int 负载
suberror E2 // 错误类型 E2 具有一个没有负载的构造器 E2
suberror E3 { // 错误类型 E3 类似于普通的枚举类型,有三个构造器
A
B(Int, x~ : String)
C(mut x~ : String, Char, y~ : Bool)
}
错误类型可以自动提升为 Error
类型,并且可以模式匹配回去:
suberror CustomError UInt
test {
let e : Error = CustomError(42)
guard e is CustomError(m)
assert_eq(m, 42)
}
由于 Error
类型可以包含多个错误类型,对 Error
类型进行模式匹配必须使用通配符 _
来匹配所有错误类型。例如:
fn f(e : Error) -> Unit {
match e {
E2 => println("E2")
A => println("A")
B(i, x~) => println("B(\{i}, \{x})")
_ => println("unknown error")
}
}
Error
通常用于不需要具体错误类型的情况,或者简单地用来捕获所有的子错误。
Failure#
一个内置的错误类型是 Failure
。
fail
是一个便利的函数,它只是一个带有预定义输出模板的构造函数,用于显示错误和源位置。在实践中,fail!
总是比 Failure
更常用。
pub fn[T] fail(msg : String, loc~ : SourceLoc = _) -> T raise Failure {
raise Failure("FAILED: \{loc} \{msg}")
}
抛出错误#
关键字 raise
被用来中断函数执行并返回一个错误。
函数的返回类型可以包含错误类型,以表明函数可能返回一个错误。例如,以下函数 div
可能返回一个类型为 DivError
的错误:
suberror DivError String
fn div(x : Int, y : Int) -> Int raise DivError {
if y == 0 {
raise DivError("division by zero")
}
x / y
}
Error
类型可以在具体的错误类型不重要时使用。为了方便起见,您可以在 raise
之后省略错误类型,以表示使用了 Error
类型。例如,以下函数签名是等价的:
fn f() -> Unit raise {
...
}
fn g() -> Unit raise Error {
...
}
对于在错误类型上是泛型的函数,您可以使用 Error
约束来实现。例如:
// Result::unwrap_or_error
fn[T, E : Error] unwrap_or_error(result : Result[T, E]) -> T raise E {
match result {
Ok(x) => x
Err(e) => raise e
}
}
错误多态#
一个高阶函数在接受另一个函数作为参数时,另一个函数可能会抛出错误,也可能不会抛出错误,这反过来又影响了这个高阶函数的行为。
一个典型的例子是 Array
的 map
函数:
fn[T] map(array : Array[T], f : (T) -> T raise) -> Array[T] raise {
let mut res = []
for x in array {
res.push(f(x))
}
res
}
然而,这样写会使得 map
函数总是有可能抛出错误,这并不是我们想要的。
因此,引入了错误多态。您可以使用 raise?
来表示可能会抛出错误,也可能不会抛出错误。
fn[T] map_with_polymorphism(
array : Array[T],
f : (T) -> T raise?
) -> Array[T] raise? {
let mut res = []
for x in array {
res.push(f(x))
}
res
}
test {
let array = [1, 2, 3]
let res = try? map_with_polymorphism(array, fn(x) { x + 1 })
let res2 = try? map_with_polymorphism(array, fn(x) { fail("") })
guard res2 is Err(_)
}
map_with_polymorphism
的签名将由实际参数推导而来。因此,在 res
后面的 try?
将会有一个警告,因为不会抛出错误。
处理错误#
直接调用函数会在出现错误时直接重新抛出错误。例如:
fn div_reraise(x : Int, y : Int) -> Int!DivError {
div(x, y) // 如果 `div` 引发错误,则重新抛出错误
}
但是,你可能想要处理错误。
Try … Catch#
你可以使用 try
和 catch
捕获和处理错误,例如:
fn main {
try div(42, 0) catch {
DivError(s) => println(s)
} else {
v => println(v)
}
}
除零
这里,try
用于调用可能引发错误的函数,catch
用于匹配和处理捕获的错误。如果没有捕获到错误,catch
块将不会执行,而是执行 else
块。
如果不需要在没有捕获到错误时执行任何操作,则可以省略 else
块。例如:
try { println(div(42, 0)) } catch {
_ => println("Error")
}
当 try
的主体是一个简单表达式时,大括号,甚至 try
关键字,都可以省略。例如:
let a = div(42, 0) catch { _ => 0 }
println(a)
转换为 Result#
您还可以捕获潜在的错误,并将其转换为一等公民的 Result
type,方法是在可能引发错误的表达式前使用 try?
:
test {
let res = try? (div(6, 0) * div(6, 3))
inspect(
res,
content=
#|Err("division by zero")
,
)
}
错误推导#
在 try
块中,可能引发多种不同类型的错误。当发生这种情况时,编译器将使用 Error
类型作为通用错误类型。因此,处理程序必须使用通配符 _
来确保捕获所有错误,并且通过 e => raise e
来抛出其他错误。例如:
fn f1() -> Unit raise E1 {
...
}
fn f2() -> Unit raise E2 {
...
}
try {
f1()
f2()
} catch {
E1(_) => ...
E2 => ...
e => raise e
}