为 Lox 解释器添加继承:超类语法、方法查找链与 super 表达式的完整实现
发布时间:2026/10/5 2:17:47 锦皓数字建站

编程语言解释器编译器语言运行时教程【免费下载链接】craftinginterpretersRepository for the book Crafting Interpreters项目地址https://gitcode.com/gh_mirrors/cr/craftinginterpreters点击查看免费下载继承inheritance是对象导向语言的核心能力之一。在《Crafting Interpreters》一书的 Part II 收官章节中作者 Robert Nystrom 为纯 Java 实现的树遍历解释器 jlox 补齐了 Lox 语言的类继承从超类语法与语法树改造到findMethod()沿继承链递归查找方法再到利用环境链机制实现super表达式。本文以 book/inheritance.md 为主线逐一拆解这些设计决策并结合仓库中的真实源码java/com/craftinginterpreters/lox与测试用例test/inheritance、test/super进行验证读完本文你将能完整理解并复刻 jlox 的继承实现。背景Part II 的最后一块拼图在 classes 一章 中jlox 已经实现了类的声明、实例化、字段、方法和构造器但彼时的类之间彼此孤立缺少“复用”能力。这一章的目标就是补上 Lox 的类继承——继承机制最早可追溯到 Simula其设计者 Kristen Nygaard 与 Ole-Johan Dahl 发现仿真程序中不同类之间存在大量共性代码继承正是为“跨类复用代码”而生。在阅读本章之前建议读者先掌握三块前置知识词法作用域与变量解析见 resolving-and-binding.md、环境链Environment与闭包捕获见 closures.md、以及this的绑定方式见 classes.md。本章实现的关键点——把超类像变量一样放进环境链——正是对既有闭包机制的复用这也是 jlox 中“用最少的新机制完成新功能”的典型手法。超类与子类语法设计与语法树改造术语与设计取舍作者借 C. A. R. Hoare 提出的“subclass”一词梳理了术语Simula 用它指“继承自其他类的类”Smalltalk 反用拉丁前缀造出“superclass”指关系的另一侧C 语境下则常说“base/derived class”。在类型论中子类通常是超类的子类型subtype子类的实例集合是超类实例集合的子集。真正值得关注的是语法选择。C/C# 用冒号:Java 用extendsPython 用圆括号Simula 甚至把超类名放在class关键字之前。到本章为止jlox 的词法分析器并没有:或extends这些保留字作者也不愿意为了一个特性新增 token于是仿照 Ruby 采用了小于号class Doughnut { // General doughnut stuff... } class BostonCream Doughnut { // Boston Cream-specific stuff... }文法给 classDecl 增加可选子句之后的超类名是可选的——Lox 不像 Java 那样存在根类Object省略超类子句意味着该类没有任何超类而非隐式继承某个根类。文法如下classDecl → class IDENTIFIER ( IDENTIFIER )? { function* } ;AST 节点超类名为什么用 Expr.Variable 而非 Token与直觉相反Stmt.Class中存储超类的字段类型是Expr.Variable而不是Token见 Stmt.java#L35-L52static class Class extends Stmt { Class(Token name, Expr.Variable superclass, ListStmt.Function methods) { this.name name; this.superclass superclass; this.methods methods; } final Token name; final Expr.Variable superclass; final ListStmt.Function methods; }原因在于文法虽然把超类子句限制为单个标识符但运行时该标识符会被当作一次变量访问来求值。在解析阶段就把它包进Expr.Variable解析器Resolver就能在后续的变量解析过程中把“跳转距离”挂到这个节点上——这与普通变量访问共用同一套解析基础设施。解析器直接翻译文法Parser.java 的 classDeclaration() 几乎逐行对应文法读完类名后若match(LESS)命中就consume(IDENTIFIER, Expect superclass name.)并构造Expr.Variable超类子句缺省时superclass保持null后续所有环节都必须显式判空private Stmt classDeclaration() { Token name consume(IDENTIFIER, Expect class name.); Expr.Variable superclass null; if (match(LESS)) { consume(IDENTIFIER, Expect superclass name.); superclass new Expr.Variable(previous()); } consume(LEFT_BRACE, Expect { before class body.); ListStmt.Function methods new ArrayList(); while (!check(RIGHT_BRACE) !isAtEnd()) { methods.add(function(method)); } consume(RIGHT_BRACE, Expect } after class body.); return new Stmt.Class(name, superclass, methods); }解析器解析超类引用并拦截自继承Resolver.visitClassStmt() 中新增了两处逻辑。其一是解析超类表达式本身虽然类通常在顶层声明、超类名多为全局变量此时解析往往“无事可做”但 Lox 允许在块内声明类因此超类名可能指向局部变量必须保证它被正确解析if (stmt.superclass ! null) { currentClass ClassType.SUBCLASS; resolve(stmt.superclass); }其二是一个必须静态拦截的边界情况——类不能继承自身class Oops Oops {}若放任其运行会破坏解释器对“继承链无环”的隐含假设。作者选择在解析阶段直接报错if (stmt.superclass ! null stmt.name.lexeme.equals(stmt.superclass.name.lexeme)) { Lox.error(stmt.superclass.name, A class cant inherit from itself.); }对应测试 test/class/inherit_self.lox 专门覆盖了这一场景。解释器运行时校验“超类必须是类”AST 经过解析后进入求值阶段。Interpreter.visitClassStmt() 首先对超类表达式求值由于求值结果可能是任意对象必须做运行时类型检查var NotAClass I am totally not a class; class Subclass NotAClass {} // ?!Object superclass null; if (stmt.superclass ! null) { superclass evaluate(stmt.superclass); if (!(superclass instanceof LoxClass)) { throw new RuntimeError(stmt.superclass.name, Superclass must be a class.); } }该行为有明确的测试证据test/inheritance/inherit_from_nil.lox 期望输出Superclass must be a class.运行时错误同目录下的 inherit_from_number.lox 与 inherit_from_function.lox 分别验证了把数字、函数当作超类时同样会被拒绝。检查通过后超类被传入LoxClass构造函数。先通过environment.define(stmt.name.lexeme, null)预声明类名这是 Lox 支持类内部引用自身/递归的关键步骤再在 LoxClass.java#L29-L36 中把超类存入superclass字段final LoxClass superclass; // LoxClass.java#L14-L16 LoxClass(String name, LoxClass superclass, MapString, LoxFunction methods) { this.superclass superclass; this.name name; this.methods methods; }至此“子类拥有超类”这一关系已经建立接下来要回答的问题是拥有超类之后类能获得什么能力继承方法三行 Java 代码的查找链动态类型语言对继承的要求比静态类型语言宽松得多不必保证子类型关系与内存布局核心语义只有一条——能在超类实例上调用的方法也能在子类实例上调用即方法沿继承链向上查找。实现它只需要改动LoxClass.findMethod()一处LoxClass.java#L38-L52LoxFunction findMethod(String name) { if (methods.containsKey(name)) { return methods.get(name); } if (superclass ! null) { return superclass.findMethod(name); } return null; }逻辑一目了然先在当前类的方法表中查找找不到就递归到超类。由于超类本身也是LoxClass只要继承链无环递归必然终止。配合 test/inheritance/inherit_methods.lox 可以完整验证三种行为class Foo { methodOnFoo() { print foo; } override() { print foo; } } class Bar Foo { methodOnBar() { print bar; } override() { print bar; } } var bar Bar(); bar.methodOnFoo(); // expect: foo bar.methodOnBar(); // expect: bar bar.override(); // expect: barmethodOnFoo()在Bar自身找不到便沿着超类链在Foo中找到继承methodOnBar()是子类独有方法override()在两个类中都存在查找从继承链底端开始、子类优先因此打印bar覆盖。test/inheritance/set_fields_from_base_class.lox 和 test/inheritance/constructor.lox 则分别验证了字段与构造器在继承场景下的行为。作者幽默地指出仅这三行代码就完成了本章一半的继承特性。调用超类方法super 表达式的语法与语义为什么需要 superfindMethod()从继承链底端向上查找子类方法覆盖override超类同名方法——类似内层作用域变量遮蔽外层变量。如果子类想彻底替换超类行为覆盖机制已经足够但实践中更常见的是精炼refine在超类行为之外追加子类特有的逻辑。由于同名方法已被覆盖子类方法若按名字调用只会递归地命中自己的覆盖版本无法触达原始实现。因此需要一种语法表达“忽略我的覆盖直接从我的超类上查找这个方法”Java 用superLox 沿用同一关键字class Doughnut { cook() { print Fry until golden brown.; } } class BostonCream Doughnut { cook() { super.cook(); print Pipe full of custard and coat with chocolate.; } } BostonCream().cook();运行输出可在 test/super/call_same_method.lox 中看到同样结构的期望输出Fry until golden brown. Pipe full of custard and coat with chocolate.语法super 表达式不可独立存在与this不同this像一个魔法变量、是孤零零的单个 token而super后续的.与属性名是表达式的不可分割部分裸写print super;是语法错误。文法上新的子句加入primary规则把属性访问一并包含primary → true | false | nil | this | NUMBER | STRING | IDENTIFIER | ( expression ) | super . IDENTIFIER ;同时和普通方法一样参数列表不属于表达式本身——super.cook()是一个super访问接一个函数调用因此你可以先取到方法句柄再单独调用var method super.cook; method();对应的 AST 节点只包含super关键字与要查找的方法名Expr.java#L155-L170static class Super extends Expr { Super(Token keyword, Token method) { ... } final Token keyword; final Token method; }解析代码位于 Parser.java 的 primary() 内命中super后依次消费.与方法名if (match(SUPER)) { Token keyword previous(); consume(DOT, Expect . after super.); Token method consume(IDENTIFIER, Expect superclass method name.); return new Expr.Super(keyword, method); }语义从“哪个超类”开始查找最直观的答案是“this的超类”但它恰好只在一部分场景下正确。关键在于——查找起点应是包含该super表达式的那个类的超类而非调用对象的超类。作者给出了经典的反例class A { method() { print A method; } } class B A { method() { print B method; } test() { super.method(); } } class C B {} C().test();程序运行到test()内部时this是C的实例C的超类是B但如果从这里开始查找会命中B.method()打印B method。而 Java/C#/C 乃至 Lox 期望的结果都是A method。正确起点是定义test()的类B的超类A因为test()定义在B内部其中的super表达式应从B的超类A开始查找。这带来一个实现难题求值super表达式时解释器手头并没有“当前方法所属的类”这一信息。一个直白但笨重的方案是给LoxFunction加字段保存所属LoxClass再维护“当前正在执行的函数”指针——管道冗长。作者借鉴上一章绑定this的思路改用环境与闭包机制但有一处关键差异this在方法每次被访问时绑定同一方法可被不同实例调用各自需要独立的this而super的超类是类声明本身的固定属性每次求值super表达式结果都相同。因此可以“在类定义执行的那一刻”一次性创建超类环境在执行类声明、定义各方法之前新建一个环境并把该类的超类绑定到名字super上随后创建的每个LoxFunction都会把这个环境捕获进闭包方法被调用并绑定this时超类环境就成为方法环境的父级环境链如下解析器侧为 super 建立作用域Resolver.visitClassStmt() 中若类声明带超类则在解析其所有方法之前压入一个新作用域并把super标记为已定义方法解析完毕后弹出该作用域Resolver.java#L125-L127if (stmt.superclass ! null) { beginScope(); scopes.peek().put(super, true); } // ...解析 methods... if (stmt.superclass ! null) endScope();只有确实存在超类时才创建该作用域这是个小优化——没有超类就没有可存放的对象。有了作用域visitSuperExpr() 把super当作普通变量来解析通过resolveLocal(expr, expr.keyword)记录环境链跳数Override public Void visitSuperExpr(Expr.Super expr) { if (currentClass ClassType.NONE) { Lox.error(expr.keyword, Cant use super outside of a class.); } else if (currentClass ! ClassType.SUBCLASS) { Lox.error(expr.keyword, Cant use super in a class with no superclass.); } resolveLocal(expr, expr.keyword); return null; }解释器侧运行时创建超类环境求值子类声明时Interpreter.java#L128-L160解释器新建一个以当前环境为父级的环境把真正的超类LoxClass对象绑定到super名随后创建的每个方法LoxFunction都会捕获当前环境作为闭包从而“携带”超类方法创建完毕再弹出该环境if (stmt.superclass ! null) { environment new Environment(environment); environment.define(super, superclass); } // ...为每个方法创建 LoxFunction... LoxClass klass new LoxClass(stmt.name.lexeme, (LoxClass)superclass, methods); if (superclass ! null) { environment environment.enclosing; }求值 super 表达式一次查找、绑定 this、调用Interpreter.visitSuperExpr() 分三步完成取出超类根据解析器记录的跳数distance从环境中取得super绑定的LoxClass。取出thissuper.cook中实例必须是当前对象this——即便方法在超类上查找实例仍是this。麻烦在于super表达式节点上没有可供解析器挂接this跳数的位置幸好环境链布局由解释器自己控制绑定this的环境总是紧贴在绑定super的环境内侧所以把距离减一即可int distance locals.get(expr); LoxClass superclass (LoxClass)environment.getAt( distance, super); LoxInstance object (LoxInstance)environment.getAt( distance - 1, this);作者坦言这段代码不算优雅但“写一本书就要展示每一行代码没法把 hack 藏进‘留给读者的练习’”。查找并绑定方法与普通 get 表达式几乎一致唯一区别是findMethod()作用于超类而非this的类找不到时抛出Undefined property运行时错误最后method.bind(object)把this绑定进方法闭包LoxFunction.bind()返回可调用的绑定方法LoxFunction method superclass.findMethod(expr.method.lexeme); if (method null) { throw new RuntimeError(expr.method, Undefined property expr.method.lexeme .); } return method.bind(object);test/super 目录下近二十个测试全面覆盖了super的各种合法与非法场景例如 call_same_method.lox子类覆盖后经super调回超类、call_other_method.lox调用超类中非同名的方法、this_in_superclass_method.lox超类方法内使用this等。拦截 super 的非法使用把错误提前到静态阶段Lox 虽是动态类型语言但“尽早帮用户发现错误”仍值得做。以下两类super用法按当前实现会在运行时崩溃——例如无超类的类中使用super时运行时假定super已被解析成功并能在环境中找到但根本没有包围它的超类环境JVM 会抛出异常击穿解释器class Eclair { cook() { super.cook(); print Pipe full of crème pâtissière.; } }更简单的病态用法super.notEvenInAClass();这两者都能仅凭源码静态判定。做法是在 Resolver 中扩展类类型枚举新增SUBCLASS区分“带超类的类”与“不带超类的类”Resolver.java#L38-L47private enum ClassType { NONE, CLASS, SUBCLASS }解析类声明时若该类是子类则置为SUBCLASSResolver.java#L89-L91解析super表达式时检查当前类类型越界即报错Resolver.java#L282-L291if (currentClass ClassType.NONE) { Lox.error(expr.keyword, Cant use super outside of a class.); } else if (currentClass ! ClassType.SUBCLASS) { Lox.error(expr.keyword, Cant use super in a class with no superclass.); }这一小段错误处理也是完成 jlox 的最后一块代码。结语Part II 的里程碑至此纯 Java 实现的 Lox 解释器jlox宣告完成。回望整个 Part II从扫描器token 与词法分析、抽象语法树、递归下降解析器、前缀/中缀表达式、对象的运行时表示、基于 Visitor 模式的求值、词法作用域与环境链、控制流、带参数的函数、闭包、静态变量解析与错误检测到类、构造器、字段、方法最终以继承收尾——全部从零实现零外部依赖只靠 JDK 标准库的少量集合类与 JVM 运行时。在仓库中可以直观对照这套实现的完整脉络Scanner.java、Parser.java、Resolver.java、Interpreter.java以及支撑对象模型的 LoxClass.java、LoxInstance.java、LoxFunction.java。阅读 java/com/craftinginterpreters/tool/GenerateAst.java 还能看到Expr.Super、Stmt.Class等节点如何自动生成。Part II 结束并非全书终点——Part III 将用 C 语言实现一个字节码虚拟机 clox。休息片刻、用你的 jlox 写几个有趣的 Lox 程序后就可以踏上下一段冒险了。延伸思考三类挑战本章末尾给出了三道开放挑战恰好可以检验对继承语义的理解深度更自由的代码复用机制Lox 只支持单继承这是唯一的跨类复用手段。若要在 mixins、traits、多继承、虚继承、扩展方法等方案中为 Lox 挑选一种并落地实现你会选哪种、为什么BETA 的inner反向语义Lox以及大多数 OO 语言从继承链底端向上查找、子类优先用super触达超类方法而 BETA 语言从顶端向下查找、超类优先超类方法通过inner调用链上下一个子类方法由超类控制子类何时何地精炼行为。若把 Lox 的覆盖与super换成 BETA 语义即调用方法时优先继承链上最高的方法、inner在最近的子类中查找同名方法没有则什么都不做试实现之class Doughnut { cook() { print Fry until golden brown.; inner(); print Place in a nice box.; } } class BostonCream Doughnut { cook() { print Pipe full of custard and coat with chocolate.; } } BostonCream().cook();期望输出为Fry until golden brown.→Pipe full of custard and coat with chocolate.→Place in a nice box.。补上你心心念念的语言特性在 the-lox-language.md 的挑战中你曾设想 Lox 缺少的特性如今你已掌握解释器构建的全流程不妨真正实现其中一个。赞分享编程语言解释器编译器语言运行时教程【免费下载链接】craftinginterpretersRepository for the book Crafting Interpreters项目地址https://gitcode.com/gh_mirrors/cr/craftinginterpreters点击查看免费下载相关推荐现代JavaScript继承完全指南掌握extends与super的终极技巧现代JavaScript继承完全指南掌握extends与super的终极技巧 现代JavaScriptES6及以上引入了类class语法让面向对象编Traceur Compiler类继承编译原型链与super关键字实现Traceur Compiler类继承编译原型链与super关键字实现 编译原理概述 Traceur CompilerTC通过将ES6类语法转换为ES5编译器开发工具Slang 编译器 AST 表达式参考从 Expr 继承体系到解析与类型检查的完整指南Slang 编译器 AST 表达式参考从 Expr 继承体系到解析与类型检查的完整指南 本篇技术指南以 Slang 开源编译器仓库中的表达式 AST抽象语法编译器图形学编程语言上一篇抖音批量下载神器一键保存创作者全作品告别手动烦恼下一篇CANN ops-nn 中 aclnnSwishBackward 算子接口详解Swish 激活函数反向传播的 NPU 实现与调用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。