第 25 章 / 共 58 章 卷五 OOP

derive 派生

第 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 字段受限)。

25.1 derive 总览

derive 能力名 写在 class 体内、字段声明之前(惯例位置),一行一个,可写多行:

overview.xl
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 unknownAbility bad.xl:2:12: [sem] 未知 derive 能力 # derive copy 写两行 bad.xl:3:12: [sem] 重复 derive 能力声明 # derive copy 与 derive moveOnly 同写 bad.xl:3:12: [sem] derive moveOnly 不能与 derive copy 或 derive clone 同时使用

derive 与第 24 章协议的关系一句话说清:derive 是协议的生产线——derive equals 生成 __eq__/__ne__,derive string 生成 __toString__,derive value 生成 __toValue__/__fromValue__;生成物与你手写的协议方法地位相同(深入细节 1 有 cgen 证据),且手写版本优先(25.10)。

25.2 derive equals:比较族补全

第 24 章的痛点回顾:只写 __eq__ 时 a != b 不取反 __eq__,回退引用同一性,可能出现 a==b 与 a!=b 同时为 true。derive equals 一行解决——__eq__ 与 __ne__ 成对生成,永远自洽:

equals.xl
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__ 一族。

25.3 derive hash:容器的值语义入场券

set<T> 与 dict 键要求元素可哈希、可比较。derive hash + derive equals 组合让你的类获得这项资格:

hash.xl
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 把容器语义从"是不是同一个"变成"是不是同一份数据"。

25.4 derive string:默认文本表示

derive string 生成 __toString__,输出"类名{字段=值}"形态,string() 与 toString() 成员糖都走它:

string.xl
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)。

25.5 derive copy 与 clone:浅与深

复制是两种能力,差别只在 class 类型字段的处理:

copyclone.xl
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。

25.6 derive value:xvalue 互转底座

derive value 生成 __toValue__/__fromValue__ 协议,让 class 与运行时的 xvalue(var 世界的值形态)互转。它是幕后能力:直接按名调用会报 unknown class/union field,var v = obj 装箱形态也不因它改变(仍是类引用)——它的用武之地是给 json/xson 序列化和 C ABI 当桥梁:

value.xl
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 不需要你手写任何转换逻辑。

25.7 derive json:JSON 持久化

最常用的一种派生。stringify 序列化、parse::<T> 反序列化(turbofish 指定目标类型)、write/read 走文件:

json.xl
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)。

25.8 derive xson:XSON 持久化

XSON 是 xlang 生态的原生对象表示法,API 与 json 完全对称:

xson.xl
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。

25.9 字段注解:@json / @xson / @persist

序列化的字段级微调采用字段尾部注解——类型 字段名 @注解(...) = 默认值,注解夹在字段名与默认值之间:

anno.xl
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)。

25.10 derive 的边界

