extends 复用父类的字段与行为,virtual/override/final 决定哪些方法可以被子类改写,动态派发让父类引用自动调用子类的实现——本章把这条链上的每条规则与每个报错逐一实测。
第 21 章的 class 是孤立的个体;本章让它们结成家族。extends 继承字段与方法;base(...) 把父类构造嵌入构造链;virtual/override/final 管理"哪些方法允许改写、改写必须显式、何处到此为止";动态派发让 Entity e = Player() 后 e.score() 自动走 Player 的实现。xlang 的继承是强约束风格:覆盖必须写 override、目标必须 virtual、签名必须精确——每条都有专属诊断,本章全部实测并附原文。
掌握 extends 声明与父类成员继承;背下 base(...) 的两条"必须显式"规则与使用位置;理解 virtual/override/final 的协作与四条报错;理解动态派发的条件(父类引用 + 虚方法)与 vtable 实现;掌握 protected 的放行范围;会用 is 守卫 + as 向下转型;了解同名字段遮蔽与多层继承链行为。
class 子类 extends 父类:子类自动拥有父类的字段与方法,可以直接增改:
class Entity int baseValue = 1 virtual func score() as int return self.baseValue end func run() as int // 非虚方法:子类继承,不可改写 return self.score() + 1 end end class Player extends Entity int bonus = 5 override func score() as int return 40 + self.bonus end end Player p = Player() println(p.score()) // 45:子类改写版 println(p.run()) // 46:继承的 run 内部也调到了子类 score println(p.baseValue) // 1:父类字段直接可用 Entity e = p // 父类引用指向子类实例(向上转型,总是合法) println(e.score()) // 45:多态的核心——见 22.4
继承是单继承:extends 后只能跟一个父类;父类名不存在时报 [sem] 未知父类。注意 run() 的输出:它定义在父类、没有被子类改写,但内部调用 virtual score() 仍派发到子类版本——虚方法的"虚"全程有效。
子类构造必须先让父类部分就位。父类有无参构造(或没有 create)时,这条链自动完成;父类 create 带参时,编译器强制你显式接力:
class Base int seed = 0 create(int seed) self.seed = seed end end class Child extends Base int value = 0 create(int seed, int value) base(seed) // 显式调用父类构造 self.value = value end end auto c = Child(7, 9) println(c.seed, " ", c.value) // 7 9
规则全集(全部实测有专属诊断):
[sem] 父类 create 需要参数,子类 create 必须显式调用 base(...)。[sem] 父类 create 需要参数,子类必须声明 create 并显式调用 base(...)。[sem] base(...) 只能在 class 的 create 体内使用。[sem] create 调用实参数量和重载签名不匹配。三件套各管一环:virtual 声明"此方法允许改写"、override 声明"我在改写父类方法"、final 声明"改写到此为止"。协作规则由编译器严格把守:
| 写法 | 结果(实测原文) |
|---|---|
| 子类改写父类 virtual 方法,不写 override | [sem] overriding a base member requires explicit override |
| override 一个非 virtual 的父类方法 | [sem] override target must be virtual |
| override 一个 final 方法 | [sem] cannot override a final class member |
| override 签名与父类不一致 | [sem] override requires an exact base member signature |
| 给 private 成员加 virtual/override/final | [sem] private class member cannot use override/final/virtual |
两个推论:非 virtual 方法根本无法被覆盖(不写 override 报第一行错,写了报第二行错)——所以 xlang 没有"意外遮蔽父类方法"这种事故;final virtual 组合表示"我是虚方法,但继承链到此为止"。override 链可以接力:A virtual → B override → C 继续 override 合法(22.8 实测)。
多态的完整条件:父类类型的引用装着子类实例,调用虚方法——运行时按实例的真实类型选实现:
Entity e = Player() // 静态类型 Entity,动态类型 Player println(e.score()) // 45:走 Player 的 override println(e.run()) // 46:run 是 Entity 的,但内部 score() 仍派发到 Player println(e.baseValue) // 1:字段没有多态——按声明类型直接取
注意引用的静态类型决定你能看见什么:e 的类型是 Entity,访问 Player 独有的字段(e.hp)报 [sem] unknown class/union field——尽管实例里明明有这个字段。多态只发生在 virtual 方法的调用上;字段访问、非虚方法都按静态类型解析(非虚方法也不可能被子类改写,22.3)。底层机制是 vtable,证据见深入细节拾遗 1。
第 21 章见过:protected 成员从外部访问报 [sem] protected class member is not accessible。它与 private 的唯一差别在继承体系内——子类的方法里可以直接用:
class Base protected int secret = 42 // 外部摸不到,子类随便用 end class Child extends Base func peek() as int return self.secret // 子类内合法 end end Child c = Child() println(c.peek()) // 42
选型准则:只想自己用 → private;想让子类参与实现但不对外开放 → protected;公开接口 → public(默认)。protected 是为继承设计的"家族内部通道"。
向上转型(子→父)永远合法;向下转型(父→子)需要运行时验证,配套两个运算符:
Entity e = Player() println(e is Player) // true:is 看动态类型,沿继承链都为 true println(e is Entity) // true if e is Player then Player p = e as Player // as 向下转型,取回完整接口 println(p.hp) // 100:子类字段回来了 end
as 失败的代价很大:实例真实类型不符时(Entity 实例 as Player),程序直接 fatal 退出(退出码 4),没有异常、没有提示——所以 as 必须配 is 守卫,上面的 if 写法是标准姿势。is 对整条继承链回答 true:C extends B extends A 时,C 的实例 is A、is B、is C 全为 true。
父类字段直接继承(22.1 的 p.baseValue);子类还可以声明同名字段——编译器不拦,行为是遮蔽:
class Base int value = 1 end class Child extends Base int value = 99 // 允许:遮蔽父类同名字段 end Child c = Child() println(c.value) // 99:子类字段赢
字段遮蔽编译器不拦,但两个 value 同时存在于实例里(父类构造照常维护父类那份),极易"改了这份读那份"——工程上视为坏味道,换个名字。另一个相关空白:xlang 没有 base.score() 式的"调用父类被覆盖方法"语法(拾遗 3)。
class A virtual func tag() as string return "A" end end class B extends A override func tag() as string return "B" end end class C extends B override func tag() as string return "C" // override 链接力:目标是 B 的 override,合法 end end A a = C() println(a.tag()) // C:隔两代照样派发到最终实现
构造与析构沿链有序:构造从祖父到孙(base create → child create),析构恰好相反——与 C++ 同款顺序。链的顶端还有一个特殊存在:object 是可选的根基类,extends object 让实例获得动态字段能力(e["hp"] = 100 式玩法),那是第 26 章的主题。
把不同子类装进同一个父类型数组,统一遍历、各自计算——多态最经典的应用:
class Shape virtual func area() as float return 0.0 end virtual func name() as string return "shape" end end class Circle extends Shape float r = 1.0 create(float r) self.r = r end override func area() as float return 3.14159 * self.r * self.r end override func name() as string return "circle" end end class Rect extends Shape float w = 1.0 float h = 1.0 create(float w, float h) self.w = w self.h = h end override func area() as float return self.w * self.h end override func name() as string return "rect" end end array<Shape> shapes = [] shapes.push(Circle(2.0)) shapes.push(Rect(3.0, 4.0)) float total = 0.0 for s in shapes println(s.name(), " ", s.area()) // circle 12.56636 / rect 12.0 total = total + s.area() end println(total) // 24.56636
遍历变量的静态类型是 Shape,每次 s.area() 却都命中正确的子类实现——新增一种图形只需再写一个 extends,遍历代码一行不改。这就是多态的工程价值:对扩展开放,对修改封闭。
用 xl cgen 观察 22.4 的 Entity/Player,生成的关键代码是:
含虚方法的 class 在结构体头部多一个虚表指针;每个虚方法占一个函数指针槽;构造时按真实类型装入对应虚表(Player 的槽里是指向子类实现的包装函数);调用点经虚表间接跳转。这就是"声明 virtual 才有运行期开销、非虚方法保持直接调用"的成本模型——virtual 是显式订阅动态派发。
C++ 里把子类对象按值赋给父类变量会"切片"(子类部分被砍掉)。xlang 没有这个病:class 实例是引用语义(第 21 章),Entity e = Player() 赋的是引用,堆上的 Player 实例完整无缺——所以 e.score() 能派发、e is Player 为 true、e as Player 能取回 hp。赋值、传参、装箱进 var,任何路径都不会丢失子类部分。"无切片"不是编译器做了特殊处理,而是引用语义的自然结果。
很多语言允许子类在 override 里调用父类版本(super.score())。xlang 的 base 关键字只服务于构造函数,base.score() 会报 [hir] typed HIR value expression is missing its static type——RC0 没有提供这条通道。需要"父类逻辑 + 子类增量"时的绕行:把父类逻辑抽成一个不同名的普通方法(如 baseScore()),子类的 override 直接调它。这是设计空白而非笔误,写代码时绕开即可。
以下报错全部来自真实编译器输出(文件路径已缩短)。
原因:xlang 不允许隐式覆盖——同名即冲突,必须声明意图。正确做法:冠以 override;若想说的是"新方法"而非覆盖,改名。
原因:只有 virtual 方法开放了改写许可。正确做法:设计父类 API 时预判扩展点,把允许改写的方法标 virtual;事后发现要改,回父类补 virtual。
原因:final 是父类作者的明确封印。正确做法:尊重封印,用组合(持有一个父类实例)而非继承来扩展行为。
原因:父类部分的初始化参数只能由子类转交,编译器不会猜。正确做法:子类 create 形参里带上父类需要的值,create 体内调 base(seed)。
原因:base 是构造链专用语法,不是"重新初始化父类"的开关。正确做法:重置逻辑写成父类的普通方法(如 baseReset()),子类方法调 self.baseReset()。
| 主题 | 规则 | 要点 |
|---|---|---|
| extends | 单继承;子类继承字段与方法 | 父类不存在报 [sem] 未知父类 |
| base(...) | 父类 create 带参则子类必须显式接力 | 只能在 create 体内;不必首句;按重载解析 |
| 三件套 | 覆盖必 override;目标必 virtual;final 封印 | 签名须精确;private/static 不参与;override 链可接力 |
| 动态派发 | 父类引用 + 虚方法 → 按动态类型选实现 | vtable 实现;字段与非虚方法按静态类型解析 |
| protected | 外部禁止,子类内部放行 | private=自己用,protected=家族用,public=大家用 |
| is/as | is 沿链回答 true;as 向下转型 | as 失败静默 fatal(退出码 4),必须先 is 守卫 |
| 字段 | 父类字段继承;同名遮蔽允许 | 遮蔽是坏味道;无 super 调用语法(RC0 空白) |
| 构造析构链 | 构造祖父→孙,析构孙→祖父 | 无切片:引用语义保住完整子类实例 |
编写 animals.xl:class Animal 带 virtual speak();Dog/Cat/Duck 各自 override。造一个 array<Animal> 混装三种动物,遍历合唱,输出每种动物的叫声。
参考思路:对照 22.9 的多态数组;speak() 返回 string 即可;体会"新增动物不改遍历代码"。
编写 users.xl:class User 有 create(string name);class Admin extends User 增加 level 字段,create(string name, int level) 里用 base(name) 接力,再写 virtual role() / override role() 让 User 答 "user"、Admin 答 "admin"。
参考思路:这是 22.2 与 22.4 的组合;注意 User 的 create 带参后,Admin 不调 base(name) 会得到坑 4 的第一条诊断——可以故意触发一次看报错。
编写 cast.xl:函数 func heal(Entity e)——若 e 是 Player 则把 hp 加满 100 并打印"已治疗",否则打印"无法治疗"。用 Player 实例与裸 Entity 实例各调一次。
参考思路:is 守卫 + as 转型(22.6 标准姿势);去掉 is 守卫直接 as,对裸 Entity 实例会得到静默 fatal——在注释里记录这条实测结论。