Lua面向对象编程
在lua原生语法特性中是不具备面向对象设计的特性。因此,要想在lua上像其他高级语言一样使用面向对象的设计方法有以下两种选择:一种是使用原生的元表(metatable)来模拟面向对象设计,另外一种则是使用第三方框架LuaScriptoCore来实现。下面将逐一讲解这两种方式的实现过程(以下内容将基于Lua 5.3版本进行阐述)。
1. 元表方式
1.1 关于元表(metatable)
在lua中每种类型变量都可以有一个元表,而元表实际上是一个table
,它用于定义原始值在特定操作下的行为。如果想改变一个变量在特定操作下的行为,则可以在它的元表中设置对应元方法(metamethod)。换种说法,元表就是一个变量钩子,用来钩取变量的底层处理方法(即元方法),然后改写这些方法的处理行为。
其中元方法如下面表格所示:
元方法 | 说明 |
---|---|
__index | 当访问变量某个key时,如果没有对应的value,则会访问元表__index 元方法所指定的对象。如果指定值为table 类型,则会访问该table 的key所对应的值;如果指定值为function 类型,则该方法返回值作为对应key的值 |
__newindex | 当设置变量的某个key时,如果没有对应的key,则会访问元表__newindex 元方法来处理键值设置 |
__add | 当两个变量进行加法操作时触发,如:var1 + var2 |
__sub | 当两个变量进行减法操作时触发,如:var1 - var2 |
__mul | 当两个变量进行乘法操作时触发,如:var1 * var2 |
__div | 当两个变量进行除法操作时触发,如:var1 / var2 |
__mod | 当两个变量进行取模操作时触发,如:var1 % var2 |
__unm | 当变量进行取反操作时触发,如:~var |
__pow | 当变量进行幂操作时触发,如:var^2 |
__concat | 当两个变量进行连接时触发,如:var1 .. var2 |
__eq | 当两个变量判断是否相等时触发,如:var1 == var2 |
__lt | 当一个变量判断是否小于另一个变量时触发,如:var1 < var2 |
__le | 当一个变量判断是否小于或等于另一个变量时触发,如: var1 <= var2` |
__call | 当变量被用作方法调用时触发,一般来说function 类型是允许被调用的,对于其他类型默认是不能进行调用的,那么该元方法的作用就是让你的变量能够像function 一样被调用 |
__tostring | 当使用tostring 转换变量为字符串或者调用print 进行打印时触发,如:local t = {}; print (t); |
__gc | 当变量被回收时触发。 |
__mode | 当设置table 为弱引用table 时使用。弱引用table 可以让保存的key或者value为弱引用状态,方便GC标记和回收(如果非弱引用情况下,必须要table被回收时,其内部的key和value才允许GC回收)。 |
元表的设置基本分为三个步骤:
- 先创建一个作为元表的
table
- 设置需要实现的元方法。
- 使用
setmetatable
方法将元表绑定到变量中。
实现代码如下:
-- 创建元表并设置元方法
local mt = {};
mt.__index = function (table, key)
return "Hello Metatable!";
end
-- 创建实例并绑定元表
local t = {};
setmetatable(t, mt);
上面的元方法在本篇文章中不会一一细说,在后面的章节会针对面向对象需要使用的__index
、__newindex
、__call
、__gc
、__tostring
这几个元方法进行举例说明。
为了帮助大家理解如何实现lua的面向对象,下面的章节会逐步地构建面向对象所需要的特性,完整地演示整个演化过程。废话不多说,直接开干~
1.2 类型声明
在开始构建类型前,我们先为面向对象设想一些基本的规则,这样可以避免后面参与扩展和开发的人因为理解的不一样,导致整个结构的规则混乱和不一致。根据需要我们先设定如下几点:
- 类型名称首字母必须大写
- 类型必须为全局的变量
- 类型必须使用
__index
元方法指向自身 - 类型必须使用
__gc
元方法进行销毁时的操作 - 类型的属性必须使用点语法进行声明和访问
- 类型的类方法和实例方法声明和调用必须使用冒号(:)语法声明,目的让方法都带有一个默认的
self
参数 - 类型的构造方法命名为
create
,并且为类方法
根据上面的约定,我们先来定义一个所有对象的基类Object
:
-- 声明类型
Object = {
};
-- 设置__index元方法
Object.__index = Object;
-- 设置__gc元方法
Object.__gc = function (instance)
-- 进行对象销毁工作
print(instance, "destroy");
end
-- 定义构造函数
function Object:create()
local instance = {
};
setmetatable(instance, self);
return instance;
end
-- 定义实例方法
function Object:toString()
print (tostring(self));
end
可以看到我们创建了一个全局的table
变量Object
来作为类型。
然后使用了元方法__index
进行自身指向,这样做的目的是使实例对象能够访问对象所定义的属性或者方法。因为__index
的特点是当变量访问指定不存在的key时,就会去调用其元表的__index
方法,由于__index
指向就是Object
,因此就会判断Object
是否存在该key,并进行返回。另外如果指向的对象也设置了元表并且使用了__index
,那么会继续寻找其元表的__index
指向,直到最终没有设置元表的对象(这个特性在实现继承时特别关键,并且效果很好)。具体找寻方式如下图所示:
元方法__gc
在这里也使用到了,利用其特性可以轻松地知道对象实例销毁时机,可以在方法里面进行一些后续的处理,类似C++中的析构函数。在这里只是简单地打印是哪个对象被销毁。
接着我们讲解一下构造方法create
的实现,方法中调用了setmetatable
方法将self
(即类型Object
)元表绑定到instance
这个变量,配合之前设置__index
元方法,实例变量就会拥有与Object
相同的一些属性和方法定义了。
toString
方法则是一个实例方法, 主要用于将对象转换成字符串。
该类型具体使用方法如下所示:
local obj = Object:create();
print(obj:toString());
冒号(:)是lua的语法糖,其等效写法为
Object.create(Object)
和obj.toString(obj)
,省略了把obj
自身作为参数传入到方法中的这个步骤,让整个调用看起来更像是obj
自身提供的方法。
1.3 添加属性声明
类型的定义少不了属性和方法的声明,上例中只进行了方法的声明,这节将会重点讲述属性如何进行声明。利用上面的例子,我们再给Object
增加一个属性。
-- 声明属性
Object.tag = 999;
很简单,这样就完成了一个简单的属性定义。在代码中可以这样使用:
local obj = Object:create();
print (obj.tag); -- 输出999
但是问题来了,在面向对象中,其实类型和实例应该都会拥有属性,即类属性和实例属性。那么如果直接在Object
中定义属性是没有办法区分这是类属性还是实例属性的。所以,这里借鉴了javascript中的<