"b = a 之后改 b,a 会变吗?"——答案取决于 a 是 int、class 还是容器。xlang 的复制体系分三层:值类型按位拷贝、class 永远共享引用、容器共享 backing 直到你显式 copy/clone。本章把第 12 章参数传递、第 25 章 derive、第 26 章 union 三条线收拢成一张完整地图,卷五收官。
前面六章埋下了三条关于"复制"的线索:第 12 章讲参数传递时说"byval 对 class 复制的是句柄不是对象";第 25 章讲 derive 时区分了 copy 浅复制与 clone 深复制;第 26 章讲 union 时看到值语义的实现就是 memcpy。本章把它们拼成完整图景,回答四个问题:默认什么时候复制?(几乎从不——赋值、传参、返回、闭包、进容器都只搬引用);想复制时怎么复制?(显式 copy()/clone(),且类型必须事先用 derive 或手写协议开放能力);浅与深差在哪?(只差在 class 引用字段/元素——标量、string、容器字段在浅复制里也按值走);哪些坑会无声致命?(无 derive 调 copy 静默 fatal、循环引用 clone 直接崩溃)。读完本章,"b = a 之后改 b,a 会变吗"这类问题你将有确定性的答案。
掌握三层复制模型(值类型按位/class 引用共享/容器共享 backing);理解"默认不复制"原则及其在赋值、传参、byval 中的一致性;分清 copy() 浅复制与 clone() 深复制对 class 字段的不同处理;会手写 __copy__/__clone__ 做定制复制(脱敏、重置);掌握容器复制的动静不对称(var 容器 clone 克隆 class 元素、typed 容器不克隆);记住两条危险边界(无 derive 静默 fatal、循环引用崩溃)及防御写法;能按决策表在赋值/copy/clone/byref 间正确选型。
| 层 | 成员 | b = a 的语义 | 独立副本怎么来 |
|---|---|---|---|
| 值类型 | int/float/bool/string/time/point/union | 按位复制,天然独立 | 赋值即是;copy() 原样返回 |
| class | 普通/object 派生/native-backed | 共享同一受管理引用 | copy()/clone()(须 derive 或手写协议) |
| 容器 | array/list/set/dict(typed 与 var) | 共享同一 backing | copy()/clone() 建独立容器 |
三层的共同点:复制从不隐式发生两次——值类型一次按位复制就够了,class 和容器则永远先共享、想要独立再显式开口。这个"默认共享、显式复制"的原则是本章所有规则的源头。
把第 12 章的结论放进复制框架再看一遍:赋值、默认参数、byval、返回、闭包捕获、容器存储,全都不调用任何复制协议——它们只让两个名字指向同一个对象:
class Item derive copy int v = 0 create(int v) self.v = v end end func touch(byval Item x) x.v = 99 // byval 也不是复制!改的是同一个对象 end auto a = Item(1) auto b = a // 共享 touch(a) println(b.v) // 99:赋值共享 + byval 共享,两处都看得见
byval 复制的只是"句柄"(参数变量本身),被调函数里给参数重新赋值不影响调用方,但通过句柄改成员双方都可见。要真正的独立对象,只有一个入口:copy()/clone()——而且类型得先开放这个能力。
copy(obj) 要求类声明 derive copy 或手写 __copy__,返回一个"一层新"的对象:
class Config derive copy derive clone int level = 0 create(int level) self.level = level end end class App derive copy derive clone string name = "" Config cfg = Config(0) create(string name, int level) self.name = name self.cfg = Config(level) end end auto app = App("demo", 1) auto twin = copy(app) twin.cfg.level = 9 // cfg 是共享引用 twin.name = "twin" // name 是按值带走的 println(app.cfg.level) // 9:改副本的 class 字段,原件遭殃 println(app.name) // demo:string 字段早已各走各路
"浅"的精确含义:标量、string、array 字段按值复制,class 类型字段只复制引用。副本与原对象因此"半独立"——顶层字段互不干扰,深层对象藕断丝连。第 25 章已见过这个模型,本章的增量是:它同样适用于容器(27.6),且有手写定制的入口(27.5)。
clone(obj) 递归到底——class 字段也各自生成独立副本:
auto ghost = clone(app) ghost.cfg.level = 5 println(app.cfg.level) // 9:clone 的 cfg 是独立副本,互不波及 println(ghost.name) // demo:顶层字段照旧按值
三条规则:嵌套的类自己也必须开放 clone 能力(derive clone 或手写 __clone__),否则编译报"字段类型必须可复制";深复制不会中途退化为浅复制(XRT 为 copy/clone 提供独立操作槽);clone 的对象图必须无环——循环引用会让进程崩溃(27.9)。
复制协议可以完全自己写——签名固定,返回新实例;与 derive 共存时手写优先:
class Session derive copy // derive 在场,但下面的手写版胜出 int token = 0 create(int token) self.token = token end func __copy__() as Session auto fresh = Session(0) // 复制即脱敏:token 不带过去 return fresh end end auto s = Session(99) auto guest = copy(s) println(s.token) // 99 println(guest.token) // 0:手写 __copy__ 的定制逻辑
手写协议的价值场景:复制时脱敏(凭证、缓存不带走)、复制时重置(统计清零、时间戳刷新)、复制时换算(官方测试用例里 __copy__ 给 value +10、__clone__ +20,用来区分两条路径)。与第 24 章协议家族同一规则:协议就是普通方法,只是名字被预定。手写版与 derive 同在一个类时,实测手写生效(derive 产物被覆盖)——与第 25 章 __eq__/__toString__ 的优先级结论一致。
容器赋值共享 backing——没有写时复制(见深入细节 2 的考证);copy()/clone() 才建独立容器。差异在"容器里装 class"时显现,而且 typed 与 var 两个世界不对称:
var items = [Item(1), Item(2)] var shared = items shared[0].v = 99 println(items[0].v) // 99:赋值共享 backing var shallow = copy(items) shallow[0].v = 55 println(items[0].v) // 55:copy 复制容器壳,class 元素仍共享 var deep = clone(items) deep[0].v = 11 println(items[0].v) // 55:clone 连元素一起克隆,彻底独立 println(deep[0].v) // 11
实测汇总四条:var 容器——copy 递归复制嵌套容器(三层实测均独立)但 class 元素保持共享引用;clone 连 class 元素也调 __clone__,完全独立。typed 容器——copy/clone 都只复制容器壳,class 元素在 clone 里也不克隆(array<Item> 实测 clone 后改元素仍波及原数组),要深复制 typed 容器得手动遍历逐个 clone。嵌套 typed 容器读出即复制——array<array<int>> 的 m[0] 取出的是内层副本,改它影响不到 m;链式 m[0][0] 读写直接编译报错(坑 5)。标量元素容器(array<int> 等)——壳独立即全独立,copy/clone 效果相同。
值类型不需要复制协议——copy()/clone() 对它们就是原样返回:
println(copy(42)) // 42 println(clone("hello")) // hello var v = 7 println(copy(v)) // 7 union Bits int i float64 f end Bits b b.i = 99 Bits b2 = copy(b) b.i = 1 println(b2.i) // 99:union 复制=memcpy,天然独立
union 的复制就是第 26 章那块 memcpy——不需要 derive、不需要协议,和 int 一样是"复制免费户"。string 看似引用类型,但不可变,按值传递从不出事。
第 25 章见过 derive moveOnly,本章给它的语义画上句号:xlang 没有用户级 move() 函数——写 move(a) 报 [sem] undeclared identifier 'move'。"移动"发生在两个地方:
所以"xlang 有移动语义吗"的准确回答是:有,但只在返回值的边界上自动发生;你能控制的只有"禁止复制"(moveOnly)与"显式复制"(copy/clone)两个开关。
边界一:无能力复制 = 静默 fatal。对没写 derive copy/clone(或手写协议)的类调 copy()/clone(),编译期放行,运行期进程直接退出(退出码 4,零输出)。防御写法有两层:写库时——class 一定义就决定复制能力(可复制就 derive,资源型就 moveOnly),把决定留在类型旁边;用时——复制别人的类之前先翻它的声明,"这个类 derive 了什么"应该成为肌肉记忆。这也是 review 清单的好条目:搜索 copy( 与 clone( 调用点,逐一核对目标类型。
边界二:循环引用 clone = 崩溃。a.next = b、b.next = a 之后 clone(a),进程崩溃(退出码 127,与第 25 章 json 循环序列化同病)。规避:clone 前把对象图树形化(断开回边);必须用图结构时放弃 clone,手写"按 id 重建"的复制逻辑。这两条边界的共同教训:复制能力是把对象图当树来处理的——环与资源都不在它的合同里。
| 你的处境 | 选择 | 理由 |
|---|---|---|
| 只想多个名字指同一对象 | 直接赋值 / byval | 零成本,默认即共享 |
| 要让函数改我的变量本体 | byref / out | 绑定变量槽,调用点显式标记 |
| 顶层独立、深层共享就行 | copy() | 配置副本、轻量分叉 |
| 完全独立的对象树 | clone() | 快照、存档;前提是无环 |
| 复制时要脱敏/重置字段 | 手写 __copy__/__clone__ | 定制逻辑,优先于 derive |
| typed 容器装 class 要深复制 | 遍历逐个 clone 重建 | clone 对 typed 容器元素不递归 |
| 资源型对象(native 句柄) | derive moveOnly,别复制 | 复制即双 Owner,fatal 等着你 |
口诀:共享是默认,复制要开口,深浅看字段,环资源莫入。
把本章的决策串起来:一个文档系统,页面是 var dict(文档对象 + 历史数组),存档要完全独立的快照,外发要脱敏副本:
class Doc derive copy derive clone string title = "" create(string title) self.title = title end end var page = {"doc": Doc("v1"), "history": [Doc("h1")]} var snap = clone(page) // 存档:连 class 元素一起克隆 snap["doc"].title = "v2" snap["history"][0].title = "h2" println(page["doc"].title) // v1:原件不动 println(page["history"][0].title) // h1:嵌套元素也不动 println(snap["doc"].title) // v2:快照自有一方天地
注意这个程序依赖的是 var 容器的 clone 行为(27.6:元素递归克隆)——若把 page 换成 typed 容器,快照就不会这么彻底,得手动遍历重建。选容器类型时,"未来要不要整体深复制"是一个真实的考量因素。
对 27.5 的 Session 跑 xl cgen,copy(s) 的编译产物分两步:
设计文档补完另一半:协议返回的是受管理引用,XRT 把结果的 payload 誊进新 record——所以 __copy__ 里 return self 也不会制造悬空句柄(xvoGetRecordOwner 找回外壳)。这套"协议生产 + 运行时落盘"的两段式,解释了为什么复制协议签名被锁死为"零参返回同类"(27.5):编译器要拿返回值做二次加工,形态不能自由。
功能清单写着"动态 var 容器采用 XRT 写时复制:赋值时共享 backing,首次写入时分离"。本章用 run 与 build 两种模式实测:var b = a 之后 b[0] = 99,a[0] 也是 99;dict 同;b.append(3) 之后 a.length 也是 3——索引写与 append 都不分离,COW 在 RC0 并未生效。XRT 底层确实保留了 xvoEnsureUnique 设施,但 xlang 侧没有接线。写作本书以实测为准:今天的正确心智模型是"var 容器赋值即共享,与 typed 容器无异"。若未来版本补上 COW,27.6 的第一条规则需要修订——这也提醒读者:对"共享还是复制"这类基础语义,拿不准时写三行代码验证,比查文档可靠。
第 21~27 章把 xlang 的对象体系讲完了,回望这张地图:21 章立起骨架(字段、方法、生命周期、引用语义);22 章加血缘(单继承、virtual 多态);23 章换外观(属性=换名方法对);24 章接神经(协议=运算符与语句的挂钩);25 章上自动档(derive=协议生产线);26 章开两翼(object 加弹性、union 减抽象);27 章收复制(默认共享、显式 copy/clone)。一条主线贯穿始终:class 是唯一对象模型,其余一切都是它的协议、派生或布局变体。卷六将进入并发世界(线程、channel、async),对象们要开始跨线程旅行了——"共享是默认"这条原则,在那时会有新的分量。
以下报错全部来自真实编译器输出(文件路径已缩短)。
原因:复制是"许可制"但 RC0 在编译期不查,运行期静默 fatal。正确做法:给类补 derive copy/clone 或手写协议;排查"程序无声消失"优先怀疑此类 fatal(第 25 章坑 2 同族)。
原因:byval 复制的是参数句柄(重新绑定不可见),不是对象本体(成员修改可见)。正确做法:函数不该碰原对象时,调用方先 copy()/clone() 再传,或函数内先复制再改。
原因:var 与 typed 容器赋值都共享 backing——文档里的写时复制 RC0 未生效(run/build 双模式实测)。正确做法:要独立容器显式 copy()/clone();标量元素两者等效,装 class 时注意 27.6 的动静差异。
原因:typed 容器的 copy/clone 只复制容器壳,不递归调元素的 __clone__(var 容器的 clone 才会)。正确做法:手动遍历建新容器逐个 clone;或整体改用 var 容器。
原因:typed 嵌套容器不支持 m[0][0] 链式索引;且 m[0] 读出的是内层副本,赋中间变量再改也影响不到 m。正确做法:需要真二维结构用 class 封装行(第 24 章 Matrix 的 __item__(x, y) 双参数形态),或改用 var 嵌套容器。
| 主题 | 规则 | 要点 |
|---|---|---|
| 三层模型 | 值按位/class 共享/容器共享 backing | 默认不复制;复制从不隐式发生 |
| 默认路径 | 赋值/传参/byval/返回/闭包/存储 | 只搬句柄;成员修改双方可见 |
| copy() | 浅复制:顶层按值、class 字段共享 | 须 derive copy 或手写 __copy__ |
| clone() | 深复制:class 字段递归克隆 | 嵌套类须有 clone 能力;对象图须无环 |
| 手写协议 | __copy__/__clone__ 零参返回同类 | 手写优先于 derive;脱敏/重置场景 |
| var 容器 | copy 复制嵌套容器;clone 连 class 元素也克隆 | 赋值共享,COW 未生效(实测) |
| typed 容器 | copy/clone 只复制壳 | class 元素 clone 也不克隆;嵌套读出即副本 |
| 标量/union | copy() 原样返回 | union 复制=memcpy,复制免费户 |
| move | 无 move() 函数;返回值自动移动 | moveOnly=禁止复制的声明 |
| 危险边界 | 无能力复制静默 fatal;循环 clone 崩溃 | 防御:类型旁决定能力 + review 调用点 |
编写 session.xl:class Session 带 token 字段,derive copy 同时手写 __copy__——返回一个 token 归零的新实例。验证 copy 出的"访客副本"拿不到原 token,而原对象不受影响。
参考思路:预期输出 99 与 0——手写协议优先于 derive 是解题钥匙(27.5);这也是"复制时该带走什么"的安全设计范式。
编写 apps.xl:Config(level) 与 App(name, cfg) 都 derive copy/clone。对同一 App 分别 copy 和 clone,改两个副本的 cfg.level 与 name,打印原对象验证:copy 副本改 cfg 波及原件、改 name 不波及;clone 副本什么都不波及。
参考思路:预期输出 9 / demo / 9——与 27.3、27.4 的示例同构;亲手跑一遍,"浅差在 class 字段"就长在记忆里了。
编写 page.xl:Doc(title) derive copy/clone;var page = {"doc": Doc("v1"), "history": [Doc("h1")]}。用 clone 做快照,改快照的 doc.title 和 history[0].title,验证原件纹丝不动;再用 copy 做一次,指出哪一层漏了。
参考思路:clone 版输出 v1 / h1 / v2(27.6 var 容器规则);copy 版改 doc.title 会波及原件(元素共享)——两层答案写进注释,就是现成的决策表应用。