强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

OCaml 教程 / OCaml 异常处理

OCaml 异常处理

异常(Exception)是 OCaml 中处理错误和非正常控制流的传统机制。本文全面讲解异常的定义、使用,以及何时应该选择 Result 类型作为替代方案。

异常定义:exception

异常用 exception 关键字定义,可以携带参数。

(* 无参数异常 *)
exception NotFound

(* 带参数的异常 *)
exception Invalid_input of string
exception Error_code of int * string

(* 使用 raise 抛出异常 *)
let lookup key assoc_list =
  match List.assoc_opt key assoc_list with
  | Some value -> value
  | None -> raise (Invalid_input (Printf.sprintf "Key '%s' not found" key))

let () =
  try
    let db = [("name", "Alice"); ("age", "30")] in
    let name = lookup "name" db in
    Printf.printf "Name: %s\n" name;
    let _ = lookup "email" db in
    ()
  with
  | Invalid_input msg -> Printf.printf "Error: %s\n" msg

raise 与 raise_notrace

exception My_error of string

(* raise 保留调用栈信息 *)
let risky_function () =
  raise (My_error "something went wrong")

(* raise_notrace 不保留调用栈,性能更好 *)
let fast_error () =
  raise_notrace (My_error "fast error")

let () =
  try risky_function () with
  | My_error msg ->
    Printf.printf "Error: %s\n" msg;
    Printf.printf "Backtrace: %s\n" (Printexc.get_backtrace ())

💡 提示raise_notrace 性能更好(避免栈回溯开销),适用于已知的、非错误的控制流异常(如提前退出循环)。需要调试时使用 raise

try-with 表达式

try ... with 是 OCaml 捕获异常的语法,它是一个表达式(有返回值)。

let safe_divide a b =
  try Some (a / b) with
  | Division_by_zero -> None

let () =
  let result = safe_divide 10 3 in
  Printf.printf "10 / 3 = %s\n"
    (Option.fold ~none:"error" ~some:string_of_int result);
  let result = safe_divide 10 0 in
  Printf.printf "10 / 0 = %s\n"
    (Option.fold ~none:"error" ~some:string_of_int result)

(* try-with 作为表达式 *)
let describe_age age =
  let category =
    try
      if age < 0 then raise (Invalid_argument "negative age");
      if age < 13 then "child"
      else if age < 18 then "teenager"
      else "adult"
    with
    | Invalid_argument _ -> "invalid"
  in
  category

let () =
  Printf.printf "Age 25: %s\n" (describe_age 25);
  Printf.printf "Age -1: %s\n" (describe_age (-1))

内置异常

OCaml 标准库预定义了多种常用异常:

异常触发条件常见来源
Failure of string通用运行时错误failwith, int_of_string
Invalid_argument of string参数不合法List.nth, 数组越界
Not_found查找失败List.assoc, Hashtbl.find
Division_by_zero除以零/ 运算符
Stack_overflow栈溢出深度递归
Out_of_memory内存不足大量分配
End_of_file到达文件末尾input_char
Sys_error of string系统 IO 错误文件操作
Match_failure of ...模式匹配不完全不完整的 match
Exit通用退出信号raise Exit
(* Failure *)
let parse_int s =
  try int_of_string s with
  | Failure _ -> 0

(* Not_found *)
let safe_assoc key xs =
  try Some (List.assoc key xs) with
  | Not_found -> None

(* Stack_overflow -- 处理深度递归 *)
let safe_factorial n =
  try
    let rec aux acc n =
      if n <= 0 then acc else aux (acc * n) (n - 1)
    in
    Some (aux 1 n)
  with
  | Stack_overflow -> None

let () =
  Printf.printf "parse: %d\n" (parse_int "42");
  Printf.printf "parse bad: %d\n" (parse_int "abc");
  Printf.printf "assoc: %s\n"
    (Option.value ~default:"N/A" (safe_assoc "x" [("x", "1"); ("y", "2")]))

异常与模式匹配

异常可以在 match 中使用,这是 OCaml 的独特特性。

type expr =
  | Num of float
  | Div of expr * expr

exception Division_by_zero_expr

let rec eval = function
  | Num x -> x
  | Div (a, b) ->
    let denominator = eval b in
    if denominator = 0.0 then raise Division_by_zero_expr
    else eval a /. denominator

let safe_eval expr =
  match eval expr with
  | value -> Ok value
  | exception Division_by_zero_expr -> Error "division by zero"
  | exception exn -> Error (Printexc.to_string exn)

let () =
  let expr = Div (Num 10.0, Div (Num 1.0, Num 0.0)) in
  match safe_eval expr with
  | Ok v -> Printf.printf "Result: %f\n" v
  | Error msg -> Printf.printf "Error: %s\n" msg

💡 提示match expr with | result -> ... | exception exn -> ... 语法可以直接在模式匹配中捕获异常,比 try-with 更灵活。

