第 21 章 / 共 58 章 卷五 OOP

class 基础

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 实例的配合与裸声明陷阱。

21.1 第一个 class

class 体里是字段(带类型标注,可给默认值)与方法(func 定义,self 指代当前实例):

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

21.2 create 构造函数

create 是构造入口的特殊声明(不写 func、不写返回类型),用 self 初始化字段:

create.xl
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() / create(int x) / create(int x, int y) 可共存;create(int x = 5) 让 Point() 与 Point(9) 都合法。实参类型不匹配报 [sem] 没有匹配的 create 重载。
  • 定义任何 create 后,无参默认构造消失——只写了 create(int x) 再调 Point(),报 [sem] create 调用实参数量和重载签名不匹配: expected 1, actual 0;想保留无参构造就补一个 create() 或给参数默认值。
  • 构造表达式有位置限制——声明右侧、return、函数实参位置均可;但 println(Point(1).x) 这种"构造后立刻链式访问"报 [checked] class constructor expression requires statement-level lowering,先赋给变量再用(坑 2)。

21.3 destroy 析构函数

destroy 在实例生命结束时自动触发——局部实例出作用域即触发,全局实例在程序结束时触发:

destroy.xl
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 等任何退出路径都会触发。

21.4 访问控制

成员默认 public;private 只允许 class 内部访问,protected 放宽到子类(子类部分第 22 章展开)。从外部越权是编译期错误,两条真实诊断:

# println(s.value)(value 是 private 字段) bad.xl:5:11: [sem] private class member is not accessible # println(b.n)(n 是 protected 字段) bad.xl:6:11: [sem] protected class member is not accessible

强制力是真实的:自由函数、其他 class、模块顶层代码都过不去。一个容易困惑的细节:越权调用 private 方法的报错文案不同——

# s.hidden()(hidden 是 private 方法) bad.xl:12:11: [sem] unknown class/union field

像"方法不存在",其实是可见性过滤后的查无此名。private 字段 + public 读方法的组合(21.8 实战)是封装的标准姿势。

21.5 引用语义与 ==

class 实例是引用:赋值、传参都共享同一个对象(与第 12 章参数传递的结论一致):

ref.xl
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 方法逐字段对比。

21.6 static 成员

static 成员属于类型而非实例:全实例共享一份,经类型名访问:

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

21.7 与 var/auto/is 配合

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

21.8 实战:银行账户

把本章构件组装起来:private 字段保护余额,public 方法定义操作,create 初始化户主:

account.xl
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 诊断拦下——余额只能通过三个方法按规则变动,这就是封装的意义。

深入细节

拾遗 1:class 编译成了什么——C 结构体与直接分发

用 xl cgen 观察 Point,生成的核心代码是:

struct XL_Class_Point { long long x; long long y; }; long long xl_class_Point__sum(XL_Class_Point* self) { return (self->x + self->y); } // 调用点:xl_class_Point__sum((XL_Class_Point*)xl6_class_data(p, ...))

字段布局就是 C 结构体,方法编译为以 self 指针为首参的普通 C 函数,调用点直接静态分发——没有虚表、没有字典查找,self.x 就是 self->x。这就是"普通成员访问不经过动态字典"承诺的兑现:class 实例的成员访问成本与 C 结构体完全相同。

拾遗 2:self 可以省略吗——遮蔽规则

实测方法体内 return x 能读到字段 x——读取时 self 可省略,字段名直接可见,但局部变量优先:方法里声明 int x = 100 后,x 指局部、self.x 指字段。写入则是另一套规则:x = 5 直接报 [sem] 赋值目标未声明——无遮蔽时字段不能省略 self 赋值;有同名局部时改的是局部,字段纹丝不动。结论:写字段永远写全 self.x——这是编译器强制的,不是风格建议。

拾遗 3:设计文档有、RC0 还没有的两处空白

对照协议设计文档,两处语法在 RC0 报 [parse] 语法错误:static const 字段(文档示例 static const int Max = 255,RC0 只认 static 可变字段);嵌套 class(class 体内再声明 class)。遇到需求时的绕行:常量用 static 可变字段加"约定不写",或模块级常量;嵌套命名用类名前缀(OuterInner)摊平。写教程不藏这两处,是为了让读者看到文档示例报错时不怀疑自己。

