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

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 := valueunit修改值
递增incr refunitref := !ref + 1
递减decr refunitref := !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 vint -> 'a -> 'a array创建 n 个 v 的数组
Array.init n fint -> (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 列表

扩展阅读