OCaml 教程 / 引用与可变性
引用与可变性
概述
OCaml 默认使用不可变绑定,但同样支持可变状态。引用(ref)是 OCaml 中最常用的可变类型,它提供了一种安全的方式来修改值。理解何时使用可变与不可变状态,是写出高效 OCaml 代码的关键。
ref 类型
ref 是一个包含单个可变字段的记录类型:
(* ref 的内部定义 *)
(* type 'a ref = { mutable contents : 'a } *)
(* 创建引用 *)
let counter = ref 0
let name = ref "Alice"
let scores = ref [1; 2; 3]
(* 查看 ref 的类型 *)
(* counter : int ref *)
(* name : string ref *)
(* scores : int list ref *)
解引用 !
使用 ! 操作符获取引用中的值:
let x = ref 42
let value = !x (* => 42 *)
let names = ref ["Alice"; "Bob"]
let current_names = !names (* => ["Alice"; "Bob"] *)
(* 复合使用 *)
let a = ref 10
let b = ref 20
let sum = !a + !b (* => 30 *)
赋值 :=
使用 := 操作符修改引用的值:
let counter = ref 0
let () =
counter := !counter + 1; (* counter 现在是 1 *)
counter := !counter + 1; (* counter 现在是 2 *)
counter := !counter * 3; (* counter 现在是 6 *)
Printf.printf "counter = %d\n" !counter
(* 输出: counter = 6 *)
⚠️ 注意::= 的返回类型是 unit,因此不能链式赋值。ref 是记录,:= 只是修改记录的 contents 字段。
ref 操作速查
| 操作 | 语法 | 类型 | 说明 |
|---|---|---|---|
| 创建 | ref value | 'a ref | 初始化引用 |
| 读取 | !ref | 'a | 获取当前值 |
| 赋值 | ref := value | unit | 修改值 |
| 递增 | incr ref | unit | ref := !ref + 1 |
| 递减 | decr ref | unit | ref := !ref - 1 |
(* incr 和 decr 是标准库中的便捷函数 *)
let n = ref 0
let () = incr n (* n = 1 *)
let () = incr n (* n = 2 *)
let () = decr n (* n = 1 *)
可变记录字段
除了 ref,记录字段也可以是可变的:
type account = {
name : string;
mutable balance : float;
mutable transactions : float list;
}
let deposit acc amount =
acc.balance <- acc.balance +. amount;
acc.transactions <- amount :: acc.transactions
let withdraw acc amount =
if acc.balance >= amount then begin
acc.balance <- acc.balance -. amount;
acc.transactions <- (-.amount) :: acc.transactions;
true
end else
false
let create_account name = {
name;
balance = 0.0;
transactions = [];
}
let () =
let acc = create_account "Alice" in
deposit acc 1000.0;
deposit acc 500.0;
let _ = withdraw acc 200.0 in
Printf.printf "%s 的余额: %.2f\n" acc.name acc.balance;
Printf.printf "交易记录: ";
List.iter (fun t -> Printf.printf "%.2f " t) acc.transactions;
print_newline ()
(* 输出:
Alice 的余额: 1300.00
交易记录: -200.00 500.00 1000.00
*)
💡 提示:可变记录字段用 mutable 关键字标记,修改用 <- 而不是 :=。
Array 模块
数组是 OCaml 中最重要的可变数据结构:
(* 创建数组 *)
let arr = [|1; 2; 3; 4; 5|]
let zeros = Array.make 10 0 (* 10 个 0 *)
let range = Array.init 5 (fun i -> i * 2) (* [|0; 2; 4; 6; 8|] *)
(* 访问元素 *)
let first = arr.(0) (* => 1 *)
(* 修改元素 *)
let () = arr.(0) <- 10 (* arr = [|10; 2; 3; 4; 5|] *)
(* 数组长度 *)
let len = Array.length arr (* => 5 *)
(* 遍历 *)
let () = Array.iter (fun x -> Printf.printf "%d " x) arr
(* 输出: 10 2 3 4 5 *)
(* 带索引遍历 *)
let () = Array.iteri (fun i x ->
Printf.printf "arr.(%d) = %d\n" i x) arr
(* 数组映射(返回新数组) *)
let doubled = Array.map (fun x -> x * 2) arr
(* => [|20; 4; 6; 8; 10|] *)
(* 就地修改 *)
let () = Array.fill arr 1 3 0
(* arr = [|10; 0; 0; 0; 5|],从索引 1 开始填充 3 个 0 *)
(* 数组切片 *)
let sub = Array.sub arr 0 3
(* => [|10; 0; 0|] *)
(* 数组排序(就地) *)
let data = [|5; 3; 1; 4; 2|]
let () = Array.sort compare data
(* data = [|1; 2; 3; 4; 5|] *)
(* 数组复制 *)
let copy = Array.copy arr
(* 数组转列表 *)
let lst = Array.to_list arr
(* 列表转数组 *)
let arr' = Array.of_list [1; 2; 3]
Array 常用函数
| 函数 | 类型 | 说明 |
|---|---|---|
Array.make n v | int -> 'a -> 'a array | 创建 n 个 v 的数组 |
Array.init n f | int -> (int -> 'a) -> 'a array | 用函数初始化 |
Array.length | 'a array -> int | 长度 |
Array.get / .(i) | 'a array -> int -> 'a | 读取 |
Array.set / .(i)<- | 'a array -> int -> 'a -> unit | 写入 |
Array.map | ('a -> 'b) -> 'a array -> 'b array | 映射 |
Array.iter | ('a -> unit) -> 'a array -> unit | 遍历 |
Array.fold_left | ('a -> 'b -> 'a) -> 'a -> 'b array -> 'a | 左折叠 |
Array.sort | ('a -> 'a -> int) -> 'a array -> unit | 就地排序 |
Array.to_list | 'a array -> 'a list | 转列表 |
Array.of_list | 'a list -> 'a array | 从列表创建 |
⚠️ 注意:Array.sort 是就地排序,会修改原数组。如需保留原数组,先 Array.copy。
可变与不可变对比
不可变列表处理
(* 不可变风格:纯函数式 *)
let add_to_list lst x = x :: lst (* 返回新列表 *)
let remove_from_list lst x = List.filter (fun y -> y <> x) lst
let lst = [1; 2; 3]
let lst2 = add_to_list lst 0 (* lst 不变,lst2 = [0; 1; 2; 3] *)
let lst3 = remove_from_list lst 2 (* lst 不变,lst3 = [1; 3] *)
可变引用处理
(* 可变风格:命令式 *)
let lst = ref [1; 2; 3]
let () = lst := 0 :: !lst (* 直接修改 *)
let () = lst := List.filter (fun x -> x <> 2) !lst
(* lst = ref [0; 1; 3] *)
对比表
| 特性 | 不可变 | 可变 |
|---|---|---|
| 线程安全 | ✅ 天然安全 | ❌ 需要同步 |
| 推理难度 | 容易 | 较难 |
| 内存效率 | 共享子结构 | 每次修改可能分配 |
| 性能 | 某些操作 O(n) | 某些操作 O(1) |
| 调试 | 值不变 | 值会改变 |
副作用控制
OCaml 是多范式语言,副作用是允许的,但应谨慎使用:
(* 纯函数 — 无副作用 *)
let pure_add a b = a + b
(* 带副作用的函数 *)
let counter = ref 0
let impure_add a b =
incr counter; (* 副作用:修改外部状态 *)
a + b
(* 将副作用隔离到边界 *)
let process_items items =
(* 纯计算 *)
let results = List.map (fun x -> x * 2) items in
let filtered = List.filter (fun x -> x > 10) results in
(* 副作用只在最后 *)
List.iter (fun x -> Printf.printf "%d " x) filtered;
print_newline ();
filtered
💡 提示:遵循"函数式核心,命令式外壳"模式——核心计算使用纯函数,副作用(IO、状态修改)推迟到程序边缘。
命令式 vs 函数式风格
求和对比
(* 命令式风格 *)
let sum_imperative lst =
let total = ref 0 in
List.iter (fun x -> total := !total + x) lst;
!total
(* 函数式风格 *)
let sum_functional lst =
List.fold_left (+) 0 lst
(* while 循环风格 *)
let sum_while lst =
let total = ref 0 in
let remaining = ref lst in
while !remaining <> [] do
match !remaining with
| x :: rest ->
total := !total + x;
remaining := rest
| [] -> ()
done;
!total
查找最大值
(* 命令式 *)
let max_imperative lst =
let best = ref min_int in
List.iter (fun x -> if x > !best then best := x) lst;
!best
(* 函数式 *)
let max_functional lst =
List.fold_left max min_int lst
(* 模式匹配 + 递归 *)
let rec max_recursive = function
| [] -> raise (Invalid_argument "空列表")
| [x] -> x
| x :: rest -> max x (max_recursive rest)
累积计算
(* 计算平均值 — 命令式 *)
let avg_imperative lst =
let sum = ref 0.0 in
let count = ref 0 in
List.iter (fun x ->
sum := !sum +. x;
incr count
) lst;
!sum /. float_of_int !count
(* 计算平均值 — 函数式 *)
let avg_functional lst =
let (sum, count) = List.fold_left
(fun (s, c) x -> (s +. x, c + 1))
(0.0, 0) lst
in
sum /. float_of_int count
何时使用可变状态
| 场景 | 推荐 | 原因 |
|---|---|---|
| 函数参数传递 | 不可变 | 简洁、安全 |
| 循环计数器 | ref / for | 命令式更自然 |
| 缓存/记忆化 | Hashtbl (可变) | 性能需要 |
| 累积计算 | fold_left (不可变) | 更函数式 |
| 性能热点 | 可变数组 | 避免分配 |
| 并发/并行 | 不可变 | 线程安全 |
| 解析器状态 | ref / 可变记录 | 状态自然可变 |
| 配置 | 不可变记录 | 安全性 |
实际场景:缓存(Memoization)
(* 使用 Hashtbl 实现记忆化 *)
let memoize f =
let cache = Hashtbl.create 128 in
fun x ->
match Hashtbl.find_opt cache x with
| Some result -> result
| None ->
let result = f x in
Hashtbl.replace cache x result;
result
(* 记忆化的斐波那契 *)
let rec fib_raw n =
if n <= 1 then n
else fib_raw (n - 1) + fib_raw (n - 2)
let fib = memoize (fun n ->
let rec aux n =
if n <= 1 then n
else fib (n - 1) + fib (n - 2)
in
aux n
)
let _ = fib 40 (* 很快! *)
(* fib_raw 40 会非常慢 *)
实际场景:计数器
(* 线程安全的计数器 *)
module Counter = struct
type t = { mutable count : int; name : string }
let create name = { count = 0; name }
let increment t = t.count <- t.count + 1
let get t = t.count
let reset t = t.count <- 0
let to_string t =
Printf.sprintf "%s: %d" t.name t.count
end
let () =
let http_requests = Counter.create "HTTP 请求" in
let db_queries = Counter.create "DB 查询" in
(* 模拟处理 *)
for _ = 1 to 100 do
Counter.increment http_requests;
if Random.bool () then Counter.increment db_queries
done;
Printf.printf "%s\n" (Counter.to_string http_requests);
Printf.printf "%s\n" (Counter.to_string db_queries)
Weak 指针简介
OCaml 提供 Weak 模块用于创建弱引用,不阻止垃圾回收:
(* 创建弱指针 *)
let weak_ref = Weak.create 1 (* 大小为 1 的弱数组 *)
(* 设置弱引用 *)
let () = Weak.set weak_ref 0 (Some [1; 2; 3])
(* 读取弱引用 *)
let value = Weak.get weak_ref 0 (* => Some [1; 2; 3] 或 None *)
(* 实现简单的弱缓存 *)
module Weak_cache = struct
let cache : (string * string Weak.t) list ref = ref []
let add key value =
let weak = Weak.create 1 in
Weak.set weak 0 (Some value);
cache := (key, weak) :: !cache
let find key =
match List.assoc_opt key !cache with
| Some weak -> Weak.get weak 0
| None -> None
end
⚠️ 注意:弱引用中的值可能随时被 GC 回收。读取弱引用时必须检查是否为 None。
实用示例:命令行参数解析器
(* 使用可变状态构建简单的参数解析器 *)
module Arg_parser = struct
type arg_spec =
| Set of bool ref
| Set_string of string ref
| Set_int of int ref
| Unit of (unit -> unit)
| String of (string -> unit)
| Int of (int -> unit)
let parse specs args =
let i = ref 0 in
while !i < Array.length args do
let arg = args.(!i) in
match List.assoc_opt arg specs with
| Some (Set r) -> r := true; incr i
| Some (Set_string r) ->
incr i;
if !i < Array.length args then
r := args.(!i);
incr i
| Some (Set_int r) ->
incr i;
if !i < Array.length args then
r := int_of_string args.(!i);
incr i
| Some (Unit f) -> f (); incr i
| Some (String f) ->
incr i;
if !i < Array.length args then
f args.(!i);
incr i
| Some (Int f) ->
incr i;
if !i < Array.length args then
f (int_of_string args.(!i));
incr i
| None ->
Printf.printf "未知参数: %s\n" arg;
incr i
done
end
let () =
let verbose = ref false in
let output = ref "output.txt" in
let count = ref 10 in
let specs = [
("-verbose", Arg_parser.Set verbose);
("-output", Arg_parser.Set_string output);
("-count", Arg_parser.Set_int count);
] in
Arg_parser.parse specs [| "-verbose"; "-output"; "result.txt"; "-count"; "42" |];
Printf.printf "verbose=%b, output=%s, count=%d\n"
!verbose !output !count
(* 输出: verbose=true, output=result.txt, count=42 *)
业务场景
| 场景 | 推荐方案 |
|---|---|
| 计数器 | ref int |
| 缓存 | Hashtbl |
| 配置 | 不可变记录 |
| 临时状态 | ref 或可变记录 |
| 性能关键数据 | Array |
| 解析器位置 | ref int |
| 游戏状态 | 可变记录 |
| 事件系统 | mutable 列表 |