属性让"读写字段"的语法执行"调用方法"的逻辑:c.area 背后是一次计算,p.hp = -5 背后可以是一道校验。get 管读、let 管写,还能带维度参数把属性变成索引形态——本章逐条实测。
字段是"存什么",方法是"做什么",属性站在中间:语法上像字段(c.area、p.hp = 42),执行上像方法(读取跑 get、写入跑 let)。它的两个经典用途:计算属性——面积、模长这类"从其他字段推出、不值得存"的值;受控写入——血量不能为负、年龄不能倒退这类"写之前先校验"的约束。xlang 的属性还支持维度参数(m.value[1]、g.at[2, 4])和 virtual/override 多态,本章把声明形态、读写组合、索引回退规则逐一实测,并给出字段/属性/方法的选型准则。
会写 get 计算属性与 let 受控写入;掌握只读/只写/读写三种组合及两条越权报错;会用维度参数把属性做成索引形态(含多维);理解 virtual get/let 的多态派发;掌握属性上复合赋值的语义与求值次数;理解索引歧义的回退规则与维度优先;能按"存数据/要逻辑/带参数"三问做选型。
get 名字() as 类型 声明一个只读计算属性,访问时不加括号:
class Circle float r = 2.0 get area() as float return 3.14159 * self.r * self.r end end auto c = Circle() println(c.area) // 12.56636:像读字段,实际跑了一遍计算 c.r = 3.0 println(c.area) // 28.27431:每次访问重新求值
每次访问都执行 get 体——没有缓存。用副作用探针实测:get 里给计数字段 +1,读两次 c.tick,计数变成 2。这意味着 get 里写重逻辑要掂量成本;也意味着 get 永远返回最新值,不存在"存下来的面积忘了更新"的问题。
let 名字(类型 v) 给属性加写入通道,校验逻辑就放在这里:
class Player private int hpValue = 100 get hp() as int return self.hpValue end let hp(int v) if v < 0 then self.hpValue = 0 // 写入校验:血量不为负 else self.hpValue = v end end end auto p = Player() println(p.hp) // 100 p.hp = 42 println(p.hp) // 42 p.hp = -5 println(p.hp) // 0:被 let 拦下修正
三种组合与对应诊断(实测):
[sem] class property assignment requires matching let。[sem] unknown class/union field——与 private 方法同款"查无此名"文案。private 字段 + public 属性是封装的进阶版:第 21 章用 reveal()/check() 方法做的事,属性用字段语法就做到了,还多了一层写入校验。
get/let 可以带维度参数,访问形态从点号变成方括号——属性就此变成"索引外观":
class Matrix array<int> cells = [10, 20, 30] get value(int idx) as int return self.cells[idx] end let value(int idx, int v) // let 末参固定是写入值 self.cells[idx] = v end end auto m = Matrix() println(m.value[1]) // 20:方括号形态 m.value[1] = 99 println(m.value[1]) // 99
注意访问语法是 m.value[1] 而非 m.value(1)——维度属性不是方法,圆括号调用报 [sem] unknown class/union field(坑 3)。同一名字的零维属性与维度属性可以共存(23.7 的回退规则由此展开)。
维度可以不止一个,let 的最后一个参数固定是写入值,前面都是维度:
class Grid int base = 10 get at(int x, int y) as int return self.base + x * 10 + y end let at(int x, int y, int v) // x、y 是维度,v 是写入值 self.base = v + x * 10 + y end end auto g = Grid() println(g.at[2, 4]) // 34:10 + 20 + 4 g.at[2, 4] = 5 println(g.base) // 29:5 + 20 + 4
约束(设计文档明确、实测印证):维度参数只支持位置参数;.../*** 可变参数报 [sem] property parameters cannot be vararg;同名不同维度数的属性算重载(get at(x) 与 get at(x, y) 可共存)。
属性和方法一样参与 virtual/override 体系,读写两侧独立派发:
class Entity int baseValue = 1 virtual get prop() as int return 10 end virtual let prop(int value) self.baseValue = value end end class Player extends Entity int bonus = 5 override get prop() as int return 70 end override let prop(int value) self.bonus = value end end Entity e = Player() println(e.prop) // 70:读派发到 Player 的 get e.prop = 9 // 写派发到 Player 的 let Player p = e as Player println(p.bonus) // 9
第 22 章的规则原样适用:覆盖必须 override、目标必须 virtual、签名必须精确。属性因此可以进入类的"契约"——父类定义"每个实体都有 prop",子类各自实现存储方式。
读写双全的属性支持 +=、-= 等复合赋值,语义是"get 一次、let 一次":
auto p = Player() p.hp = 10 p.hp += 5 // 等价 p.hp = p.hp + 5:先 get 后 let println(p.hp) // 15 p.hp -= 3 println(p.hp) // 12
维度属性的复合赋值有一个关键保证(官方测试用例锁定):索引表达式只求值一次——p.hp[propertyIndex(2)] += 5 里 propertyIndex 只被调用一次,不会像朴素展开那样求值两遍。用计数函数验证过:一次复合赋值,调用计数恰好 +1。
obj.prop[i] 有两种可能的含义,解析顺序实测如下:
class Team array<string> data = ["a", "b"] get names() as array<string> return self.data end end auto t = Team() println(t.names[1]) // b:无维度属性,回退为 (t.names)[1]
设计文档还给了 (obj.prop)[i] 强制索引返回值的写法,但实测在维度属性存在时它仍走维度属性(应索引返回值却返回了维度结果)——这条强制通道 RC0 尚未生效,记为待修空白;需要索引返回值时,先把属性赋给局部变量再取下标即可。
三者的边界由两个问题决定:要不要逻辑?要不要参数?
| 选择 | 场景 | 代价/收益 |
|---|---|---|
| 字段 | 纯数据,读写无需干预 | 零开销直接访问;日后加校验要改调用点 |
| 属性 | 读取要计算 / 写入要校验 / 要索引外观 | 每次访问一次调用;调用点无感,可随时从字段升级 |
| 方法 | 有副作用的动作、参数与"值"无关 | 语义最直白;调用点必须写括号 |
经验法则:名词用属性,动词用方法——area、length、hp 是名词(对象的某种"值"),draw、save、attack 是动词(对象做的"事")。计算昂贵又无参数时,宁可在 get 里缓存到字段(自己管失效),也别让每次访问跑重活。
用 xl cgen 观察 23.2 的 hp 属性,生成的核心代码是:
属性没有任何特殊运行时:get/let 就是一对换名方法,访问语法在编译期翻译成对应调用,分发方式与普通方法相同(virtual 属性走虚表,普通属性直接调用)。这解释了两件事:属性开销 = 一次方法调用;"属性是字段语法的方法"不是比喻,是实现。
为什么写入侧叫 let 而不是 set?设计文档给出的理由是避免与 set 容器类型撞名——xlang 已有 set<T>(第 17 章),如果属性写入侧也叫 set,set 一词就同时承担"集合类型""赋值动作"两个角色,解析与阅读都会纠缠。get/let 取自 BASIC 家族的传统(LET x = 1),读作"让它成为"。文档还规定 get/let/set 不得作为变量名与成员名,但 RC0 对变量名尚未强制执行(实测 int let = 1 可用)——作为字段名则会让编译直接失败(坑 5),写代码时照旧避开这三个词。
23.1 的副作用探针值得再强调:属性访问没有缓存,for 循环里写 c.area 一千次就跑一千次计算。三种应对:热路径先把属性读进局部变量;get 体内自建缓存(字段存结果 + 脏标记);干脆改方法命名(calcArea())让成本显形。反过来,这个"每次求值"也是正确性保证——属性永远不会返回过期值,这是它与"字段存派生值"方案的本质差异。
以下报错全部来自真实编译器输出(文件路径已缩短)。
原因:写入通道由 let 单独声明,缺 let 即只读。正确做法:补 let hp(int v);只读是刻意的就改从方法写入底层字段。
原因:没有 get 的名字在读取位置查无此名——报错文案与"不存在"相同,容易误判为拼写错误。正确做法:补 get;只写属性本就少见,先确认是不是把"设值方法"误写成了属性。
原因:维度属性不是方法,调用语法是方括号。正确做法:m.value[3];想要圆括号语义就老老实实写方法。
原因:属性的维度签名必须是固定位置参数(... 与 *** 都被禁)。正确做法:声明确定维度数;不定长索引用 array 参数或方法承载。
原因:get/let/set 是属性声明的上下文关键字,用作 class 字段名会让编译产物丢失入口(RC0 的下游怪异报错,非优雅诊断)。正确做法:字段改名(如 getCount → count);普通局部变量用这三个词 RC0 暂不拦,但文档已禁,别依赖。
| 主题 | 规则 | 要点 |
|---|---|---|
| get | get 名() as 类型;访问不加括号 | 每次访问都求值,无缓存 |
| let | let 名(类型 v);写入入口 | 校验逻辑的家;只读写报 requires matching let |
| 组合 | 只读/只写/读写 | 只写读报 unknown class/union field(文案误导) |
| 维度属性 | get 名(维度...);访问 obj.名[i] | let 末参是值;圆括号调用无效;禁 vararg |
| virtual 属性 | virtual get/let + override | 读写两侧独立派发,规则同方法 |
| 复合赋值 | += 等 = get 一次 + let 一次 | 维度索引表达式只求值一次 |
| 索引回退 | 维度优先;否则索引属性返回值 | (obj.prop)[i] 强制写法 RC0 未生效 |
| 选型 | 名词属性、动词方法、纯数据字段 | 属性 = 字段语法的方法对(cgen 证据) |
编写 temp.xl:class Temperature 私有存摄氏度字段,提供 celsius 读写属性与 fahrenheit 读写属性(写 fahrenheit 时换算回摄氏度存储)。演示双向读写。
参考思路:f = c × 1.8 + 32,反向 c = (f - 32) / 1.8;存储只留一份(摄氏度),另一个单位纯计算——属性的"单一事实源"经典案例。
编写 counter.xl:class Counter 的 count 属性读写,let 里把值钳制在 0~100(小于 0 记 0,大于 100 记 100)。再用 += 演示复合赋值也受钳制。
参考思路:复合赋值 = get + let,钳制只在 let 里写一次就两种入口都管住;对照 23.6 的语义验证输出。
编写 board.xl:class Board 内部用 array<int> 存 9 格棋盘,提供维度属性 cell(int i) 读写(带越界保护:越界读返回 -1、越界写忽略),再提供 get row(int r) 返回某行三格之和。打印验证。
参考思路:cell 用方括号访问 b.cell[4];row 带维度参数,访问 b.row[1];越界保护逻辑全部收进 get/let,调用点永远安全——这就是维度属性相对裸数组的价值。