错误处理#

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

错误多态#

一个高阶函数在接受另一个函数作为参数时,另一个函数可能会抛出错误,也可能不会抛出错误,这反过来又影响了这个高阶函数的行为。

一个典型的例子是 Arraymap 函数:

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#

你可以使用 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 的主体是一个简单表达式时,大括号,甚至 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
}