错误处理#
错误处理一直是我们语言设计的核心。接下来我们将解释 MoonBit 中的错误处理。我们假设您对 MoonBit 有一些了解,如果没有,请查看 MoonBit 之旅。
错误类型#
在 MoonBit 中,所有的错误类型都可以用 Error
,一个通用的错误类型,来表示。
但是,Error
不能直接构造。必须定义一个具体的错误类型,形式如下:
type! E1 Int // 错误类型 E1 具有一个构造器 E1,并带有一个 Int 负载
type! E2 // 错误类型 E2 具有一个没有负载的构造器 E2
type! E3 { // 错误类型 E3 类似于普通的枚举类型,有三个构造器
A
B(Int, x~ : String)
C(mut x~ : String, Char, y~ : Bool)
}
错误类型可以自动提升为 Error
类型,并且可以模式匹配回去:
type! 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 fail[T](msg : String, loc~ : SourceLoc = _) -> T!Failure {
raise Failure("FAILED: \{loc} \{msg}")
}
抛出错误#
关键字 raise
被用来中断函数执行并返回一个错误。
函数的返回类型可以包含错误类型,以表明函数可能返回一个错误。例如,以下函数 div
可能返回一个类型为 DivError
的错误:
type! DivError String
fn div(x : Int, y : Int) -> Int!DivError {
if y == 0 {
raise DivError("division by zero")
}
x / y
}
Error
类型可以在具体的错误类型不重要时使用。为了方便起见,您可以在函数名或返回类型后面加上后缀 !
,以表示使用了 Error
类型。例如,以下函数签名是等价的:
fn f() -> Unit! {
...
}
fn g!() -> Unit {
...
}
fn h() -> Unit!Error {
...
}
对于匿名函数和矩阵函数,您可以在关键字 fn
后面加上 !
后缀来实现这一点。例如:
type! IntError Int
fn h(f : (Int) -> Int!, x : Int) -> Unit {
...
}
fn g() -> Unit {
let _ = h(fn! { x => raise IntError(x) }, 0)
let _ = h(fn!(x) { raise IntError(x) }, 0)
}
对于在错误类型上是泛型的函数,您可以使用 Error
约束来实现。例如:
// Result::unwrap_or_error
fn[T, E : Error] unwrap_or_error(result : Result[T, E]) -> T!E {
match result {
Ok(x) => x
Err(e) => raise e
}
}
处理错误#
直接调用函数会在出现错误时直接重新跑出错误。你可以在函数应用中的函数名后面附加 !
。例如:
fn div_reraise(x : Int, y : Int) -> Int!DivError {
div(x, y) + 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
的主体是一个简单表达式时,大括号可以省略。例如:
let a = try div!(42, 0) catch {
_ => 0
}
println(a)
转换为 Result#
您还可以捕获潜在的错误,并将其转换为一等公民的 Result
type,方法是:
在可能引发错误的表达式前使用
try?
在函数名后添加
?
test {
let res = div?(6, 3)
inspect!(res, content="Ok(2)")
let res = try? div(6, 0) * div(6, 3)
inspect!(
res,
content=
#|Err("division by zero")
,
)
}
错误推导#
在 try
块中,可能引发多种不同类型的错误。当发生这种情况时,编译器将使用 Error
类型作为通用错误类型。因此,处理程序必须使用通配符 _
来确保捕获所有错误。例如:
fn f1() -> Unit!E1 {
...
}
fn f2() -> Unit!E2 {
...
}
try {
f1!()
f2!()
} catch {
E1(_) => ...
E2 => ...
_ => ...
}
您还可以使用 catch!
来重新抛出未捕获的错误,以方便处理。当您只想处理特定错误并重新抛出其他错误时,这很有用。例如:
try {
f1!()
f2!()
} catch! {
E1(_) => ...
}