第7章 类(Classes: Object-Oriented Programming)
一、导读
面向对象编程通过类和对象来组织代码,提供封装、继承和多态等特性。本章介绍如何实现 Lox 语言的类系统,包括类的定义、对象的创建、方法的调用以及 this 关键字的处理,完成树遍历解释器部分的最后一个重要特性。
学习目标:掌握类和对象的实现机制,理解方法调用和 this 绑定的原理,学会实现继承和方法查找链,能够处理初始化方法和对象生命周期。
重要性:面向对象编程是现代软件开发中最主流的编程范式之一。理解类和对象的底层实现,能帮助你更好地使用面向对象语言,设计更合理的类层次结构,避免常见的面向对象设计陷阱。
核心问题:类定义时应该存储哪些信息?对象实例如何创建和管理?方法调用时 this 如何绑定到正确的对象?继承链上的方法查找如何工作?super 关键字如何访问父类方法?
二、核心概念详解
2.1 类定义的语法解析
类定义包含类名、可选的超类(superclass)和方法列表。解析器将其解析为类声明节点,解释器则创建类对象(LoxClass),存储类名、方法映射和超类引用。方法在类定义时被解析为函数对象,存储在类的方法表(Map<String, LoxFunction>)中。如果类有超类,则超类引用也会被保存在类对象中,用于后续的方法查找和继承。
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() 如果存在,会在对象创建后自动调用。
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 总是指向调用该方法的对象实例。
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 关键字提供了直接访问父类方法的能力,它创建一个特殊的查找起点,从父类的方法表开始搜索。
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 案例一:实现简单的图形类层次
使用类和继承实现一个图形绘制系统:
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 244.2 案例二:用闭包模拟对象
在没有类的语言中,可以用闭包模拟对象的行为:
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 的绑定过程:
class A {
method() { return this; }
}
class B < A {
method() { return super.method(); }
}
var b = B();
print b.method() == b;