可变性、身份与副作用
理解引用、拷贝和原地修改,避免把 JavaScript 对象直觉带进其他运行时。
可变性、身份与副作用
JavaScript 对象通过引用共享,Python 也有可变 list 和 dict,C++ 则把值、引用和指针的区别暴露在类型里。数据是否被原地修改,会影响缓存、并发和测试。迁移时应先写出所有权:调用方是否仍持有旧值,函数是否借用对象,返回结果是否拥有独立存储。没有这个约定,语法看似正确,实际可能让模型特征、机器人状态或缓存被悄悄改写。
学习目标
- 能区分变量绑定、对象身份和内容修改,判断函数是否会影响调用方状态。
- 能通过不可变快照或复制边界减少隐藏副作用,并识别复制的成本。
- 能用输入未改变、输出正确和失败无副作用的测试验证约定。
同一个变量名不代表同一种所有权
下面这段代码只保留同一个意图,重点观察输入边界、数据流和失败语义,而不是逐字符翻译。
function addTag(item: Item, tag: string) {
item.tags.push(tag);
return item;
} # Python
def add_tag(item, tag):
item["tags"].append(tag)
return item
// C++
void add_tag(Item& item, std::string tag) {
item.tags.push_back(std::move(tag));
} 用不可变快照隔离阶段
如果事件要被多个消费者读取,优先创建新对象或冻结内部模型;如果确实要原地更新,限定在一个拥有者内部,并让函数名/文档说明副作用。C++ 的 const Item& 表示只读借用,按值参数表示复制或移动;Python 可以用 dataclass(frozen=True),JS/TS 可以使用结构化复制或只读类型,但运行时仍要防止嵌套对象被改写。
function withTag(item: Item, tag: string): Item {
return { ...item, tags: [...item.tags, tag] };
}
const before = { id: "a1", tags: ["raw"] };
const after = withTag(before, "validated");
副作用如何影响工程
原地修改可能减少复制,适合明确拥有缓冲区的高频 C++ 算法;但跨线程共享同一可变对象会引入锁和竞态。Python/JS 的闭包若捕获可变列表,异步回调执行顺序会改变结果。AI 批处理可以在边界把输入转成只读批次,机器人控制状态则应区分“最新快照”和“待处理命令”,不要让消费者反向修改生产者数据。
常见错误与排错思路
最常见错误是浅拷贝:只复制外层对象,嵌套 tags 或 metadata 仍和原对象共享。排错时比较修改前后两个引用/地址,给嵌套字段打印身份,并写连续调用测试;C++ 若出现 double free 或悬空指针,检查裸指针是否表达借用还是所有权,优先改成 RAII 容器。若结果依赖测试顺序,先消除共享全局状态,再考虑加锁。
对比原地修改与不可变结果
列表/数组变量指向对象,不代表赋值就创建了独立副本。若函数修改调用方持有的状态,测试顺序、缓存和并发行为都会受影响。下面的变换保留原数据并显式生成新快照:
function withStatus(job, status) {
return { ...job, status };
}
const original = { id: "j1", status: "queued" };
const updated = withStatus(original, "ready");
console.log(original.status, updated.status, original === updated);
预期是 queued ready false:原对象未变,新对象身份不同。对象展开只做浅拷贝;若嵌套字段也会变更,需复制被修改的路径或使用不可变数据结构,而不是误以为深层都已隔离。对大型图像/张量,复制可能很贵,应使用只读视图或明确的所有权转移。
运行验证:副作用与并发边界
测试至少断言输入快照没有变化、输出字段满足预期、校验失败时没有部分修改。可在函数前后冻结/比较测试数据,或复制一份基线做差异比较。跨线程共享状态时,即使每个写操作看起来简单,也要把整个业务不变量作为原子更新单元;更简单的设计通常是消息传递和单一写入者。
对嵌套对象分别检查顶层与子对象身份,可以暴露浅拷贝残留的共享引用。若更新失败必须保持原值,先把所有校验放在提交修改之前,或构建完整的新快照后一次替换,避免中途异常留下半更新状态。
排查“某处改了缓存”的问题时,先记录对象身份和变更时间,再沿所有写入点追踪;不要只搜变量名,因为多个别名可能指向同一对象。将状态更新收拢到单个函数,能让调用者看清是否发生原地修改。
迁移练习
请完成:设计一个给事件增加标签的函数,并决定它是原地修改还是返回新对象;列出两个消费者同时拿到该事件时,哪种选择更容易保证结果一致。
可变性、身份与副作用练习
设计一个给事件增加标签的函数,并决定它是原地修改还是返回新对象。
给我一点提示
先列出调用方是否还需要旧对象,再决定 API 的契约,并检查嵌套数组是否仍共享。
查看参考答案
事件要广播给多个消费者时返回新对象,并复制 tags;单一拥有者的热路径可以原地修改,但接口要标注副作用并禁止其他线程同时借用。测试同时断言旧事件不变、新事件有标签。 本节结论
跨语言代码最容易出错的地方不是括号,而是对象身份和修改权限。把所有权写进接口,才能在性能和可预测性之间做出有依据的选择。
小结
可变性关乎谁能改动共享对象,不只是 const 或变量名。通过显式复制/借用契约、不可变快照和副作用测试,可以让缓存、AI 特征与设备状态更容易推理。
阶段共 8 节课,按顺序完成更容易建立完整的迁移模型。