⚠️ 常见错误

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

坑 1:给默认构造传实参
# bad.xl 内容:class 里只有字段,没有写 create # Point p = Point(1, 2) > xl run bad.xl bad.xl:5:16: [sem] class 默认构造不接收实参: expected 0, actual 2

原因:没有 create 时只有无参默认构造,语言不会按字段顺序自动喂参。正确做法:自己写 create(int x, int y) 并逐字段赋值。

坑 2:构造后立刻链式访问
# bad.xl 内容: # println(Point(1).x) > xl run bad.xl bad.xl:7:14: [checked] class constructor expression requires statement-level lowering

原因:构造表达式不能作为成员访问的接收者(RC0 的语句级 lowering 限制)。正确做法:先 auto p = Point(1) 再 p.x;声明右侧、return、函数实参位置都不受此限。

坑 3:外部读写 private 成员
# bad.xl 内容: # println(s.value) // value 是 private > xl run bad.xl bad.xl:5:11: [sem] private class member is not accessible

原因:private 只允许 class 内部访问,自由函数也一样被拦。正确做法:提供 public 读方法(reveal/check);注意越权调 private 方法的报错是 [sem] unknown class/union field,文案像"不存在",实为可见性拦截。

坑 4:static 方法里用 self
# bad.xl 内容: # static func nope() as int / return self.value > xl run bad.xl bad.xl:4:12: [sem] static class member cannot use self

原因:静态方法不属于任何实例,没有 self。正确做法:访问 static 字段用类型名(Counter.total);要操作实例就把实例作参数传进来。

坑 5:定义 static func create(保留名冲突)
# bad.xl 内容: # class Entity / static func create() as int ... > xl run bad.xl bad.xl:2:15: [sem] create/destroy must use the special declaration form

原因: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

复习题

✍️ 先遮住答案想一想
  1. class 里字段不写默认值、create 里也不赋值,读出来是什么?会报错吗?
  2. 写了 create(int x) 之后 Point() 还能用吗?报错原文是什么?两种恢复办法?
  3. destroy 什么时候触发?能带参数或写两个重载吗?
  4. 外部访问 private 字段与 private 方法的报错文案分别是什么?
  5. a == b 在两个字段全同的不同实例上为什么是 false?
  6. Point p(裸声明)之后 println(p.x) 会发生什么?为什么?
参考答案
  1. 零值(int 为 0),不报错——字段零值兜底,语言不强制显式初始化。
  2. 不能。[sem] create 调用实参数量和重载签名不匹配: expected 1, actual 0。恢复:补一个 create() 重载,或给参数默认值 create(int x = 5)。
  3. 局部实例出作用域时、全局实例程序结束时触发;return/break/continue 路径都算。不能带参数([sem] destroy 不能声明参数),也不支持重载。
  4. 字段:[sem] private class member is not accessible;方法:[sem] unknown class/union field(可见性过滤后的查无此名)。
  5. class 的 == 是引用同一性比较,不比内容;字段相同但两个对象引用不同即为 false。
  6. 运行时 fatal 退出(退出码 4)。裸声明不构造实例,p 的值是 null,对 null 做成员访问直接崩溃。

编程练习

练习 1:平面向量

编写 vec.xl:定义 class Vec2(字段 x/y 为 float),create 双参构造,方法 length() 返回模长、scale(float k) 缩放自身、dot(Vec2 other) 返回点积。造两个向量演示全部方法。

参考思路:模长用 (x*x+y*y) 的平方根——没有 sqrt 时可用牛顿迭代几步,或改用 length2() 返回平方模长;other.x 在同类方法内访问合法(private 也是"类内"可见)。

练习 2:实例计数器

编写 count.xl:给 class Widget 加 static int alive 字段,create 里 +1、destroy 里 -1;写一个函数造 3 个实例后返回,前后各打印一次 alive,验证析构时机。

参考思路:函数内 alive 峰值 3,函数返回后回落——对照 21.3 的 Tracker 输出顺序;static 字段经 Widget.alive 访问。

练习 3:安全仓库

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