前言
面向对象在前文已经谈过,虽然Lua是一个脚本语言,本就不是为了面向对象而设计的。但是它的元方法可以非常优美的让我们设计出类之间的继承关系,单继承、多继承等。
本文就是简单的剖析一下,云风大佬早在十多年前对于Lua面向对象的设计。
继承的形式
- 通过元表指向父类,每次查找不存在的字段时,层层向上查找。
- 通过元表指向父类,每次查找就clone父类的所有字段到当前元表中。
- 通过元表指向父类,当查找不存在的字段时,将该字段clone到当前元表,下一次就可以直接返回。
基类的构造
这里采用的是第三种形式,参考云风大佬的源码如下:
#!/usr/local/bin/lua
local _class = {}
function class(super)
local class_type = {}
class_type.ctor = false
class_type.super = super
class_type.new = function(...)
local obj = {}
do
local create
create = function(cls, ...)
if cls.super then
create(cls.super, ...)
end
if cls.ctor then
cls.ctor(obj, ...)
end
end
create(class_type, ...)
end
setmetatable(obj, { __index = _class[class_type] })
return obj
end
-------------------------------------------------------------------------------------
local vtbl = {}
_class[class_type] = vtbl
setmetatable(class_type, { __newindex = function(t, k, v) vtbl[k] = v end })
if super then
setmetatable(vtbl, { __index = function(t, k)
local ret = _class[super][k]
vtbl[k] = ret
return ret
end})
end
return class_type
end
源码剖析
首先简述一下每个变量结构的大体含义:
_class
:全局表,用于存储不同类的内存指针,指向vtbl
。super
:当前类的继承父类。class_type
:最终构造出的类,设置了ctor
、super
、new
,以及元表的__newindex
属性。ctor
:构造函数,默认false
。super
:本类的父类。new
:实例化函数,用于实例化对象。obj
:实例化的对象的表,会设置该表的元表的__index
属性,为全局表_class
索引的本类所对应的表vtbl
。create
:默认有父类就先构造父类的原则,由上而下的构造ctor
。vtbl
:(Virtual Table)本类用于在全局表_class
索引的表,用于存储新增的属性、方法等(见第3点)。
还有没谈到的最重要的一点,也就是最后的一个代码段if super then
:
- 这里就是设置
vtbl
的元表,其元表的__index
索引的是一个function
,当字段找不到时,并且super
存在时,去拿到存储在全局表_class
中super
索引的表中该字段对应的值,并且取出后存储在本类的vtbl
表中。
使用范例
#!/usr/local/bin/lua
require "OO" -- OO.lua 就是上面的那个基类
local People = class()
function People:ctor(who, name, age)
print("class People ctor")
self.who = who
self.name = name
self.age = age
end
function People:print_info()
print(self.who, self.name, self.age)
end
function People:say()
print(string.format("%s say hello", self.who))
end
-----------------------------------------------------------------
local Student = class(People)
function Student:ctor(who, name, age)
print("class stu ctor")
self.who, self.name, self.age = who, name, age
end
function Student:say()
print(string.format("%s say 'HAPPY'", self.who))
end
people = People.new("people", "Cauchy", 19)
people:say()
people:print_info()
print()
student = Student.new("student", "AQ_Scott", 20)
student:say()
student:print_info()
输出:
class People ctor
people say hello
people Cauchy 19
class People ctor
class stu ctor
student say 'HAPPY'
student AQ_Scott 20
分析:
- local People = class(),返回 class_type{},设置 class_type{} 的元表的
__newindex
属性,一个function
去对应的 vtbl{} 中更新值,vtbl{} 相当于存放了 People 的字段和方法,并且全局表 _class{} 记录 class_type{} 作为索引指向的其 vtbl{}。 - people = People.new(…) 实例化的时候,返回 obj{},
create
去构造 People的父类,由于 People 不存在super
,我们设置了 People 的ctor
方法,所以调用该方法。注意 这里是 cls.ctor去调用,第一个参数是 obj{},而People:ctor()
,所以 obj{} 作为self
传入,设置好了对应的...
参数。并且 obj{} 的元表的__index
索引到全局表 _class[People],即为 vtbl{}。而 vtbl{} 存放了 People 的字段方法,所以 people 可以获取到 People 的字段和方法。
-
local Student = class(People),也是同理创建,不过多了
super
属性为People。Student 的元表的__newindex
对自己对应的 vtbl{} 中找字段,如果不存在,会去 vtbl{} 的元表的__index
查找,即去 People 的 vtbl{} 中取值,同时值赋给 Student 的 vtbl{}。 -
Student 实例化
new
出来的对象 student,元表的__index
指向全局表 _class[Student],即为 vtbl{}。
从输出可以看到,在实例化 student 的时候,先调用了 People 的构造函数。print_info 方法也能够正常执行,并且 say 方法也成功重写了。
还存在的疑惑:
实例化多个对象的时候,用的基类的表 vtbl{} 不是同一个吗?
update:2023年5月10日
所有通过class函数定义的类都会共享一个vtbl表。这个表用于存储每个类的成员变量和成员方法。当我们创建一个类的实例时,它的元表会被设置为对应类的vtbl表。因此,所有该类的实例都会共享同一个vtbl表。
在实践中,这并不会造成问题,因为每个类的成员变量和成员方法都是独立的,并且每个实例都有自己的成员变量和成员方法。虽然它们共享同一个vtbl表,但是通过元表机制,每个实例都能够正确地访问到自己的成员变量和成员方法。
如果需要为不同的对象分别存储数据,可以将数据存储在实例本身的属性中,而不是存储在vtbl表中。
总结:
- 这个框架提供了一个
class
函数,用于定义类和实现继承,同时也提供了一个new
方法,用于创建类的实例。 - 在这段代码中,
_class
是一个全局表,用于存储所有已定义的类及其对应的虚拟函数表(vtbl
)。class
函数接收一个参数super
,表示父类。如果没有父类,则super
为nil
。 - 首先,在
class_type
表中定义了三个属性:ctor
、super
和new
。其中,ctor
表示构造函数,super
表示父类,new
则是用于创建类实例的方法。new
方法通过递归调用父类的构造函数来完成继承,并调用当前类的构造函数进行初始化。最后,使用setmetatable
将实例的元表设置为当前类的虚拟函数表,以便实现成员变量和成员方法的访问。 - 接下来,创建了一个名为
vtbl
的空表,并将其关联到当前类上。同时,通过设置元表,实现了继承功能。如果当前类有父类,那么在访问当前类的成员变量和成员方法时,会先从父类中查找,如果父类中存在相应的成员,则将其复制到当前类的虚拟函数表中,并返回该成员的值。 - 最后,
class
函数返回当前类class_type
,以便在创建类实例时使用。
这段代码实现了一个简单的面向对象编程框架,可以用于定义类、实现继承和创建类实例。它的实现原理是通过Lua的元表机制来实现成员变量和成员方法的访问,以及继承和多态性的实现。