Lua 是一门很简单的语言. 本身语言设计中没有原生的 oop 设计, 但是我们可以使用 setmetatable 来做一个伪装的 class.

setmetatable

setmetatable 类似于 python__metaclass__, 是用来设置一个 table 的元 table. 如下为 coolshell 中的一个简单的例子:

1
2
fraction_a = {numerator=2, denominator=3}
fraction_b = {numerator=4, denominator=7}

如上, 我们设置了两个分数, 分别为 fraction_a, fraction_b. 我们想要对这两个分数进行相加操作. 直接使用 + 必然是不行的.

Lua 给我们提供了 setmetatable. 如下, 我们来让 fraction_a, fraction_b 可以相加.

1. 创建一个 metatable

1
funciton_op = {}

2.metatable__add 方法进行完善, 要求传入两个 table, 可以对这两个 table 按照分数相加的算法进行执行。

1
2
3
4
5
6
7

function_op.__add = function(f1, f2)
ret = {}
ret.numerator = f1.numerator * f2.denominator + f2.numerator * f1.denominator
ret.denominator = f1.denominator * f2.denominator
return ret
end

3. 我们使用 setmetatable, 来设定 fraction_a, fraction_b, 的 metatablefunction_op

1
2
3

setmetatable(fraction_a, function_op)
setmetatable(fraction_b, function_op)

这样, 当我们执行 fraction_a + fraction_b 时, 会自动调用自己所属的 metatable__add 方法, 同时传入自己, + 后面的操作变量作为第二个参数传入.

如上, 当我们调用 fraction_a + fraction_b 时候, 会执行 funciton_op.__add(fraction_a, fraction_b);

: metatable 可以理解为 javascript 中的 prototype.

metatable 还支持非常多的 metamethod, 如下:

metamethod 说明
__add(a, b) 对应表达式 a + b
__sub(a, b) 对应表达式 a - b
__mul(a, b) 对应表达式 a * b
__div(a, b) 对应表达式 a / b
__mod(a, b) 对应表达式 a % b
__pow(a, b) 对应表达式 a ^ b
__unm(a) 对应表达式 -a
__concat(a, b) 对应表达式 a .. b
__len(a) 对应表达式 #a
__eq(a, b) 对应表达式 a == b
__lt(a, b) 对应表达式 a < b
__le(a, b) 对应表达式 a <= b
__index(a, b) 对应表达式 a.b
__newindex(a, b, c) 对应表达式 a.b = c
__call(a, …) 对应表达式 a(…)

如何让 Lua 可以 OOP

如果我们设定了一个 table, 给这个 table 设定了一些方法, 那么别人来调用这个 table, 同时传入我自己, 是不是就可以 OOP 了 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local Class = {}    --创建了一个名为 Class 的 class


Class.getName = function(obj) --给 Class 设定了一个 getName 的方法
return obj.name
end


Class.setName = function(obj, name) --给 Class 设定了一个 setName 的方法, 对传入的对象, 我们可以设置
obj.name = name
end


c = {} --初始化一个 table

Class.setName(c, 'mario') --调用 Class.setName, 给 c 设定 name

print(Class.getName(c)) --调用 Class.getName, 用来获取 c 的 name

上面的代码有这么几个问题:

  • 每次我都要使用 Class 作为前缀, 使用 Class 的方法来操作 obj, 有点麻烦
  • Class 的这几个方法, 第一个参数都是 obj, 有啥办法能省略么 ?
  • 我每次都要初始化一个 table, 再把这个 tablemetatable 设置为这个名为 Classtable 么? 可以封装一下么?

如下, 我们一个一个解决:

每次我都要使用 Class 作为前缀, 使用 Class 的方法来操作 obj, 有点麻烦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
local Class = {}    --创建了一个名为 Class 的 class


Class.getName = function(obj) --给 Class 设定了一个 getName 的方法
return obj.name
end


Class.setName = function(obj, name) --给 Class 设定了一个 setName 的方法, 对传入的对象, 我们可以设置
obj.name = name
end


c = {} --初始化一个 table

setmetatable(c, {__index=Class})

