错误处理#
错误处理一直是我们语言设计的核心。接下来我们将解释 MoonBit 中的错误处理。我们假设您对 MoonBit 有一些了解,如果没有,请查看 MoonBit 之旅。
错误类型#
MoonBit 中使用的错误值必须具有错误类型。错误类型可以用以下形式定义:
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)
}
函数的返回类型可以包含错误类型,以表明函数可能返回一个错误。例如,以下函数 div
可能返回一个类型为 DivError
的错误:
type! DivError String
fn div(x : Int, y : Int) -> Int!DivError {
if y == 0 {
raise DivError("division by zero")
}
x / y
}
这里使用了关键字 raise
来中断函数执行并返回一个错误。
默认错误类型#
MoonBit 提供了一个默认错误类型 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)
}
如上例所示,type!
定义的错误类型可以在引发错误时作为 Error
类型的值使用。
请注意,只有错误类型或 Error
类型可以用作错误。对于在错误类型上是泛型的函数,您可以使用 Error
约束来实现。例如:
// Result::unwrap_or_error
fn unwrap_or_error[T, E : Error](result : Result[T, E]) -> T!E {
match result {
Ok(x) => x
Err(e) => raise e
}
}
由于 Error
类型可以包含多个错误类型,对 Error
类型进行模式匹配必须使用通配符 _
来匹配所有错误类型。例如:
type! E4
type! E5
fn f(e : Error) -> Unit {
match e {
E4 => println("E1")
E5 => println("E2")
_ => println("unknown error")
}
}
处理错误#
有三种方式可以处理错误:
在函数应用中的函数名后面附加
!
,以便在出现错误时直接重新抛出错误,例如:
fn div_reraise(x : Int, y : Int) -> Int!DivError {
div!(x, y) // 如果 `div` 引发错误,则重新抛出错误
}
在函数名后面附加
?
,将结果转换为Result
类型的值,例如:
test {
let res = div?(6, 3)
inspect!(res, content="Ok(2)")
let res = div?(6, 0)
inspect!(
res,
content=
#|Err("division by zero")
,
)
}
Use
try
andcatch
to catch and handle errors, for example:
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")
}
catch
关键字是可选的,当 try
的主体是一个简单表达式时,大括号可以省略。例如:
let a = try {
div!(42, 0)
} catch {
_ => 0
}
println(a)
!
和 ?
符号也可以用于方法调用和管道运算符。例如:
type T Int
type! E Int derive(Show)
fn k(self : T) -> Unit!E {
...
}
fn l() -> Unit!E {
let x = T(42)
k!(x)
x.k!()
x |> k!()
}
然而对于可能引发错误的中缀运算符,如 +
*
,必须使用原始形式,例如 x.op_add!(y)
,x.op_mul!(y)
。
此外,如果函数的返回类型包含错误类型,则函数调用必须使用 !
或 ?
进行错误处理,否则编译器将报告错误。
错误推导#
在 try
块中,可能引发多种不同类型的错误。当发生这种情况时,编译器将使用 Error
类型作为通用错误类型。因此,处理程序必须使用通配符 _
来确保捕获所有错误。例如:
fn f1() -> Unit!E1 {
...
}
fn f2() -> Unit!E2 {
...
}
try {
f1!()
f2!()
} catch {
E1(_) => ...
E2 => ...
_ => ...
}
您还可以使用 catch!
来重新抛出未捕获的错误,以方便处理。当您只想处理特定错误并重新抛出其他错误时,这很有用。例如:
try {
f1!()
f2!()
} catch! {
E1(_) => ...
}
示例:除零#
我们将编写一个小例子来演示 MoonBit 错误处理系统的基础知识。考虑以下 div
函数,它将在除零时引发错误:
type! DivisionByZeroError String
fn div(x : Int, y : Int) -> Int!DivisionByZeroError {
if y == 0 {
raise DivisionByZeroError("division by zero")
}
x / y
}
在以前,我们通常使用 type
来定义一个包装器类型,该类型包装了某些现有的外部类型。然而,在这里,我们使用 !
将 type
附加到 DivisionByZeroError
,以定义一个错误类型,该类型包装了 String
。
type! E S
从S
构造一个错误类型E
就像 type
一样,type!
可能有一个像上面的 DivisionByZeroError
那样的数据,也可能没有,甚至可能像普通的 enum
一样有多个构造器:
type! ConnectionError {
BrokenPipe(Int,String)
ConnectionReset
ConnectionAbort
ConnectionRefused
}
要使用 DivisionByZeroError
类型,我们通常会定义一个函数,该函数通过在签名中定义返回类型为 T ! E
来表示它会引发错误,其中 T
是实际的返回类型,E
是错误类型。在这个例子中,它是 Int!DivisionByZeroError
。错误可以使用 raise e
抛出,其中 e
是 E
的实例,可以使用 S
的默认构造器构造。
任何错误的实例都是一个二等公民对象。这意味着它只能出现在返回值中。如果返回值包含错误,函数签名必须调整以匹配返回类型。
MoonBit 中的 test
块也可以看作是一个函数,返回类型为 Unit!Error。
调用一个可出错的函数#
一个可出错的函数通常有两种调用方式:f!(...)
和 f?(...)
。
直接调用#
f!(...)
直接调用函数。可能的错误必须在调用 f
的函数中处理。我们可以重新抛出它,而不实际处理错误:
// We have to match the error type of `div2` with `div`
fn div2(x : Int, y : Int) -> Int!DivisionByZeroError {
div!(x,y)
}
或者像其他许多语言一样使用 try...catch
块:
fn div3(x : Int, y : Int) -> Unit {
try {
div!(x, y)
} catch { // `catch` and `except` works the same.
DivisionByZeroError(e) => println("inf: \{e}")
} else {
v => println(v)
}
}
catch...
子句的语义类似于模式匹配。我们可以解包错误以检索底层的 String
并打印它。此外,还有一个 else
子句来处理 try...
块的值。
fn test_try() -> Result[Int, Error] {
// compiler can figure out the type of a local error-able function.
fn f() -> _!_ {
raise Failure("err")
}
try Ok(f!()) { err => Err(err) }
}
如果 try
的主体是一行代码(表达式),则大括号可以省略。catch
关键字也可以省略。在 try
主体可能引发不同错误的情况下,可以使用特殊的 catch!
来捕获一些错误,同时重新抛出其他未捕获的错误:
type! E1
type! E2
fn f1() -> Unit!E1 { raise E1 }
fn f2() -> Unit!E2 { raise E2 }
fn f() -> Unit! {
try {
f1!()
f2!()
} catch! {
E1 => println("E1")
// E2 gets re-raised.
}
}
转换为 Result#
提取值#
Result
类型的对象是 MoonBit 中的一等公民。Result
有 2 个构造器:Ok(...)
和 Err(...)
,前者接受一个一等公民对象,后者接受一个错误对象。
使用 f?(...)
,返回类型 T!E
被转换为 Result[T,E]
。我们可以使用模式匹配从中提取值:
let res = div?(10, 0)
match res {
Ok(x) => println(x)
Err(DivisionByZeroError(e)) => println(e)
}
f?()
基本上是一个语法糖,等价于
let res = try {
Ok(div!(10, 0))
} catch {
s => Err(s)
}
注意
T?
和f?(...)
之间的区别:T
是一个类型,T?
等价于Option[T]
,而f?(...)
是对可出错函数f
的调用。
除了模式匹配,Result
还提供了一些有用的方法来处理可能的错误:
let res1: Result[Int, String] = Err("error")
let value = res1.or(0) // 0
let res2: Result[Int, String] = Ok(42)
let value = res2.unwrap() // 42
or
如果结果是Ok
,则返回值,如果是Err
,则返回默认值unwrap
如果结果是Err
,则会崩溃,如果是Ok
,则返回值
映射值#
let res1: Result[Int, String] = Ok(42)
let new_result = res1.map(fn(x) { x + 1 }) // Ok(43)
let res2: Result[Int, String] = Err("error")
let new_result = res2.map_err(fn(x) { x + "!" }) // Err("error!")
map
将函数应用于内部的值;如果结果是Err
,则不执行任何操作。map_error
则相反。
与一些语言不同,MoonBit 对可出错值和可空值进行了区分。尽管有些人可能将它们类比对待,因为一个不包含值的 Err
对象就像 null
。MoonBit 知道这一点。
to_option
将Result
转换为Option
。
let res1: Result[Int, String] = Ok(42)
let option = res1.to_option() // Some(42)
let res2: Result[Int, String] = Err("error")
let option1 = res2.to_option() // None
内置错误类型和相关函数#
在 MoonBit 中,Error
是一个通用的错误类型:
// These signatures are equivalent. They all raise Error.
fn f() -> Unit! { .. }
fn f!() -> Unit { .. }
fn f() -> Unit!Error { .. }
fn test_error() -> Result[Int, Error] {
fn f() -> _!_ {
raise DivisionByZeroError("err")
}
try {
Ok(f!())
} catch {
err => Err(err)
}
}
尽管构造器 Err
期望一个 Error
类型,我们仍然可以将 DivisionByZeroError
类型的错误传递给它。
但是 Error
不能直接构造。它是用来传递的,而不是直接使用:
type! ArithmeticError
fn what_error_is_this(e : Error) -> Unit {
match e {
DivisionByZeroError(_) => println("DivisionByZeroError")
ArithmeticError => println("ArithmeticError")
... => println("...")
_ => println("Error")
}
}
Error
通常用于不需要具体错误类型的情况,或者简单地用来捕获所有的子错误。
由于 Error
包含多种错误类型,这里不允许部分匹配。我们必须通过提供一个通配符 _
来进行兜底匹配。
我们通常使用内置的 Failure
错误类型来表示通用错误:通用意味着它用于不值得单独定义类型的错误。
fn div_trivial(x : Int, y : Int) -> Int!Failure {
if y == 0 {
raise Failure("division by zero")
}
x / y
}
除了直接使用构造器,函数 fail!
提供了一个快捷方式来构造 Failure
。如果我们查看源代码:
pub fn fail[T](msg : String, ~loc : SourceLoc = _) -> T!Failure {
raise Failure("FAILED: \{loc} \{msg}")
}
我们可以看到 fail
只是一个带有预定义输出模板的构造函数,用于显示错误和源位置。在实践中,fail!
总是比 Failure
更常用。
其他用于打破控制流的函数有 abort
和 panic
。它们是等效的。在任何地方的 panic
都会手动在那个地方崩溃程序,并打印出堆栈跟踪。