class 是 xlang 的静态类基座:固定字段布局、固定方法集合、构造与析构生命周期、引用语义。本章把声明形态、create/destroy、访问控制、static 成员逐条实测,为卷五 OOP 打底。
前面各章的值——int、string、array、dict——都是语言给定的;class 让你定义自己的类型:把字段(数据)和方法(行为)装进一个命名实体,再用 Point(1, 2) 这样的调用批量制造实例。本章实测 class 的全部基础构件:字段与默认值、create 构造(含重载与位置限制)、destroy 析构时机、self 语义、public/private/protected 访问控制的真实强制力、static 成员、与 var/auto/is 的配合,并用一个银行账户实战收束。继承、多态、协议方法留待第 22 章及以后。
会写 class 声明(字段、默认值、方法);掌握 create 的参数化、重载、默认参数与"定义后默认构造消失"规则;理解 destroy 的触发时机与无参约束;背下 private/protected 越权的两条真实报错;理解 class 的引用语义(赋值/传参共享、== 比引用);会用 static 字段/方法及类型名访问;掌握 auto/var/is 与 class 实例的配合与裸声明陷阱。
class 体里是字段(带类型标注,可给默认值)与方法(func 定义,self 指代当前实例):
class Point int x = 3 // 字段:类型 + 默认值 int y = 4 func sum() as int // 方法:self 指当前实例 return self.x + self.y end end Point p = Point() // 调用类型名即构造实例 println(p.x, " ", p.y, " ", p.sum()) // 3 4 7 p.x = 10 // 字段公开可写 println(p.sum()) // 14 auto q = Point() // auto 推断为 Point println(type(q)) // Point:type() 回答类名
要点:字段不带默认值时零值兜底(int 为 0),不会因"未初始化"报错;方法内写字段必须冠 self(遮蔽规则见深入细节拾遗 2);Point() 是无 create 时的默认构造,不接收实参(见常见错误坑 1)。
create 是构造入口的特殊声明(不写 func、不写返回类型),用 self 初始化字段:
class Point int x int y create(int x, int y) // 参数化构造 self.x = x self.y = y // 不赋值的字段保持零值,不报错 end end Point p = Point(1, 2) println(p.x + p.y) // 3
create 的三条实测规则:
create(int x = 5) 让 Point() 与 Point(9) 都合法。实参类型不匹配报 [sem] 没有匹配的 create 重载。Point(),报 [sem] create 调用实参数量和重载签名不匹配: expected 1, actual 0;想保留无参构造就补一个 create() 或给参数默认值。println(Point(1).x) 这种"构造后立刻链式访问"报 [checked] class constructor expression requires statement-level lowering,先赋给变量再用(坑 2)。destroy 在实例生命结束时自动触发——局部实例出作用域即触发,全局实例在程序结束时触发:
class Tracker int id create(int id) self.id = id println(f"create {id}") end destroy() println(f"destroy {self.id}") end end func scope() auto t = Tracker(1) println("in scope") end scope() // create 1 → in scope → destroy 1(函数返回即析构) println("after scope") // after scope auto g = Tracker(2) // create 2;程序结束才 destroy 2
约束:destroy 不能带参数([sem] destroy 不能声明参数)、不支持重载,每个 class 至多一个。它是释放资源(句柄、缓冲、连接)的挂钩点——return、break、continue 等任何退出路径都会触发。
成员默认 public;private 只允许 class 内部访问,protected 放宽到子类(子类部分第 22 章展开)。从外部越权是编译期错误,两条真实诊断:
强制力是真实的:自由函数、其他 class、模块顶层代码都过不去。一个容易困惑的细节:越权调用 private 方法的报错文案不同——
像"方法不存在",其实是可见性过滤后的查无此名。private 字段 + public 读方法的组合(21.8 实战)是封装的标准姿势。
class 实例是引用:赋值、传参都共享同一个对象(与第 12 章参数传递的结论一致):
class Counter int n = 0 func inc() self.n = self.n + 1 end end Counter a = Counter() Counter b = a // 共享,不是复制 b.inc() println(a.n) // 1:b 的修改 a 看得见 func bump(Counter c) c.inc() end bump(a) println(a.n) // 2:传参同样共享 println(a == b) // true:== 比的是引用同一性 println(a == Counter()) // [checked]:构造表达式不能这样用,先赋值再比
实测:a == b(同引用)为 true,而两个字段完全相同的不同实例相等为 false——== 是身份比较不是内容比较。想要值比较需要自己写 equals 方法逐字段对比。
static 成员属于类型而非实例:全实例共享一份,经类型名访问:
class Counter static int total = 0 // 类型级字段,全实例共享 int n = 0 create() Counter.total = Counter.total + 1 // 统计实例数 end static func reset() // 静态方法:没有 self Counter.total = 0 end end Counter a = Counter() Counter b = Counter() println(Counter.total) // 2 Counter.reset() println(Counter.total) // 0 Counter.total = 5 // 可写(无 static const 拦截,见拾遗 3) println(Counter.total) // 5
三条边界:static 方法里用 self 报 [sem] static class member cannot use self;经实例访问 static 成员(c.total)报 [checked] class field not found——静态成员只能从类型名走;create/destroy 是保留名,不能定义 static func create(坑 5)。
class Point int x = 1 end Point p = Point() auto q = Point() // auto 推断为 Point(静态类型) var v = p // var 装箱,仍是共享引用 v.x = 99 // var 也能点字段(动态分派) println(p.x) // 99 println(type(v)) // Point:type() 穿透装箱答类名 println(v is Point) // true println(p is int) // false Point r // 裸声明:不构造! println(r) // null——访问 r.x 会 fatal 退出
要点:auto 从构造表达式推断出静态类类型;var 装箱后保持引用语义(v.x 改的是同一个对象);is 判断类成员身份。最大的坑是裸声明:Point r 不会构造实例,r 是 null,访问成员直接 fatal(退出码 4)——声明 class 变量时永远带 = Point()。
把本章构件组装起来:private 字段保护余额,public 方法定义操作,create 初始化户主:
class Account private int balance = 0 // 余额外部摸不到 string owner create(string owner) self.owner = owner end func deposit(int amount) self.balance = self.balance + amount end func withdraw(int amount) as bool if amount > self.balance then return false // 透支拒绝 end self.balance = self.balance - amount return true end func check() as int return self.balance end end Account acc = Account("小明") acc.deposit(100) println(acc.withdraw(30)) // true println(acc.withdraw(80)) // false:余额不足 println(acc.check()) // 70 Account alias = acc // 别名共享同一账户 alias.deposit(50) println(acc.check()) // 120
acc.balance 若在外部直接读写,编译期即被 21.4 的 private 诊断拦下——余额只能通过三个方法按规则变动,这就是封装的意义。
用 xl cgen 观察 Point,生成的核心代码是:
字段布局就是 C 结构体,方法编译为以 self 指针为首参的普通 C 函数,调用点直接静态分发——没有虚表、没有字典查找,self.x 就是 self->x。这就是"普通成员访问不经过动态字典"承诺的兑现:class 实例的成员访问成本与 C 结构体完全相同。
实测方法体内 return x 能读到字段 x——读取时 self 可省略,字段名直接可见,但局部变量优先:方法里声明 int x = 100 后,x 指局部、self.x 指字段。写入则是另一套规则:x = 5 直接报 [sem] 赋值目标未声明——无遮蔽时字段不能省略 self 赋值;有同名局部时改的是局部,字段纹丝不动。结论:写字段永远写全 self.x——这是编译器强制的,不是风格建议。
对照协议设计文档,两处语法在 RC0 报 [parse] 语法错误:static const 字段(文档示例 static const int Max = 255,RC0 只认 static 可变字段);嵌套 class(class 体内再声明 class)。遇到需求时的绕行:常量用 static 可变字段加"约定不写",或模块级常量;嵌套命名用类名前缀(OuterInner)摊平。写教程不藏这两处,是为了让读者看到文档示例报错时不怀疑自己。
以下报错全部来自真实编译器输出(文件路径已缩短)。
原因:没有 create 时只有无参默认构造,语言不会按字段顺序自动喂参。正确做法:自己写 create(int x, int y) 并逐字段赋值。
原因:构造表达式不能作为成员访问的接收者(RC0 的语句级 lowering 限制)。正确做法:先 auto p = Point(1) 再 p.x;声明右侧、return、函数实参位置都不受此限。
原因:private 只允许 class 内部访问,自由函数也一样被拦。正确做法:提供 public 读方法(reveal/check);注意越权调 private 方法的报错是 [sem] unknown class/union field,文案像"不存在",实为可见性拦截。
原因:静态方法不属于任何实例,没有 self。正确做法:访问 static 字段用类型名(Counter.total);要操作实例就把实例作参数传进来。
原因:create/destroy 是生命周期保留名,只能以特殊形态(无 func、无返回类型)声明,不能当作普通静态工厂名。正确做法:工厂方法改名 make/of/from——static func fromUnix(int s) as Entity。
| 主题 | 规则 | 要点 |
|---|---|---|
| 声明 | class 名 / 字段 + 方法 / end | 字段带类型可给默认值;缺省零值兜底 |
| create | 特殊声明,可重载可默认参数 | 定义后默认构造消失;未赋值字段零值;链式访问受限 |
| destroy | 无参、不可重载 | 作用域退出/程序结束触发;任何退出路径都走 |
| 访问控制 | 默认 public;private 本类;protected 加子类 | 越权 [sem] 编译期拦截;private 方法报错文案不同 |
| 引用语义 | 赋值/传参共享;== 比引用 | 要值比较自写 equals;var 装箱保持共享 |
| static | 类型名访问;无 self;不参与实例布局 | static const 与嵌套 class RC0 未实现;create/destroy 保留名 |
| 类型配合 | auto 推断类类型;is 判身份;type() 答类名 | 裸声明得 null,访问成员 fatal |
| 实现 | C 结构体 + 直接静态分发 | 成员访问无字典无虚表,成本等同 C |
编写 vec.xl:定义 class Vec2(字段 x/y 为 float),create 双参构造,方法 length() 返回模长、scale(float k) 缩放自身、dot(Vec2 other) 返回点积。造两个向量演示全部方法。
参考思路:模长用 (x*x+y*y) 的平方根——没有 sqrt 时可用牛顿迭代几步,或改用 length2() 返回平方模长;other.x 在同类方法内访问合法(private 也是"类内"可见)。
编写 count.xl:给 class Widget 加 static int alive 字段,create 里 +1、destroy 里 -1;写一个函数造 3 个实例后返回,前后各打印一次 alive,验证析构时机。
参考思路:函数内 alive 峰值 3,函数返回后回落——对照 21.3 的 Tracker 输出顺序;static 字段经 Widget.alive 访问。
编写 vault.xl:class Vault 用 private 数组存秘密字符串,提供 put(string s)、count()、get(int i) 三个 public 方法,越界 get 返回空串;尝试从外部直接访问私有数组,把编译报错原文贴进注释。
参考思路:private array<string> items = [];get 里用 contains 思路判界(i >= 0 且 i < items.length);外部访问 items 会触发 [sem] private class member is not accessible。