--[[
这个命令分为两步:
第一步, 创建了一个 table, 设定 table 的 __index 为 Class
第二步, 设定 c 的 metatable 是这个新创建的 table
这样, 当 c 如果调用不存在的函数, 会尝试去调用 Class 的函数, 同时把 c 当做第一个参数传入进去
--]]

c.setName(c, 'mario') --由于设定了 metatable, 这么调用可行

print(c.getName(c)) -- 由于设定了 metatable, 这么调用可行

如上, 我们每次调用, 就不需要写 Class 了, 直接写新生成的 table 名称即可.

Class 的这几个方法, 第一个参数都是 obj, 有啥办法能省略么 ?

第二个问题呢, lua 自己提供了一个语法糖,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

local Class = {} --创建了一个名为 Class 的 class


function Class:getName() --给 Class 设定了一个 getName 的方法
return self.name
end

--[[
相当于:

Class.getName(obj)
return obj.name
end
--]]

function Class:setName(name) --给 Class 设定了一个 setName 的方法, 对传入的对象, 我们可以设置
self.name = name
end

--[[
相当于:

Class.setName(obj, name)
obj.name = name
end
--]]


c = {} --初始化一个 table

setmetatable(c, {__index=Class})

--[[
这个命令分为两步:
第一步, 创建了一个 table, 设定 table 的 __index 为 Class
第二步, 设定 c 的 metatable 是这个新创建的 table
这样, 当 c 如果调用不存在的函数, 会尝试去调用 Class 的函数, 同时把 c 当做第一个参数传入进去
--]]

c:setName('mario') --由于设定了 metatable, 这么调用可行
--[[
相当于调用了 c.setName(c, 'mario')
--]]

print(c:getName()) -- 由于设定了 metatable, 这么调用可行
--[[
相当于 print(c.getName(c))
--]]

如上, 我们使用 function Class:getName() 替代了 Class.getName = function(obj). 同时, 给所有的代码中增加了一个隐形的 self 表示调用当前函数的 table

我每次都要初始化一个 table, 再把这个 tablemetatable 设置为这个名为 Classtable 么? 可以封装一下么?

我们先写一个函数

1
2
3
4
5
6
7
function ClassObject(metaTable, obj) --创建函数, 传入一个 obj

if obj == nil then
obj = {}
end
return setmetatable(obj, {__index=metaTable}) --设定这个 obj 的 metatable 的 __index 为 metaTable
end

如上, 当我们调用的时候, 返回一个 metatablemetaTabletable.

貌似不是太优雅…

如果我们直接给 Class 这个 table 设定一个 new 函数呢?

1
2
3
4
5
6
7
8
9
10
11
12
local Class = {}

Class.new = function(metaTable, newObj) --当我们传入 metaTable, newObj, 返回了 newObj, 并且 newObj 的 metatable 的 __index 为 `metaTable`

if newObj == nil then
newObj = {}
end

return setmetatable(newObj, {__index=metaTable}) --setmetatable 返回第一个参数
end

Class.new(Class, {}) --创建这个 table

metaTableClass 时 :

1
2
3
4
5
6
7
local Class = {}

Class.new = function(Class, newObj)
if newObj == nil then
newObj = {}
end
return setmetatable(newObj, {__index=Class})

这样, 貌似还是不好看. 我们看上面解决问题 2 的时候的语法糖, 继续改写上面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
local Class = {}

function Class:new(obj)

if obj == nil then
obj = {}
end

return setmetatable(obj, {__index=self})
end

obj1 = Class:new() --不给 obj 设置默认值
obj2 = Class:new({name='mario'}) --给 obj 设置默认值

如上, 最后我们总结一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
local Class = {}

function Class:new(obj)
if obj == nil then
obj = {}
end

return setmetatable(obj, {__index=self})
end

function Class:getName()
return self.name
end

function Class:setName(name)
self.name = name
end

--如下为调用方法

obj = Class:new()

obj:setName('obj') --设定 name

print(obj:getName()) --返回上面设置的 obj

最后的最后, 当我们要创建一个类, 可以直接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
local className = {}

function className:new(obj)
--返回一个 metatable 为 {__index=self} 的 table
end

function className:method1()
end

function className:method2()
end

-- 等等等

-- 实例化

obj = className:new()

-- 调用方法:

obj:method1()
obj:method2()