03 不可变数据
03 不可变数据
“可变状态是万恶之源——至少在并发编程中是如此。” — Rich Hickey
3.1 不可变性概述
不可变性(Immutability) 是函数式编程的核心原则之一:数据一旦创建就不能被修改。任何"修改"操作都会返回一个新的数据副本。
3.1.1 为什么需要不可变性
| 问题 | 可变数据 | 不可变数据 |
|---|---|---|
| 并发安全 | 需要锁/同步 | 天然安全 |
| 时间旅行调试 | 不可能 | 天然支持 |
| 变更追踪 | 需要手动实现 | 简单比较即可 |
| 缓存安全 | 可能被意外修改 | 安全 |
| 撤销/重做 | 复杂 | 简单的历史栈 |
| 推理难度 | 高(需要跟踪所有修改点) | 低(值不会变) |
3.1.2 值 vs 身份
这是理解不可变性的关键概念:
| 概念 | 含义 | 示例 |
|---|---|---|
| 值(Value) | 不可变的数据本身 | 数字 42、字符串 "hello" |
| 身份(Identity) | 随时间变化的逻辑实体 | 用户、购物车、银行账户 |
// 值:不可变
const x = 42;
// 不能改变 42 这个值
// 身份:随时间关联不同值
let account = { balance: 100 }; // 身份 account → 值 { balance: 100 }
account = { balance: 150 }; // 身份 account → 值 { balance: 150 }
// account 身份没有变,但关联的值变了
// 函数式做法:身份指向新的不可变值
const account1 = { balance: 100 };
const account2 = { ...account1, balance: 150 }; // 新的不可变值
// account1 和 account2 都是不可变的
3.2 不可变数据的基本实践
3.2.1 JavaScript 中的不可变性
// ❌ 可变操作
const user = { name: 'Alice', age: 30 };
user.age = 31; // 直接修改
// ✅ 不可变操作
const user = { name: 'Alice', age: 30 };
const updatedUser = { ...user, age: 31 }; // 创建新对象
// ❌ 数组变异
const items = [1, 2, 3];
items.push(4); // 修改原数组
items.splice(1, 1); // 删除元素
// ✅ 不可变数组操作
const items = [1, 2, 3];
const added = [...items, 4]; // 添加
const removed = items.filter((_, i) => i !== 1); // 删除
const updated = items.map((item, i) => i === 1 ? 99 : item); // 更新
3.2.2 Object.freeze 的局限
// Object.freeze 只是浅冻结
const obj = Object.freeze({
name: 'Alice',
address: { city: 'Beijing' },
});
obj.name = 'Bob'; // 静默失败(严格模式抛错)
obj.address.city = 'Shanghai'; // ⚠️ 仍然可以修改嵌套对象!
// 深冻结函数
const deepFreeze = (obj) => {
if (typeof obj !== 'object' || obj === null) return obj;
Object.freeze(obj);
Object.values(obj).forEach(deepFreeze);
return obj;
};
3.2.3 Python 中的不可变性
# Python 原生不可变类型
# tuple, frozenset, str, int, float, bool
# ❌ 可变
user = {'name': 'Alice', 'age': 30}
user['age'] = 31
# ✅ 不可变方式
user = {'name': 'Alice', 'age': 30}
updated_user = {**user, 'age': 31}
# 使用 pyrsistent 库
from pyrsistent import pmap, pvector, pset
user = pmap({'name': 'Alice', 'age': 30})
updated_user = user.set('age', 31) # 返回新对象
items = pvector([1, 2, 3])
added = items.append(4) # 返回新 vector
3.2.4 Rust 中的不可变性
// Rust 默认不可变
let x = 42;
// x = 43; // 编译错误!
// 需要显式声明 mut
let mut y = 42;
y = 43; // OK
// 结构体默认不可变
struct User {
name: String,
age: u32,
}
let user = User { name: "Alice".into(), age: 30 };
// user.age = 31; // 编译错误!
// 使用 clone 创建修改后的副本
let updated_user = User {
name: user.name.clone(),
age: 31,
};
// 使用 derive 宏简化
#[derive(Clone)]
struct User {
name: String,
age: u32,
}
let updated = User { age: 31, ..user.clone() };
3.2.5 Haskell 中的不可变性
-- Haskell 中所有值默认不可变
data User = User { name :: String, age :: Int }
user :: User
user = User "Alice" 30
-- 使用 Record update 语法创建新值
updatedUser :: User
updatedUser = user { age = 31 }
-- 原始 user 不受影响
-- age user == 30
-- age updatedUser == 31
3.2.6 Clojure 中的不可变性
;; Clojure 的数据结构默认不可变
(def user {:name "Alice" :age 30})
;; assoc 返回新 map,原 map 不变
(def updated-user (assoc user :age 31))
;; 两者都保持不变
;; (:age user) => 30
;; (:age updated-user) => 31
;; 向量操作
(def items [1 2 3])
(def added (conj items 4)) ;; [1 2 3 4]
;; items 仍然是 [1 2 3]
3.3 持久化数据结构(Persistent Data Structure)
持久化数据结构是指在修改时保留旧版本的数据结构。这是高效实现不可变性的关键。
3.3.1 朴素实现 vs 持久化实现
| 方式 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 朴素深拷贝 | O(n) | O(n) | 每次修改都复制全部数据 |
| 持久化数据结构 | O(log n) | O(1)* | 通过结构共享实现 |
| 原地修改 | O(1) | O(1) | 可变方式,不安全 |
*摊销成本
3.3.2 结构共享(Structural Sharing)
结构共享的核心思想:新旧版本的数据结构共享未被修改的部分。
原始 Trie(32 分支向量):
[A, B, C]
/ | \
[1,2] [3,4] [5,6]
添加元素 7 后:
[A, B, C']
/ | \
[1,2] [3,4] [5,6,7]
共享 ↑ 新创建
3.3.3 Clojure 的向量(Vector)
;; Clojure 向量使用 32 分支的 Trie
(def v1 [1 2 3 4 5])
;; conj 返回新向量,共享大部分结构
(def v2 (conj v1 6))
;; v1 和 v2 共享内部节点
;; v1 => [1 2 3 4 5]
;; v2 => [1 2 3 4 5 6]
;; 时间复杂度 O(log32 n) ≈ O(1)
;; 对百万级元素仍然高效
3.3.4 JavaScript 中使用 Immutable.js
const { List, Map } = require('immutable');
// 创建不可变 List
const list1 = List([1, 2, 3, 4, 5]);
const list2 = list1.push(6);
console.log(list1.toArray()); // [1, 2, 3, 4, 5] 未改变
console.log(list2.toArray()); // [1, 2, 3, 4, 5, 6]
// 创建不可变 Map
const map1 = Map({ name: 'Alice', age: 30 });
const map2 = map1.set('age', 31);
console.log(map1.toJS()); // { name: 'Alice', age: 30 }
console.log(map2.toJS()); // { name: 'Alice', age: 31 }
// 性能:大列表的不可变更新
const largeList = List(Array.from({ length: 1000000 }, (_, i) => i));
const updated = largeList.set(999999, -1); // O(log32 n) ≈ O(1)
3.3.5 Python 中使用 Pyrsistent
from pyrsistent import pvector, pmap, pset, freeze, thaw
# 持久化向量
v1 = pvector([1, 2, 3, 4, 5])
v2 = v1.append(6)
v3 = v1.set(0, 99)
# v1 不变
assert list(v1) == [1, 2, 3, 4, 5]
assert list(v2) == [1, 2, 3, 4, 5, 6]
assert list(v3) == [99, 2, 3, 4, 5]
# 深度嵌套的不可变数据
data = freeze({
'users': [
{'name': 'Alice', 'scores': [90, 85, 92]},
{'name': 'Bob', 'scores': [78, 82, 88]},
]
})
# 修改嵌套数据
from pyrsistent import assoc_in
updated = assoc_in(data, ['users', 0, 'scores', 0], 95)
3.3.6 Rust 中的 im crate
use im::{vector, Vector, OrdMap};
fn main() {
// 持久化向量
let v1: Vector<i32> = vector![1, 2, 3, 4, 5];
let v2 = v1.push_back(6);
let v3 = v1.update(0, 99);
// v1 未变
assert_eq!(v1, vector![1, 2, 3, 4, 5]);
assert_eq!(v2, vector![1, 2, 3, 4, 5, 6]);
assert_eq!(v3, vector![99, 2, 3, 4, 5]);
// 持久化 Map
let m1: OrdMap<String, i32> = OrdMap::new();
let m2 = m2.update("count".to_string(), 1);
// m1 仍为空,m2 包含 { "count": 1 }
}
3.4 Copy-on-Write(写时复制)
Copy-on-Write(COW)是一种优化策略:共享数据直到需要修改时才进行复制。
3.4.1 COW 原理
初始状态:
ref1 → [A, B, C]
引用 ref2:
ref1 → [A, B, C] ← ref2
(共享同一数据)
修改 ref2:
ref1 → [A, B, C]
ref2 → [A, B, D] (写时复制,创建新副本)
3.4.2 Rust 中的 COW
use std::borrow::Cow;
fn process(input: &str) -> Cow<str> {
if input.contains(' ') {
// 需要修改:创建新字符串
Cow::Owned(input.replace(' ', "-"))
} else {
// 不需要修改:借用原始数据
Cow::Borrowed(input)
}
}
fn main() {
let s1 = "hello-world";
let s2 = "hello world";
let r1 = process(s1); // 借用,无复制
let r2 = process(s2); // 创建新字符串
println!("{}", r1); // hello-world
println!("{}", r2); // hello-world
}
3.4.3 Swift 的 COW 实现
// Swift 的 Array、Dictionary、Set 自带 COW
var a = [1, 2, 3, 4, 5]
var b = a // 共享同一存储
b.append(6) // 此时才复制
print(a) // [1, 2, 3, 4, 5]
print(b) // [1, 2, 3, 4, 5, 6]
3.5 不可变数据的操作模式
3.5.1 更新嵌套对象
问题: 更新深层嵌套的数据
JavaScript(手动展开):
const state = {
user: {
profile: {
address: {
city: 'Beijing',
street: 'Main St',
},
},
},
};
// ❌ 糟糕:代码冗长
const updated = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
address: {
...state.user.profile.address,
city: 'Shanghai',
},
},
},
};
// ✅ 使用 immer 库
const { produce } = require('immer');
const updated = produce(state, (draft) => {
draft.user.profile.address.city = 'Shanghai';
});
Python(使用 lenses):
from lenses import lens
state = {
'user': {
'profile': {
'address': {
'city': 'Beijing',
'street': 'Main St',
}
}
}
}
# 使用 pyrsistent 的 assoc_in
from pyrsistent import freeze, thaw, assoc_in
frozen_state = freeze(state)
updated = assoc_in(frozen_state, ['user', 'profile', 'address', 'city'], 'Shanghai')
3.5.2 不可变更新的常用操作
| 操作 | JavaScript | Python | Rust (im) |
|---|---|---|---|
| 添加字段 | {...obj, key: val} | {**d, key: val} | map.insert(k, v) |
| 删除字段 | const {key, ...rest} = obj | {k:v for k,v in d.items() if k!='key'} | map.remove(k) |
| 更新字段 | {...obj, key: newVal} | {**d, key: newVal} | map.insert(k, v) |
| 数组追加 | [...arr, item] | [*lst, item] | vec.push_back(x) |
| 数组删除 | arr.filter(...) | [x for x in lst if ...] | vec.remove(i) |
| 深层更新 | immer produce | assoc_in | 递归更新 |
3.6 不可变性与性能
3.6.1 性能开销分析
| 操作 | 可变版本 | 不可变版本 | 开销来源 |
|---|---|---|---|
| 修改字段 | O(1) | O(log n)* | 持久化数据结构更新 |
| 创建对象 | O(1) | O(1) | 相同 |
| 内存占用 | 基准 | +10~30% | 共享节点开销 |
| 比较相等 | O(n) | O(1) | 引用比较即可 |
*使用结构共享时
3.6.2 优化策略
策略 1:批量更新
// ❌ 低效:多次更新
let state = { count: 0, name: 'Alice' };
state = { ...state, count: state.count + 1 };
state = { ...state, name: 'Bob' };
state = { ...state, count: state.count + 1 };
// ✅ 高效:合并更新
const state = { count: 0, name: 'Alice' };
const finalState = {
...state,
count: state.count + 2,
name: 'Bob',
};
策略 2:使用 Transducer 避免中间集合
// ❌ 创建中间数组
const result = [1, 2, 3, 4, 5]
.filter(x => x > 2) // 中间数组 [3, 4, 5]
.map(x => x * 2); // 最终数组 [6, 8, 10]
// ✅ 使用 Transducer(无中间数组)
const transducer = R.compose(
R.filter(x => x > 2),
R.map(x => x * 2)
);
const result = R.transduce(transducer, R.flip(R.append), [], [1, 2, 3, 4, 5]);
策略 3:结构相等性优化
// 使用引用相等性检查,O(1)
const isImmutableEqual = (a, b) => a === b; // 引用相同即相等
// 配合 React 的 memo/useMemo 使用
const ListItem = React.memo(({ item }) => {
return <div>{item.name}</div>;
}, (prev, next) => prev.item === next.item); // O(1) 比较
3.7 不可变性在各框架中的应用
3.7.1 React + Redux
// Redux reducer 必须返回新状态
const todoReducer = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: action.id, text: action.text, completed: false }];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
);
default:
return state;
}
};
// React 依赖引用相等性进行浅比较优化
// 如果 state 引用不变,组件不会重新渲染
3.7.2 函数式状态管理
// Elm-like 架构
const initState = { todos: [], filter: 'all' };
const update = (model, msg) => {
switch (msg.type) {
case 'ADD':
return { ...model, todos: [...model.todos, msg.todo] };
case 'FILTER':
return { ...model, filter: msg.filter };
default:
return model;
}
};
// 时间旅行调试
const history = [initState];
const dispatch = (msg) => {
const newModel = update(history[history.length - 1], msg);
history.push(newModel);
render(newModel);
};
// 撤销
const undo = () => {
if (history.length > 1) {
history.pop();
render(history[history.length - 1]);
}
};
3.8 业务场景
3.8.1 电商购物车
// 不可变购物车
const Cart = {
create: () => ({ items: [], total: 0 }),
addItem: (cart, product, quantity = 1) => {
const existing = cart.items.find(i => i.productId === product.id);
const newItems = existing
? cart.items.map(i =>
i.productId === product.id
? { ...i, quantity: i.quantity + quantity }
: i
)
: [...cart.items, { productId: product.id, name: product.name, price: product.price, quantity }];
return {
items: newItems,
total: newItems.reduce((sum, i) => sum + i.price * i.quantity, 0),
};
},
removeItem: (cart, productId) => {
const newItems = cart.items.filter(i => i.productId !== productId);
return {
items: newItems,
total: newItems.reduce((sum, i) => sum + i.price * i.quantity, 0),
};
},
};
// 使用
let cart = Cart.create();
cart = Cart.addItem(cart, { id: 1, name: 'Laptop', price: 9999 }, 1);
cart = Cart.addItem(cart, { id: 2, name: 'Mouse', price: 99 }, 2);
// cart 的每个版本都是不可变的,可以保存历史记录
3.8.2 版本控制系统
# 用不可变数据结构模拟 Git 的快照
from pyrsistent import freeze, thaw
class VersionControl:
def __init__(self):
self.history = [freeze({'files': {}, 'message': 'init'})]
def commit(self, files, message):
current = self.history[-1]
new_state = current.set('files', freeze(files)).set('message', message)
self.history.append(new_state)
return new_state
def get_version(self, index):
return self.history[index]
def log(self):
return [(i, s['message']) for i, s in enumerate(self.history)]
# 每个版本都是不可变的快照
vc = VersionControl()
vc.commit({'readme.md': '# Hello'}, 'Initial commit')
vc.commit({'readme.md': '# Hello', 'main.py': 'print("hi")'}, 'Add main.py')
# 可以随时访问任何历史版本
v0 = vc.get_version(0)
v1 = vc.get_version(1)
3.9 注意事项
| 注意事项 | 说明 |
|---|---|
| 不要过度拷贝 | 使用持久化数据结构,而非深拷贝 |
| 注意嵌套层级 | 深层嵌套使用 immer 或 lenses 简化 |
| 性能敏感路径 | 热路径中考虑使用可变数据 |
| 外部库兼容 | 某些库要求可变输入,需要在边界转换 |
| 序列化/反序列化 | 不可变结构可能需要特殊处理 |
| 调试体验 | 持久化数据结构在调试器中可能显示复杂 |
3.10 小结
| 要点 | 说明 |
|---|---|
| 值 vs 身份 | 值不可变,身份随时间关联不同值 |
| 持久化数据结构 | 通过结构共享实现高效不可变更新 |
| Copy-on-Write | 需要修改时才复制数据 |
| 性能权衡 | 结构共享使不可变更新接近 O(1) |
| 工程价值 | 并发安全、时间旅行、变更追踪、缓存安全 |
扩展阅读
- 《Clojure for the Brave and True》 — Daniel Higginbotham,Chapter 10
- Immutable.js 文档
- Understanding Clojure’s Persistent Vectors — Jean Niklas L’orange
- Persistent Data Structures - Wikipedia
- Immer.js 简介 — Michel Weststrate
下一章:04 一等公民函数 — 函数作为值的力量