7手工实现解释器

类

面向对象

类对象继承this

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第7章 类(Classes: Object-Oriented Programming)

一、导读

面向对象编程通过类和对象来组织代码,提供封装、继承和多态等特性。本章介绍如何实现 Lox 语言的类系统,包括类的定义、对象的创建、方法的调用以及 this 关键字的处理,完成树遍历解释器部分的最后一个重要特性。

学习目标:掌握类和对象的实现机制,理解方法调用和 this 绑定的原理,学会实现继承和方法查找链,能够处理初始化方法和对象生命周期。

重要性:面向对象编程是现代软件开发中最主流的编程范式之一。理解类和对象的底层实现,能帮助你更好地使用面向对象语言,设计更合理的类层次结构,避免常见的面向对象设计陷阱。

核心问题:类定义时应该存储哪些信息?对象实例如何创建和管理?方法调用时 this 如何绑定到正确的对象?继承链上的方法查找如何工作?super 关键字如何访问父类方法?

二、核心概念详解

2.1 类定义的语法解析

类定义包含类名、可选的超类(superclass)和方法列表。解析器将其解析为类声明节点,解释器则创建类对象(LoxClass),存储类名、方法映射和超类引用。方法在类定义时被解析为函数对象,存储在类的方法表(Map<String, LoxFunction>)中。如果类有超类,则超类引用也会被保存在类对象中,用于后续的方法查找和继承。

lox
class Breakfast {
  cook() {
    print "Eggs a-fryin'!";
  }
  serve(who) {
    print "Enjoy your breakfast, " + who + ".";
  }
}

2.2 对象实例的创建机制

对象(Instance)是类的实例,包含一组字段(field)存储状态。Lox 允许在对象创建后动态添加字段(类似 JavaScript),字段存储在对象的 Map<String, Object> 中。通过 ClassName() 调用类来创建新对象(类似 Java 的 new),类对象在接收到调用时创建并返回一个新的实例。初始化方法 init() 如果存在,会在对象创建后自动调用。

lox
class Cake {
  init(flavor) {
    this.flavor = flavor;
  }
  describe() {
    print "I am a " + this.flavor + " cake.";
  }
}

var cake = Cake("chocolate");
cake.describe();  // 输出: I am a chocolate cake.

2.3 方法调用的绑定过程

方法是与类关联的函数,调用时需要将对象作为 this 参数传递。当通过 object.method(args) 语法访问方法时,解释器首先在对象的类中查找对应名称的方法。如果找到,将方法包装为一个 LoxInstance 绑定的函数(bind),在调用时自动将 this 设置为该对象。这种绑定机制确保了方法体内的 this 总是指向调用该方法的对象实例。

java
Object get(String name) {
  if (fields.containsKey(name)) {
    return fields.get(name);
  }
  LoxFunction method = klass.findMethod(name);
  if (method != null) return method.bind(instance);
  throw new RuntimeError("Undefined property '" + name + "'.");
}

2.4 继承的实现策略

子类通过 < 语法继承父类(Lox 使用 < 而非 extends)。子类可以访问父类的所有方法,也可以覆盖(override)父类的方法。当调用子类对象的方法时,解释器先在子类的方法表中查找,找不到则沿着继承链向上搜索到父类。super 关键字提供了直接访问父类方法的能力,它创建一个特殊的查找起点,从父类的方法表开始搜索。

lox
class Animal {
  speak() {
    print "Some sound";
  }
}

class Dog < Animal {
  speak() {
    print "Woof!";
  }
}

class Puppy < Dog {
  speak() {
    super.speak();  // 调用 Dog 的 speak
    print "(tiny bark)";
  }
}

2.5 this 关键字的语义

this 关键字在方法体内指向调用该方法的对象实例。在实现上,this 被当作一个隐式的参数传递给方法。当方法被绑定到对象时(通过 bind 操作),this 被设置为该对象的引用。在方法体内,通过 this 可以访问和修改对象的字段,调用对象的其他方法。this 的值在运行时确定,取决于哪个对象调用了方法,而非方法定义在哪个类中。

三、深入分析

3.1 方法查找的性能问题

简单的方法查找需要沿着继承链逐级搜索,在最坏情况下(深层继承、方法在根类中)需要遍历整个继承链。实际的语言实现中,通常通过方法缓存(Method Cache)来优化:将已经查找过的方法名和对应的函数对象缓存起来,下次查找同名方法时直接返回缓存结果。Python 的 CPython 实现和 Ruby 的 YARV 虚拟机都使用了类似的技术。

3.2 字段与方法的命名冲突

当对象同时存在同名字段和方法时,需要明确的优先级规则。Lox 的实现中,字段优先于方法:如果对象有一个名为 foo 的字段,那么 object.foo 返回字段的值,而非方法。这种设计简单直观,但在某些情况下可能导致意外的遮蔽。其他语言有不同的策略,如 Python 中字段和方法共享命名空间但方法优先。

