类是创建对象的模板,对象是类的实例,Lua 中没有类的概念,所以我们只能使用现有的支持去模拟类的概念。
table 实现成员变量与成员函数
Lua 的 table 就是一种对象,可以有成员变量(如下例 sound),也可以有成员函数,(如下例 makesound):
1 | Dog = {sound = 'wolf'} |
首先 Lua 的 table 默认是引用类型,dog1 与 dog2 都是全局变量 Dog 的引用。
这个例子中,makesound 函数中使用了全局变量 Dog.sound 进行赋值操作,所以当 Dog = nil 的时候,dog1.makesound()
函数执行的时候当然会报错。
改造一下这个例子,makesound 函数定义的时候我们引入一个 self
参数:
1 | Dog = {sound = 'wolf'} |
makesound 函数,使用第一个参数 self
来标识调用者,而不是使用全局变量,self 和 全局变量Dog 引用了同一个 table,这样就可以工作了。
不过对 Dog = nil 赋值以后,Dog 这个全局变量的 table 不就是 nil 了吗,dog1, dog2 都是对 Dog 的引用,dog1, dog2 为何不是 nil?
要解释这个问题,要说 Lua 的 table 不是值而是对象,Dog = {sound = ‘wolf’} 实际上是创建了这个 table 的引用,最终 Dog,dog1,dog2 都在引用这个 table
Dog = nil,只是解除了 Dog 的引用,此时 dog1, dog2 还都在引用它。当对一个 table 的引用为 0 时,Lua 的垃圾收集器最终会删除该 table,并释放它所占用的内存空间。下面这个例子可以很好的解释:
1 | local a = {} -- 创建一个table,并将它的引用存储在a |
好,现在我们回来。使用 self
参数是所有面向对象语言的一个核心。大多数面向对象语言都对程序员隐藏了 self
参数,从而使得程序员不必显示地声明这个参数。毕竟,定义成员函数和调用的时候都多了一个参数好麻烦。
Lua 也可以,当我们在定义函数和调用函数时,使用了冒号代替点号,则能隐藏 self
参数,这实际是个语法糖,我们重写上面的代码:
1 | Dog = {sound = 'wolf'} |
现在我们的对象有一个标识符(名称dog1),有成员变量和成员函数,但是他们都是一个全局对象的引用。我们可以把它当成是一个全局类来使用,它的变量是静态变量,方法是静态方法。
我们如何才能创建拥有相似行为的多个对象呢?
类
在 Lua 中,要表示一个类,只需创建一个专用作其他对象的原型(prototype)。原型也是一种常规的对象,也就是说我们可以直接通过原型去调用对应的方法。当其它对象(类的实例)遇到一个未知操作时,原型会先查找它。这很像 Javascript。
而利用table 的元表与 __index 特有键就可以实现这种原型表述。对象a调用任何不存在的成员都会到对象b中查找。术语上,可以将b看作类,a看作对象。
1 | setmetatable(a, {__index = b}) |
所以可以这样模拟一个类:
1 | Dog = {sound = 'wolf'} |
由此我们可以理解为,dog1 与 dog2 都是 Dog 类的对象实例,由于 metatable 特性,导致调用 dog1 不存在的方法,会自动查找 metatable 中的 __index 键,最终调用了 Dog 的方法。另外 sound 可以看成类的 public 属性
dog1.sound
实际上 getmetatable(dog1).__index.sound
dgo1:makesound()
实际上 getmetatable(dog1).__index:makesound()
这里还有一个小优化:
1 | function Dog:new(o) |
最后,我们给出 Lua 实现一个类的最终姿势:
1 | local _M = {} |