错误处理#

错误处理一直是我们语言设计的核心。接下来我们将解释 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#

你可以使用 trycatch 捕获和处理错误,例如:

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(_) => ...
}