class 是规则严明的公民,本章的两位是"非常规户":object 给对象装上弹性——运行期随意增删字段;union 把抽象剥到见底——几个字段共享同一块内存,连"当前是哪个字段"都不记。一个最松,一个最硬,却都是 class 家族的正式成员。
前五章把 class 的标准形态讲完了:字段固定、方法齐全、协议可扩、derive 可省。本章讲 class 家族的两翼。object 是标准库提供的普通基类,class X extends object 之后实例获得动态字段能力——运行期随手写 p.rank = 9,不需要事先声明,配置袋、 duck typing、快速原型都靠它;第 25 章留下的伏笔(动态字段是否参与序列化)也在本章揭晓。union 走相反方向:它是 C 风格的共享内存布局声明,所有字段占同一块存储,写 d 读 i 看到的是同一批字节的另一种解读——它不是"安全的代数数据类型",而是 C ABI 的翻译官,为第 32 章的 C 互操作铺路。两者共性值得记住:都不是新的对象系统——object 是普通 class、union 是值类型,语言刻意不造第二套机制。
会用 extends object 读写动态字段(点号与方括号两形态);掌握动态字段三条边界(缺失读得 null、与声明字段双存储、object() 可直接实例化);知道动态字段参与 derive json 序列化且排在声明字段前;能按"要方法继承还是要万用装箱"在 object 与 var 间选型;会声明顶层具名 union 与类内匿名 union,理解共享内存的位级证据;掌握 union 的值语义(传参/赋值/容器全复制);逐条记住 union 的九条限制与 typedef/== 两处 RC0 失效;理解"DEMO6 不提供 struct、union 只服务 C ABI"的设计口径。
| object(extends object) | union | |
|---|---|---|
| 本质 | 标准库普通基类,携带字段协议 | 值类型,C 风格共享内存布局 |
| 字段 | 声明字段 + 运行期任意新增 | 编译期固定,共享同一块存储 |
| 语义 | 引用语义(class 家族) | 值语义(传参/赋值即复制) |
| 方法/继承/协议 | 全部支持(就是 class) | 全部不支持 |
| 典型场景 | 配置袋、动态属性、序列化载体 | C ABI、二进制布局、协议头 |
一句话定位:object 给 class 加弹性,union 给类型减抽象。接下来先松后硬。
继承 object 之后,实例可以在运行期随意增写字段——点号和方括号两种写法等价:
class Actor extends object int id = 0 create(int id) self.id = id end end auto a = Actor(7) a.hp = 120 // 未声明的字段,直接写 a.name = "hero" a["score"] = 30 // 方括号形态,等价 println(a.id) // 7:声明字段照常 println(a.hp) // 120 println(a["score"]) // 30 a.hp = a.hp + 5 println(a.hp) // 125:动态字段参与运算 println(type(a)) // Actor
三个要点:动态字段不需要声明,写即创建;点号形态(a.hp)与方括号形态(a["score"])读写在同一个动态字典上;声明字段、方法、继承、协议一切照常——Actor 仍然是第 21~25 章那个 class,只是基类换成了 object。object 不是保留字,是标准库注入的普通类名;auto o = object() 直接实例化一个"纯动态袋"也合法(o.tag = 1 即用)。
三条实测边界,条条有坑相:
第 25 章的伏笔揭晓:动态字段参与序列化,且排在声明字段之前,roundtrip 完整还原:
class Player extends object derive json int id = 0 string name = "" create() end end auto p = Player() p.id = 7 p.name = "hero" p.rank = 9 // 动态字段 p.guild = "红宝石" // 动态字段 string text = json.stringify(p) println(text) // {"rank":9,"guild":"红宝石","id":7,"name":"hero"} auto q = json.parse::<Player>(text) println(q.rank) // 9:动态字段被还原 println(q.guild) // 红宝石
这使 extends object + derive json 成为弱结构数据的最佳载体:声明字段放稳定核心(id、name),动态字段收零散附加(rank、guild),序列化一键带走。注意 25.10 的边界依旧有效——动态字段值若是 null 的 class 引用同样危险;袋里只放标量、string、容器最安全。
两者都能"装任意东西",但抽象层级不同:
| 维度 | object 动态字段 | var |
|---|---|---|
| 定位 | 形状可变的对象 | 类型擦除的值箱 |
| 访问 | 点号/方括号,直读直写 | 取出须 as 显式转换 |
| 家族能力 | 方法、继承、协议、derive 全有 | 无(只是值) |
| 典型风险 | 读缺失字段静默 null | as 错类型 fatal(第 15 章) |
经验法则:要"像个对象"用 object,要"装个值"用 var。配置、记录、 duck typing 选 object(还能挂方法);跨函数搬运算术值、异构容器元素选 var。var dict 也能当动态袋(bag["hp"]),但没有点号语法、不能挂方法——那是数据,不是对象。
union 在顶层必须具名,字段用 类型 名 声明(不带默认值),裸声明即零初始化:
union Payload int i float64 d end Payload p // 零初始化,无 create println(p.i) // 0 p.i = 42 println(p.i) // 42 println(type(p)) // Payload p.d = 3.5 println(p.d) // 3.5 println(p.i) // 4615063718147915776:3.5 的位模式!
最后一行是 union 的灵魂:所有字段共享同一块内存。写入 d 后,i 读到的不是旧值 42,而是 3.5 的 IEEE 754 位模式按整数解读的结果。xlang 不记录"当前激活字段"、不生成 tag、不做任何保护——读哪个字段是你的责任。这是故意的:union 的全部意义就是描述 C 的内存布局,多一分抽象就偏离 ABI 一分。
union 与 int、float 一样是值类型——传参复制、赋值复制、进容器复制:
union Bits int i float64 f end func mutate(Bits b) b.i = 999 end Bits a a.i = 10 mutate(a) println(a.i) // 10:传参是复制,函数改不到原件 Bits c c = a c.i = 20 println(a.i) // 10:赋值也是复制 array<Bits> arr = [a] arr[0].i = 30 println(a.i) // 10:进容器还是复制
与 class 的引用语义(第 21 章)恰好相反——这份"怎么折腾都不影响原件"正是 C 结构体的行为。容器配合也到位:set<Payload> 按位内容去重(set{42, 42, 43} 的 length 是 2),底层是 memcmp。但注意一个 RC0 裂缝:a == b 直接比较两个 union 会报下游 tcc 错误(坑 5),值比较目前只存在于容器内部路径。
class 体内可以写匿名 union,字段直接暴露为 class 的字段——用来模拟 C 结构体里的匿名 union:
class Packet int tag = 0 union int code float64 ratio end create() end end auto pkt = Packet() pkt.tag = 7 pkt.code = 200 println(pkt.code) // 200:像普通字段一样访问 pkt.ratio = 1.5 println(pkt.code) // 4609434218613702656:共享内存依旧 union Payload2 int i float64 d end class Envelope Payload2 payload // 具名 union 作 class 字段 create() end end auto e = Envelope() e.payload.i = 9 println(e.payload.i) // 9:嵌套访问
两种嵌套形态各司其职:匿名 union 让 Packet 拥有"tag 加一段共享内存"的 C 结构体布局;具名 union 作字段则保持独立类型身份(Envelope 嵌套 Payload2)。匿名 union 的字段名不能与 class 其他成员冲突(报 [sem] duplicate union field name 同款检查)。
九条限制全部实测,诊断原文照录:
| 限制 | 诊断/现象 |
|---|---|
| 无方法 | 写 func 报 [parse] 语法错误 |
| 无 create/destroy | [parse] 语法错误 |
| 无继承 | extends 报 [parse] 语法错误 |
| 字段无默认值 | [sem] union fields do not support default values |
| 不能为空 | [sem] union must contain at least one field |
| 字段名不重复 | [sem] duplicate union field name |
| 顶层必须具名 | [sem] top-level union must have a name |
| class 内必须匿名 | [sem] class inline union must be anonymous in this phase |
| 字段须平凡 C 兼容类型 | var/class 字段报 [sem] union field type must be a plain C-compatible value type |
另有两处实测出入要留意:string 字段可以声明使用(文档口径禁止非平凡类型,实测漏网——写入后读其它字段看到的是字符串指针的位值,生命周期风险自担,别依赖);typedef 别名 union 的字段访问失效——typedef Bits = Payload 后 type(b) 正确显示 Payload,但 b.i 读写都报 [checked] class field receiver is not a class value,别名目前只能用于类型标注场景。
设计文档把话挑明了:DEMO6 不提供 struct 语句,union 也不是"未来安全联合类型"的雏形——它就是 C ABI 的声明工具。三个推论:
经典场景:收到一个 32 位的"打包字",既要整体比较,又要拆出其中的字节字段——union 让"两种解读"零成本共存:
union IpAddr int u32 // 整体:网络字节序的 32 位地址 int8 first // 首字节:共享同一存储的低 8 位 end IpAddr ip ip.u32 = 2130706433 // 0x7F000001,即 127.0.0.1 println(ip.u32) // 2130706433 println(ip.first) // 1:低 8 位(小端机的首字节) println(ip.u32 / 16777216) // 127:高 8 位,A 段 class Header int kind = 0 union int code // 文本协议:状态码 float64 ratio // 二进制协议:压缩率 end create() end end auto h = Header() h.kind = 1 h.code = 404 println(h.kind * 10000 + h.code) // 10404 auto h2 = Header() h2.kind = 2 h2.ratio = 0.75 println(h2.ratio * 100.0) // 75.0
Header 演示了 C 协议解析的标准手法:kind 当"人工 tag",匿名 union 承载随 kind 变化的载荷——语言不帮你记激活字段,你自己用 kind 记。这与 C 代码里 struct { int type; union { ... } } 的布局一模一样,也正是它生成的 C 代码的样子(深入细节 2)。读哪支字段之前先看 kind,是这类代码铁律。
对 26.2 的 Actor 跑 xl cgen,动态字段读写编译为:
"object 是普通基类"在这里落地:动态字段靠 object 携带的 __field__/__setField__/__item__/__setItem__ 四个字段协议实现(第 24 章协议家族成员),底层是一张 xvalue 哈希表。代价随之清晰:声明字段访问是编译期偏移(零成本),动态字段访问是一次方法调用加一次字符串键哈希查找——热路径请用声明字段,动态字段留给冷数据。这也解释了 26.3 的双存储:点号先查声明布局、落空才进协议;方括号直接进协议。
26.6 的 Payload 生成的 C 代码是教科书级直白:
值语义的实现就是 memcpy——这就是为什么传参、赋值、进容器"总是复制"。类内匿名 union 生成 C 匿名 union 字段,布局与手写 C 结构体完全一致。第 32 章讲 extern/export 时会回到这里:具名 union 可以直接做 C ABI 的参数与返回值类型,导出头文件里的 typedef 就是上面这段——xlang 侧和 C 侧看到的是同一块内存的同一副图纸。
26.3 的"同名双存储"(a.id=42 与 a["id"]=43 共存)初看像 bug,其实是分层设计的必然:声明字段走编译期固定布局(快、可校验),动态字典走运行期哈希表(慢、免声明),两套各有存在理由,强行合并反而两头不靠。但语言可以做得更好——至少 a["id"] 与 a.id 同名时给一次警告。在那之前,团队规范补上这条:extends object 的类里,方括号只用于真正动态的键(运行期才知道名字的),声明过的字段永远用点号。弹性是好东西,前提是每个人都知道哪块地是弹性的。
以下报错全部来自真实编译器输出(文件路径已缩短)。
原因:动态字段是 extends object 的专属能力,普通 class 的字段集在编译期封闭。正确做法:要动态字段就继承 object;只要一两个额外属性,老老实实声明字段。
原因:union 字段共享内存,"默认值"无从谈起——整个 union 只有一份零初始化。正确做法:去掉默认值;需要初始值就在声明后显式赋值。
原因:var、class、容器、资源句柄都需要生命周期管理,而 union 不记激活字段、无法析构——只能放平凡 C 兼容值类型。正确做法:字段换成 int/float 家族、point、time 等值类型;string 实测漏网可用但有生命周期风险,别依赖。
原因:class 内嵌 union 当前只支持匿名形态(字段直接暴露);具名内联 union 需要嵌套访问规则,RC0 直接拒绝。正确做法:要么匿名内联,要么把具名 union 提到顶层、作为 class 字段类型引用(26.8 的 Envelope 写法)。
原因:union 的值比较只实现在容器内部路径(set 去重按 memcmp 正常),== 运算符没有为 union 生成比较调用,下游 C 编译器报出不优雅的错误。正确做法:逐字段比较(a.i == b.i),或借助 set<T> 的成员判定。
| 主题 | 规则 | 要点 |
|---|---|---|
| 动态字段 | extends object 后写即创建 | 点号/方括号等价;读缺失得 null;object() 可裸实例化 |
| 双存储 | 声明字段与动态字典同名共存 | 点号走声明、方括号走字典,互不覆盖 |
| 动态字段 × json | 参与序列化,排在声明字段前 | roundtrip 完整;弱结构数据首选载体 |
| object vs var | 像对象用 object,装值用 var | object 有方法/继承/derive;var 是类型擦除值箱 |
| union 基础 | 顶层具名;字段共享内存;裸声明零值 | 写 d 读 i 得位模式;无 tag 无保护 |
| union 值语义 | 传参/赋值/容器全复制(memcpy) | 与 class 引用语义相反;set 按位去重 |
| 嵌套形态 | 类内匿名 union / 具名 union 作字段 | 匿名字段直达 pkt.code;具名保持类型身份 |
| union 限制 | 九条禁令(方法/继承/默认值/空/重名等) | string 字段漏网勿依赖;typedef 别名字段访问失效;== 不可用 |
| 设计口径 | union = C ABI 工具,非 struct 非安全联合 | 激活字段自己记(人工 tag);第 32 章回链 |
编写 profile.xl:class Profile extends object,声明 version 字段并 derive json。运行期写入 theme/fontSize/sidebar 三个动态字段,json.stringify 打印,再 parse 回来验证动态字段还原;最后读一个从未设置的字段观察输出。
参考思路:输出形如 {"theme":"dark","fontSize":14,"sidebar":true,"version":1}——动态字段在前;读缺失字段得 null(这就是 26.3 的静默规则,练习里亲眼见一次印象最深)。
编写 reg.xl:union Reg 含 int whole 与 int8 lowByte。写入 whole = 305419896(0x12345678),读 lowByte 验证低 8 位;再把 lowByte 改成 1,读 whole 观察高 24 位不动、低 8 位被换。最后裸声明一个 Reg 验证零初始化。
参考思路:预期输出 120(0x78)、305419777(0x12345601)、0——低字节在小端机上与 whole 的低 8 位共享存储,改哪头另一头都看得见。
编写 color.xl:class Rgb extends object,声明 r/g/b 字段与 pack() 方法(r×65536+g×256+b);union Packed 含 int argb 与 int8 blue。把 Rgb(18, 52, 86) 打包进 Packed,读 argb 整体值与 blue 通道;再给实例写动态字段 note 并打印——一个类同时用上声明字段、方法、动态字段,旁边站着共享内存的 union。
参考思路:预期 1193046、86、note 原文。打包用算术、解包靠 union——两种思路在同一程序里对照,正是本章"加弹性"与"减抽象"的合影。