Result 类型替代方案

OCaml 4.03+ 引入了 Result 类型,提供了不使用异常的错误处理方式。

type ('a, 'b) result = Ok of 'a | Error of 'b

(* 使用 Result 替代异常 *)
let divide_safe a b =
  if b = 0 then Error "division by zero"
  else Ok (a / b)

let parse_int_safe s =
  try Ok (int_of_string s) with
  | Failure msg -> Error (Printf.sprintf "Cannot parse '%s': %s" s msg)

let bind_result f = function
  | Ok x -> f x
  | Error e -> Error e

let ( >>= ) = bind_result

(* 链式 Result 操作 *)
let calculate a_str b_str =
  parse_int_safe a_str >>= fun a ->
  parse_int_safe b_str >>= fun b ->
  divide_safe a b

let () =
  match calculate "10" "3" with
  | Ok v -> Printf.printf "Result: %d\n" v
  | Error msg -> Printf.printf "Error: %s\n" msg;

  match calculate "10" "0" with
  | Ok v -> Printf.printf "Result: %d\n" v
  | Error msg -> Printf.printf "Error: %s\n" msg

异常 vs Result 对比

维度异常Result
类型安全❌ 不在类型签名中('a, 'e) result
性能✅ 正常路径无开销⚠️ 需要包装/解包
强制处理❌ 可以忽略✅ 编译器警告
组合性❌ 难以链式组合bind/map
调试✅ 有栈回溯❌ 需手动记录
短路退出✅ 自然支持✅ 需要 bind
嵌套过深✅ 自动冒泡❌ 需要逐步 bind

异常性能影响

(* 异常作为控制流——高性能技巧 *)
exception Found of int

let find_in_matrix matrix target =
  try
    Array.iteri (fun i row ->
      Array.iteri (fun j v ->
        if v = target then raise (Found (i * Array.length row + j))
      ) row
    ) matrix;
    None
  with
  | Found idx -> Some idx

let () =
  let matrix = [|
    [| 1; 2; 3 |];
    [| 4; 5; 6 |];
    [| 7; 8; 9 |];
  |] in
  match find_in_matrix matrix 5 with
  | Some idx -> Printf.printf "Found at index: %d\n" idx
  | None -> Printf.printf "Not found\n"

⚠️ 注意:异常用于正常的控制流(如上面的提前退出循环)时性能很好,因为 raise_notrace 没有栈回溯开销。但不应滥用——代码可读性优先。

异常与模块接口

.mli 文件中声明模块可能抛出的异常是良好实践。

(* parser.mli *)
exception Syntax_error of string * int * int  (* message, line, col *)
exception Unexpected_eof

type t
val create : string -> t
val parse : t -> Ast.program
(** @raise Syntax_error if input contains syntax errors
    @raise Unexpected_eof if input ends unexpectedly *)

最佳实践:何时用异常 vs Result

场景推荐方案原因
程序员错误(bug)异常应该修复而非处理
可预期的错误Result调用方需要决策
IO 错误异常深层调用栈冒泡方便
解析错误Result可能需要收集多个错误
控制流(提前退出)异常性能好,语义清晰
API 边界Result类型安全,强制处理
性能关键路径异常(raise_notrace)避免包装开销

实际业务场景:用户注册验证

type validation_error =
  | Empty_name
  | Invalid_email of string
  | Password_too_short
  | Age_out_of_range of int

let validate_name name =
  if String.trim name = "" then Error Empty_name
  else Ok name

let validate_email email =
  if String.contains email '@' then Ok email
  else Error (Invalid_email email)

let validate_password pwd =
  if String.length pwd >= 8 then Ok pwd
  else Error Password_too_short

let validate_age age =
  if age >= 0 && age <= 150 then Ok age
  else Error (Age_out_of_range age)

let ( let* ) = Result.bind

let validate_registration name email password age =
  let* name = validate_name name in
  let* email = validate_email email in
  let* password = validate_password password in
  let* age = validate_age age in
  Ok (name, email, password, age)

let string_of_error = function
  | Empty_name -> "Name cannot be empty"
  | Invalid_email e -> Printf.sprintf "Invalid email: %s" e
  | Password_too_short -> "Password must be at least 8 characters"
  | Age_out_of_range a -> Printf.sprintf "Invalid age: %d" a

let () =
  match validate_registration "Alice" "[email protected]" "secure123" 25 with
  | Ok (n, e, p, a) -> Printf.printf "Valid: %s, %s\n" n e
  | Error err -> Printf.printf "Error: %s\n" (string_of_error err);

  match validate_registration "" "bad-email" "short" (-5) with
  | Ok _ -> Printf.printf "Valid\n"
  | Error err -> Printf.printf "Error: %s\n" (string_of_error err)

扩展阅读