3.3 类的元编程能力

Lox 的类系统相对简单,但通过动态字段添加和函数作为一等公民,可以实现一些元编程模式。例如,可以在运行时动态给类添加方法(通过修改类的方法表),实现类似 mixin 的功能。在生产级语言中,元类(Metaclass)、反射(Reflection)和注解(Annotation)等机制提供了更强大的元编程能力。

四、实践案例

4.1 案例一:实现简单的图形类层次

使用类和继承实现一个图形绘制系统:

lox
class Shape {
  init(name) {
    this.name = name;
  }
  area() {
    print "Unknown area for " + this.name;
    return 0;
  }
  describe() {
    print this.name + " with area " + this.area();
  }
}

class Circle < Shape {
  init(radius) {
    super.init("Circle");
    this.radius = radius;
  }
  area() {
    return 3.14159 * this.radius * this.radius;
  }
}

class Rectangle < Shape {
  init(width, height) {
    super.init("Rectangle");
    this.width = width;
    this.height = height;
  }
  area() {
    return this.width * this.height;
  }
}

var circle = Circle(5);
circle.describe();  // 输出: Circle with area 78.53975

var rect = Rectangle(4, 6);
rect.describe();  // 输出: Rectangle with area 24

4.2 案例二:用闭包模拟对象

在没有类的语言中,可以用闭包模拟对象的行为:

lox
fun createPoint(x, y) {
  fun getX() { return x; }
  fun getY() { return y; }
  fun setX(newX) { x = newX; }
  fun setY(newY) { y = newY; }
  fun toString() {
    return "(" + x + ", " + y + ")";
  }
  return [getX, getY, setX, setY, toString];
}

var point = createPoint(1, 2);
var getX = point[0];
var setX = point[2];
var toString = point[4];

print toString();  // 输出: (1, 2)
setX(10);
print toString();  // 输出: (10, 2)

这个例子展示了闭包如何封装私有状态,模拟对象的封装特性。

五、常见误区

5.1 误区一:this 是在定义时确定的

this 的值是在方法调用时确定的,而非定义时。同一个方法被不同对象调用时,this 指向不同的对象。这种"动态绑定"是多态的基础。如果 this 在定义时确定,那么继承和方法覆盖将失去意义。

5.2 误区二:子类自动继承父类的字段

在 Lox 中,继承只涉及方法的继承,字段是动态添加在对象实例上的。父类的 init() 方法不会被自动调用,子类需要显式通过 super.init() 来初始化继承的字段。这与 Java 等语言自动调用父类构造器的行为不同。

5.3 误区三:类和对象是同一回事

类是对象的模板(Blueprint),定义了对象的结构和行为;对象是类的实例(Instance),拥有具体的状态。类本身不存储字段值,字段值存储在对象实例中。多个对象可以共享同一个类,但各自拥有独立的字段值。

5.4 误区四:super 调用的是最近父类的方法

super 的查找起点是调用 super 的方法所在类的直接父类,而非当前对象的实际类型的父类。这意味着即使在子类覆盖的方法中调用 super,super 的查找起点仍然是该方法定义所在类的父类,而非子类链上的更高层。

5.5 误区五:面向对象必须有多态和继承

Lox 的类系统虽然包含了继承和多态,但面向对象编程的核心是封装(Encapsulation)和消息传递(Message Passing)。Smalltalk 风格的纯面向对象语言中,一切皆对象,通过消息传递来交互。理解面向对象的本质有助于在不同场景中选择合适的设计模式。

六、本章小结

本章讲解了面向对象特性的实现方法。类定义了对象的结构和行为,存储方法映射和继承关系。对象是类的实例,动态存储字段值。方法调用通过 this 绑定到调用对象,实现了动态分派。继承通过方法查找链实现,子类可以覆盖父类方法,super 关键字提供了访问父类方法的能力。这些特性使 Lox 语言具备了完整的面向对象编程能力,完成了树遍历解释器的核心功能。

七、思考题

请为 Lox 的类系统添加静态方法(static method)的支持,静态方法不需要 this 参数,直接通过类名调用。

如果允许多重继承(一个类可以有多个父类),方法查找会产生什么问题?请设计一种解决方案(如 C3 线性化算法)。

比较 Lox 的动态字段添加与 Java 的固定字段声明,分析各自的优缺点。

请实现一个 instanceof 运算符,判断一个对象是否是某个类(或其子类)的实例。

分析以下代码的执行结果,并解释 this 的绑定过程:

lox
class A {
  method() { return this; }
}
class B < A {
  method() { return super.method(); }
}
var b = B();
print b.method() == b;
7/9类
0%