四条实测边界,逐条记牢:

  • 手写协议覆盖 derive——类里同时有 derive equals 和手写 __eq__(哪怕永远 return false),== 走手写版;derive string 与手写 __toString__ 同理。derive 是"默认实现",你写的永远最后说了算。
  • derive equals/hash 只对标量字段可靠——字段含 class 或 array 等容器时,比较结果恒为 false(连 c = a 后 a == c 都 false)。机制见深入细节 1;实务规则:要给容器当元素,类里只放标量与 string 字段。
  • null 的 class 字段不能序列化——derive json 的类字段值为 null 时 stringify 静默 fatal(退出码 4,无消息);两个对象互相引用(循环)则进程直接崩溃(退出码 127)。序列化前保证引用字段非空、对象图无环。
  • native 字段是禁区——含 native handle 字段的类写 derive copy 报 [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 装进一个存档类,一次看全它们的分工:

save.xl
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"的示范。

深入细节

拾遗 1:derive 生成了什么——cgen 证据与比较机制

对 25.2 的 Rec 跑 xl cgen,derive equals 生成的是两个普通 C 函数:

bool xl_class_Rec______eq____(XL_Class_Rec* self, XL_Class_Rec other); bool xl_class_Rec______ne____(XL_Class_Rec* self, XL_Class_Rec other); // a == b → 直接调用 xl_class_Rec______eq____(a, *b)

命名规则与第 24 章手写协议完全一致——derive 产物就是"编译器代写的协议方法",无特殊运行时。函数体揭示了 25.10 边界 2 的机制:每个字段的比较是把两侧字段各自转成 xvalue、再比较两者的字符串形态(xvoToString + memcmp)。标量与 string 的字符串形态即值本身,比较可靠;而 class/数组字段转换出的字符串形态含地址([table:0000000000XX…]),每次转换都是新对象、新地址——于是比较恒不相等,连同引用自比。这不是语义设计,是 RC0 实现路径的副作用,未来若改为逐字段递归调 __eq__,结论会更新。

拾遗 2:derive 与手写协议的优先级设计

"手写覆盖 derive"(25.10 边界 1)初看是隐患——手滑写了个永远 false 的 __eq__ 就把 derive equals 废了。但它恰恰是合理设计:derive 的定位是可降级的默认值,不是铁律。典型用法是"先 derive 快速跑通,某个字段需要特殊比较(比如浮点容差)时,手写对应协议接管";与第 24 章 ~= 分工同理。团队协作时值得立一条约定:derive 与手写同族协议共存必须留注释说明原因,否则 Review 时删掉手写版——它的存在通常意味着有人忘了 derive 已经在了。

拾遗 3:持久化的默认哲学——private 不出门

@json 注解体系里最值得品味的一条默认规则:private 字段默认不参与序列化。它把"访问控制"与"持久化边界"对齐——你标记 private 的字段通常是实现细节(缓存、内部句柄、校验和),本就不该写进对外文本;要破例必须显式 @json(include),把"泄露实现细节"变成一个看得见的决定。对照第 21 章 private 的"查无此名"报错哲学:xlang 对封装的态度是一以贯之的——默认收紧,破例留痕。required 注解实测未生效(25.9)也提醒一件事:注解是声明意图的地方,但意图是否兑现要以实测为准,RC0 阶段尤其如此。

⚠️ 常见错误

以下报错全部来自真实编译器输出(文件路径已缩短)。

坑 1:没写 derive 就序列化
# bad.xl 内容:class Plain 无任何 derive # auto text = json.stringify(p)(第 6 行) > xl run bad.xl bad.xl:6:28: [sem] json/xson 持久化 class 实参必须声明对应的 derive value/json/xson

原因:序列化是"许可制",class 必须显式声明 derive value/json/xson 之一;json.parse::<T> 对目标类型有同款检查([sem] json/xson 持久化目标 class 必须声明对应的 derive value/json/xson)。正确做法:给类补 derive json(或 xson)。

坑 2:没写 derive 就 copy()——静默 fatal
# bad.xl 内容:class Plain 无 derive;auto c = copy(p) > xl run bad.xl (无任何输出,进程退出码 4)

原因:copy()/clone() 对无对应 derive 的类在 RC0 编译放行、运行静默 fatal——比坑 1 的编译期报错隐蔽得多。正确做法:复制前确认类有 derive copy/clone;排查"程序无声消失"时优先怀疑这类运行期 fatal。

坑 3:序列化含 null 的类字段
# bad.xl 内容:class Node derive json;Node next = null # println(json.stringify(a)) > xl run bad.xl (无任何输出,进程退出码 4)

原因:derive json 无法处理 null 的 class 字段;若是双向互相引用(循环),进程直接崩溃(退出码 127)。正确做法:序列化前把可空字段填上占位实例,或改用 var 字段承载;对象图保持树形无环。

坑 4:derive copy 的类里藏着不可复制字段
# bad.xl 内容:class BadAutoCopy derive copy,字段是 native 句柄类 > xl run bad.xl bad.xl:10:12: [sem] derive copy/clone 的字段类型必须可复制

原因:derive copy/clone 会检查每个字段——native 句柄、无复制能力的 class 字段都会让派生失败;derive value/json/xson 对字段也有同类限制。正确做法:字段类补齐 derive copy/clone/json;native 资源类改 derive moveOnly,复制语义自己实现。

坑 5:能力名写错、写重或写冲突
> xl run bad.xl bad.xl:2:12: [sem] 未知 derive 能力 bad.xl:3:12: [sem] 重复 derive 能力声明 bad.xl:3:12: [sem] derive moveOnly 不能与 derive copy 或 derive clone 同时使用

原因: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
jsonJSON 序列化/反序列化stringify/parse::<T>/write/read;美化参数 true
xsonXSON 序列化/反序列化API 与 json 对称;内部存档可用
moveOnly禁止复制的声明与 copy/clone 互斥;资源型类的身份证
注解@json/@xson/@persist改名/skip/include/omit=empty;private 默认跳过;required 未生效
边界手写覆盖 derivenull 字段 fatal;循环崩溃;容器字段比较失效;native 受限

复习题

✍️ 先遮住答案想一想
  1. derive equals 相对手写 __eq__ 解决了什么隐患?@compare(skip) 用在什么场景?
  2. copy(car) 后改副本的 engine.power 为什么会影响原对象?clone 为什么不会?
  3. 只写 derive value 的类能走 json.stringify 吗?derive value 生成的协议为什么平时看不见?
  4. derive json 的类有一个 private 字段,默认会序列化吗?如何强制包含、如何跳过 public 字段?
  5. 手写 __eq__ 与 derive equals 同在一个类里,== 走哪个?这个设计的用途是什么?
  6. 为什么含 array 字段的类写了 derive equals,a == a 也得 false?
参考答案
  1. 手写 __eq__ 而不写 __ne__ 时 != 回退引用语义,可能出现 == 与 != 同真;derive equals 成对生成、永远互反。@compare(skip) 用于缓存、时间戳等"不影响对象身份"的字段。
  2. copy 是浅复制:class 字段只复制引用,副本与原对象的 engine 是同一个实例;clone 深复制递归生成独立副本。string/标量字段在浅复制里也按值复制,所以 name 互不影响。
  3. 能——value 是 json/xson 的底座。它生成的 __toValue__/__fromValue__ 按名调用报 unknown class/union field,只在序列化与 ABI 边界被编译器使用。
  4. 不会——private 默认跳过。强制包含用 @json(include);跳过 public 字段用 @persist(skip)(两家都跳)或 @json(skip)。
  5. 走手写版。derive 是"可降级的默认实现":先 derive 跑通,个别字段需特殊逻辑(如容差比较)时手写接管;共存应留注释防误删。
  6. derive equals 逐字段转 xvalue 比较字符串形态(cgen 证据);数组/class 字段的字符串形态含每次转换新生的地址,恒不相等——故比较族 derive 只适用于标量/string 字段的类。

编程练习

练习 1:配置持久化

编写 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 成功隐身)。

练习 2:点集去重与手写接管

编写 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 是可降级的默认实现。

练习 3:浅与深的对照实验

编写 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。