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)