第 24 章结尾留了个痛点:比较协议写一半比不写更危险,六个全写又是体力活。derive 就是答案——在 class 里写一行 derive equals,编译器替你生成自洽的 __eq__/__ne__;写一行 derive json,序列化反序列化全包。本章把九种派生能力逐一实测,连它力有不逮的边界也画清楚。
第 24 章的协议是"手动挡":每个 __add__、__eq__ 都自己写,灵活但繁琐,还容易写一半留下隐患。本章的 derive 是"自动挡":在 class 体内写一行 derive 能力名,编译器替你生成整套协议实现。它解决三类问题:比较自洽——derive equals 一次生成配对的 __eq__/__ne__,根除第 24 章"只写 __eq__ 导致 a==b 与 a!=b 同时为 true"的陷阱;复制——derive copy/clone 区分浅复制与深复制,一行替代逐字段搬运;持久化——derive value/json/xson 让对象与 JSON/XSON 文本、xvalue 运行时值自由互转,配合 json.parse::<Packet>(text) 的 turbofish 写法完成反序列化。字段级还有 @json("name")、@persist(skip) 等注解微调序列化形态。本章照例全部实测,包括 derive 管不着的边界:手写协议覆盖优先级、null 字段、循环引用、容器字段比较的失效机制。
掌握九种 derive 能力词(copy/clone/value/json/xson/string/hash/equals/moveOnly)各自生成什么;理解 derive equals 如何根除"比较写一半"隐患;分清 copy 浅复制与 clone 深复制对 class 字段的不同处理;会用 json.stringify/parse::<T>/write/read 完成 JSON 持久化 roundtrip;会用 @json/@persist 注解做重命名、跳过、包含与空值省略;理解手写协议覆盖 derive 的优先级;记住 derive 的四条边界(容器/类字段比较失效、null 字段 fatal、循环引用崩溃、native 字段受限)。
derive 能力名 写在 class 体内、字段声明之前(惯例位置),一行一个,可写多行:
class Packet derive copy // 浅复制:copy(p) 可用 derive clone // 深复制:clone(p) 可用 derive value // 与 xvalue 运行时值互转(幕后底座) derive json // JSON 序列化/反序列化 derive xson // XSON 序列化/反序列化 derive string // 默认文本表示:string(p) 可用 derive hash // 哈希:set/dict 的入场券之一 derive equals // 相等比较:自动生成 __eq__ + __ne__ int id = 0 string name = "" end
除这七种常用能力外,实测还接受 derive moveOnly(显式声明"禁止复制",与 copy/clone 互斥)。能力名有编译期校验,写错、写重、写冲突各有专属诊断(以下为真实输出):
derive 与第 24 章协议的关系一句话说清:derive 是协议的生产线——derive equals 生成 __eq__/__ne__,derive string 生成 __toString__,derive value 生成 __toValue__/__fromValue__;生成物与你手写的协议方法地位相同(深入细节 1 有 cgen 证据),且手写版本优先(25.10)。
第 24 章的痛点回顾:只写 __eq__ 时 a != b 不取反 __eq__,回退引用同一性,可能出现 a==b 与 a!=b 同时为 true。derive equals 一行解决——__eq__ 与 __ne__ 成对生成,永远自洽:
class Rec derive equals int id = 0 string name = "" int scratch @compare(skip) = 0 // 不参与比较的字段 create(int id, string name, int scratch) self.id = id self.name = name self.scratch = scratch end end auto a = Rec(7, "alpha", 1) auto b = Rec(7, "alpha", 99) // scratch 不同 auto c = Rec(8, "alpha", 1) println(a == b) // true:scratch 被 @compare(skip) 排除 println(a != b) // false:__ne__ 与 __eq__ 严格互反 println(a == c) // false println(a != c) // true
逐字段比较,@compare(skip) 把缓存、时间戳这类"不影响身份"的字段排除在外。注意 derive equals 只生成 __eq__/__ne__——<、<= 等序比较不在其中,需要排序仍按第 24 章手写 __lt__ 一族。
set<T> 与 dict 键要求元素可哈希、可比较。derive hash + derive equals 组合让你的类获得这项资格:
class Pt derive hash derive equals int x = 0 int y = 0 create(int x, int y) self.x = x self.y = y end end auto p1 = Pt(1, 2) auto p2 = Pt(1, 2) // 与 p1 同值不同引用 auto p3 = Pt(3, 4) set<Pt> s = set{p1, p2, p3} println(s.length) // 2:p1、p2 按值去重 println(p2 in s) // true:按值查找 auto p4 = Pt(9, 9) println(p4 in s) // false
没有 derive 的类实例也能放进 set,但按引用判等——两个 Pt(1, 2) 算两个元素。derive hash/equals 把容器语义从"是不是同一个"变成"是不是同一份数据"。
derive string 生成 __toString__,输出"类名{字段=值}"形态,string() 与 toString() 成员糖都走它:
class Rec derive string int id = 0 string name = "" int scratch @string(skip) = 0 // 不出现在文本里 create(int id, string name, int scratch) self.id = id self.name = name self.scratch = scratch end end auto r = Rec(7, "alpha", 42) println(string(r)) // Rec{id=7, name=alpha} println(r.toString()) // Rec{id=7, name=alpha}:成员糖同路
两个注意:println(r) 依然不走路——第 24 章的盲区对 derive string 同样成立,输出仍是 [class:地址],要文本就显式 string();字符串字段值不带引号(name=alpha 而非 name="alpha"),日志场景够用,需要精确再现时自己写 __toString__(手写优先,见 25.10)。
复制是两种能力,差别只在 class 类型字段的处理:
class Engine derive copy derive clone int power = 0 create(int power) self.power = power end end class Car derive copy derive clone string name = "" Engine engine = Engine(0) create(string name, int power) self.name = name self.engine = Engine(power) end end auto car = Car("beetle", 50) auto twin = copy(car) // 浅复制 twin.engine.power = 300 // engine 字段是共享引用! twin.name = "twin" println(car.engine.power) // 300:原对象被波及 println(car.name) // beetle:string 字段按值复制 auto ghost = clone(car) // 深复制 ghost.engine.power = 400 println(car.engine.power) // 300:clone 的 engine 独立,互不影响
规则一句话:copy 复制一层——标量、string、array 字段按值带走,class 字段只复制引用(两边指同一个对象);clone 递归复制——class 字段也各自生成独立副本。被嵌套的类自身也要有 derive copy/clone,否则编译报错(坑 4)。经验法则:配置快照、存档读档用 clone;"改副本不影响原件,但共享大件"用 copy。
derive value 生成 __toValue__/__fromValue__ 协议,让 class 与运行时的 xvalue(var 世界的值形态)互转。它是幕后能力:直接按名调用会报 unknown class/union field,var v = obj 装箱形态也不因它改变(仍是类引用)——它的用武之地是给 json/xson 序列化和 C ABI 当桥梁:
class Rec derive value // 只写 value,没写 json int id = 0 create(int id) self.id = id end end auto r = Rec(7) println(json.stringify(r)) // {"id":7}:value 足以撑起序列化
实测只声明 derive value 的类就能走 json.stringify——derive json/xson 本质是在 value 之上注册"持久化许可"。日常代码里你几乎不会单独写 derive value,但它解释了为什么 derive json 不需要你手写任何转换逻辑。
最常用的一种派生。stringify 序列化、parse::<T> 反序列化(turbofish 指定目标类型)、write/read 走文件:
class Packet derive json int id = 0 string name = "" float ratio = 0.0 bool active = false create() end end auto p = Packet() p.id = 7 p.name = "hero" p.ratio = 3.5 p.active = true string text = json.stringify(p) println(text) // {"id":7,"name":"hero","ratio":3.5,"active":true} auto q = json.parse::<Packet>(text) // turbofish 指定类型 println(q.name) // hero:roundtrip 完成 println(json.write("packet.json", p, true)) // true:写文件,美化 auto f = json.read::<Packet>("packet.json") println(f.id) // 7
要点:stringify 第二参数 true 换美化输出(tab 缩进);嵌套的 derive json 类字段会递归展开成嵌套对象;反序列化不要求零参 create——只有 create(int) 的类实测也能 parse(字段逐个回填);write/read 返回/接受文件路径,write 返回 bool 表成败。没有 derive 的类走这两个入口会在编译期被拒(坑 1)。
XSON 是 xlang 生态的原生对象表示法,API 与 json 完全对称:
class Packet derive xson int id = 0 string name = "" float ratio = 0.0 create() end end auto p = Packet() p.id = 7 p.name = "hero" p.ratio = 3.5 string text = xson.stringify(p) println(text) // {"id":7,"name":"hero","ratio":3.5} auto q = xson.parse::<Packet>(text) println(q.ratio) // 3.5
纯标量记录的 XSON 文本与 JSON 输出实测完全一致;选型建议:对外交换用 json(通用),程序内部存档可用 xson(与 xvalue 同族,演进同步)。xson.write/read 文件 API 同样存在,用法同 json。
序列化的字段级微调采用字段尾部注解——类型 字段名 @注解(...) = 默认值,注解夹在字段名与默认值之间:
class User derive json int id = 0 string name @json("user_name") = "" // 序列化键改名 string token @persist(skip) = "" // JSON/XSON 都不参与 private int level @json(include) = 0 // private 强制包含 private int secret = 0 // private 默认跳过 create() end func setAll(int lv, int sec, string tk) self.level = lv self.secret = sec self.token = tk end end auto u = User() u.id = 3 u.name = "bob" u.setAll(9, 42, "t0k3n") println(json.stringify(u)) // {"id":3,"user_name":"bob","level":9}
规则汇总(全部实测):@json("名") 改序列化键名(反序列化也认新键);@persist(skip) 同时跳过 JSON 与 XSON,@json(skip)/@xson(skip) 只管单家;private 字段默认不参与,@json(include) 强制纳入;@json(omit=empty) 让空值字段不输出(实测空字符串被省略)。一个 RC0 空白:文档列出 required(反序列化必填),实测缺失该字段时不报错,静默回填零值——别拿它当校验用。另有 @compare(skip)/@string(skip) 分别服务 derive equals(25.2)与 derive string(25.4)。
四条实测边界,逐条记牢:
[sem] derive copy/clone 的字段类型必须可复制;写 derive value/json/xson 报 [sem] derive value/json/xson 字段必须是基础类型、动态 var/容器,或同样可持久化的 class。资源型类用 derive moveOnly 表明身份。另有一条"无消息陷阱"值得单独警惕:没写 derive copy/clone 的类调用 copy()/clone(),编译通过、运行静默 fatal(退出码 4,无任何输出)——比编译报错危险得多(坑 2)。
把七种 derive 装进一个存档类,一次看全它们的分工:
class Equip derive copy derive clone derive json derive string int atk = 0 create(int atk) self.atk = atk end end class Save derive copy derive clone derive json derive xson derive string int id = 0 string player @json("player_name") = "" Equip weapon = Equip(0) int cache @persist(skip) @string(skip) = 0 create(int id, string player, int atk) self.id = id self.player = player self.weapon = Equip(atk) end end auto s1 = Save(1, "hero", 50) auto s2 = copy(s1) s2.weapon.atk = 999 println(s1.weapon.atk) // 999:浅复制共享 weapon println(string(s1)) // Save{id=1, player=hero, weapon=[table:...]} string text = json.stringify(s1) println(text) // {"id":1,"player_name":"hero","weapon":{"atk":999}} auto back = json.parse::<Save>(text) println(back.player) // hero println(back.weapon.atk) // 999:嵌套对象递归还原 auto xb = xson.parse::<Save>(xson.stringify(s1)) println(xb.id) // 1:XSON 同路
读输出学三件事:cache 被 @persist(skip) 与 @string(skip) 双重隐身;derive string 遇到 class 字段输出 [table:地址](不递归调 weapon 的 __toString__,想美化就自己写);Save 没写 derive equals——因为 weapon 是 class 字段,写了也比较不出真值(25.10 边界 2),这正是"知道何时不 derive"的示范。
对 25.2 的 Rec 跑 xl cgen,derive equals 生成的是两个普通 C 函数:
命名规则与第 24 章手写协议完全一致——derive 产物就是"编译器代写的协议方法",无特殊运行时。函数体揭示了 25.10 边界 2 的机制:每个字段的比较是把两侧字段各自转成 xvalue、再比较两者的字符串形态(xvoToString + memcmp)。标量与 string 的字符串形态即值本身,比较可靠;而 class/数组字段转换出的字符串形态含地址([table:0000000000XX…]),每次转换都是新对象、新地址——于是比较恒不相等,连同引用自比。这不是语义设计,是 RC0 实现路径的副作用,未来若改为逐字段递归调 __eq__,结论会更新。
"手写覆盖 derive"(25.10 边界 1)初看是隐患——手滑写了个永远 false 的 __eq__ 就把 derive equals 废了。但它恰恰是合理设计:derive 的定位是可降级的默认值,不是铁律。典型用法是"先 derive 快速跑通,某个字段需要特殊比较(比如浮点容差)时,手写对应协议接管";与第 24 章 ~= 分工同理。团队协作时值得立一条约定:derive 与手写同族协议共存必须留注释说明原因,否则 Review 时删掉手写版——它的存在通常意味着有人忘了 derive 已经在了。
@json 注解体系里最值得品味的一条默认规则:private 字段默认不参与序列化。它把"访问控制"与"持久化边界"对齐——你标记 private 的字段通常是实现细节(缓存、内部句柄、校验和),本就不该写进对外文本;要破例必须显式 @json(include),把"泄露实现细节"变成一个看得见的决定。对照第 21 章 private 的"查无此名"报错哲学:xlang 对封装的态度是一以贯之的——默认收紧,破例留痕。required 注解实测未生效(25.9)也提醒一件事:注解是声明意图的地方,但意图是否兑现要以实测为准,RC0 阶段尤其如此。
以下报错全部来自真实编译器输出(文件路径已缩短)。
原因:序列化是"许可制",class 必须显式声明 derive value/json/xson 之一;json.parse::<T> 对目标类型有同款检查([sem] json/xson 持久化目标 class 必须声明对应的 derive value/json/xson)。正确做法:给类补 derive json(或 xson)。
原因:copy()/clone() 对无对应 derive 的类在 RC0 编译放行、运行静默 fatal——比坑 1 的编译期报错隐蔽得多。正确做法:复制前确认类有 derive copy/clone;排查"程序无声消失"时优先怀疑这类运行期 fatal。
原因:derive json 无法处理 null 的 class 字段;若是双向互相引用(循环),进程直接崩溃(退出码 127)。正确做法:序列化前把可空字段填上占位实例,或改用 var 字段承载;对象图保持树形无环。
原因:derive copy/clone 会检查每个字段——native 句柄、无复制能力的 class 字段都会让派生失败;derive value/json/xson 对字段也有同类限制。正确做法:字段类补齐 derive copy/clone/json;native 资源类改 derive moveOnly,复制语义自己实现。
原因:derive 的能力词是封闭集合(copy/clone/value/json/xson/string/hash/equals/moveOnly),拼错、重复、互斥同写都被编译期拦下。正确做法:对照 25.1 总览表核对拼写;moveOnly 与 copy/clone 二选一。
| derive | 生成物 | 要点 |
|---|---|---|
| equals | __eq__ + __ne__(成对自洽) | @compare(skip) 排除字段;只适用于标量/string 字段类 |
| hash | 哈希协议 | 与 equals 搭档,set<T>/dict 键的值语义 |
| string | __toString__(类名{字段=值}) | @string(skip);println 仍不走;手写优先 |
| copy | __copy__(浅复制) | class 字段共享引用;标量/string/array 按值 |
| clone | __clone__(深复制) | class 字段递归独立;字段类须有对应 derive |
| value | __toValue__/__fromValue__ | 幕后底座;单独声明即可 json.stringify |
| json | JSON 序列化/反序列化 | stringify/parse::<T>/write/read;美化参数 true |
| xson | XSON 序列化/反序列化 | API 与 json 对称;内部存档可用 |
| moveOnly | 禁止复制的声明 | 与 copy/clone 互斥;资源型类的身份证 |
| 注解 | @json/@xson/@persist | 改名/skip/include/omit=empty;private 默认跳过;required 未生效 |
| 边界 | 手写覆盖 derive | null 字段 fatal;循环崩溃;容器字段比较失效;native 受限 |
编写 config.xl:class Config 有 env、workers、secretKey 三个字段,derive json;env 序列化键改名 environment,secretKey 用 @persist(skip) 跳过。打印美化 JSON,再 parse 回来验证 env/workers 还原。
参考思路:@json("environment") 写字段名后;json.stringify(c, true) 美化;输出应只有 environment 与 workers 两个键(实测 secretKey 成功隐身)。
编写 points.xl:class Point(x, y) 加 derive hash/equals,把 Point(1,2)、Point(1,2)、Point(3,4) 放进 set<Point>,验证 length 为 2、p2 in s 为 true。再写一个 Odd 类:derive equals 同时手写永远返回 false 的 __eq__,验证 o1 == o2 输出 false——亲身体会"手写覆盖 derive"。
参考思路:set 去重依赖 hash+equals 双 derive 缺一不可;Odd 实验印证 25.10 边界 1——derive 是可降级的默认实现。
编写 cars.xl:Engine(power) 与 Car(name, engine) 都 derive copy/clone。对同一辆 Car 分别 copy 和 clone,修改两个副本的 engine.power 与 name,打印原对象验证:copy 的副本改 engine 波及原对象、改 name 不波及;clone 的副本什么都不波及。
参考思路:预期输出 300 / beetle / 300——浅复制里"浅"只作用于 class 引用字段;被嵌套的 Engine 也必须带 derive copy/clone,否则撞坑 4。