表是Lua中最重要的数据结构,而metatable和metamethod又是表中的关键,lua可以通过元表来定义一些表的独特操作,像比较基础的定义表的算数运算和C++中的重定义运算符比较像。正因为元表的存在,才让lua有能力实现一些更加复杂的数据结构和模拟面向对象。本篇记录一些相关知识,对于基础部分简单过一下,主要记录后面一些思考。
metatable & metamethod
元表让我们能够为某些类型的值添加一些未曾有过的操作,比如定义一个a + b,a和b都是表。一般情况下肯定是加不了的。而此时lua会去寻找a or b中有没有元表,如果发现了存在元表并且元表中存在key:__add,那么会使用__add对应的value来完成a + b的操作。
获取某个变量的元表:getmetatable(a)
为某个变量设置元表:setmetatable(a, _mt) _mt就是元表
假设前面的a和b代表的是集合,比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local Set = {}
local mt = {}
function Set.new (l)
local set = {}
setmetatable(set, mt) -- 将mt设置为元表
for _, v in ipairs(l) do set[v] = true end
return set
end
function Set.union (a, b)
local res = Set.new{}
for k in pairs(a) do res[k] = true end
for k in pairs(b) do res[k] = true end
return res
end
return Set
那么为了实现相加的操作,可以为元表mt添加metamethod:
1
mt.__add = Set.union
此时两个集合相加的操作,就被设置成了并操作。
其他类型的metamethod用起来都和__add没有本质的区别,这里仅记录一些都有哪些:
算数运算:乘法(mul),减法(sub)、浮点除法(div)、向下取整除法(idiv)、取反(unm)、取模(mod)和求幂(pow。所有的按位操作也有对应的元方法:按位与(band)、按位或(bor)、按位异或(bxor)、按位非(bnot)、左移(shl)和右移(shr)。还可以为连接运算符定义行为,使用字段 concat。(注意每个操作定义时都是需要__的)
关系运算:等于(eq),小于(lt),小于等于(le)。其他的比较都是根据这三个进行转换的。
Library-Defined:__tostring,__metatable。
对于__metatable,它可以保护我们已经定义好的类型,设置了__metatable之后,再使用getmetatable会去获取__metatable的value,并且此时不再允许setmetatable了。
比如对之前的set类的元表添加一行:
1
mt.__metatable = "can not set metatable"
之后再另一个文件中,按如下方式写:
1
2
3
4
5
6
local class = require("set")
local s1 = class.new{10, 20, 30, 50}
print(getmetatable(s1))
local m2 = {}
setmetatable(s1, m2)
这时输出不会是表的地址,而是”can not set metatable”,并且后面的setmetatable会报错:
cannot change protected metatable
Table-Access Metamethods
除了上面的一些运算操作,对于元表最关键的元方法是__index和__newindex,他们分别定义了表中不存在key的时候的访问和设置操作。通过他们可以为表定义一些默认的行为。比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
function setDefault(t, value)
local mt = {
__index = function ()
return value
end
}
setmetatable(t, mt)
end
local tab = { x = 1, y = 2}
print(tab.z) --> nil
setDefault(tab, 0)
print(tab.z) --> 0
上述代码中,tab初始时没设置z,所以自然返回nil,而在设置了元表之后,即便仍然没有z,但是元表中存在__index,会按照index的metamethod执行,返回设置好的默认值0。
这里有一个设计上的问题,如果有多个表都调用setDefault设置元表,同时因为return value是通过外面的参数传进来的,每一次调用也会多一个闭包,在实际编程中可能经常会不注意造成性能浪费,尤其是在模拟面向对象的时候,元表往往就是类或者父类,如果不注意开销会很大。解决方法是要把可能会多次用于设置的元表提出去。比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local key = {}
local mt_g = {
__index = function(t)
return t[key]
end
}
function setDefault_(t, value)
t[key]= value
setmetatable(t, mt_g)
end
setDefault_(tab, 0)
local tab2 = { x = 3 }
setDefault_(tab2, 1)
print(getmetatable(tab)) --> table:000001C5A425F650
print(getmetatable(tab2)) --> table:000001C5A425F650
如上述代码,此时的__index设置的函数需要传递参数t,也就是包含这个元表的变量本身。在setDefault_中设置了t[key]用来保存默认值数据,所以按照这样再访问元表时,每次访问的都会是一个元表,默认值本身存到了每个变量自身当中。
这里有一个小点:使用{}作为key,可以保证键值唯一,防止命名冲突,因为lua表以表为键的时候存的是地址,每次新建一个空表存的地址不一样。
1
2
3
4
5
6
7
8
9
local t = {}
local key1 = {}
local key2 = {}
t[key1] = "value1"
t[key2] = "value2"
print(t[key1]) -- 输出 "value1"
print(t[key2]) -- 输出 "value2"
控制表的访问
如果我们想对某个表的访问进行控制,比如说进行追踪记录或者限制读写等,可以使用代理表的方法。以追踪表的访问功能为例,如果只是加一个元表,我们虽然可以再元表中的访问方法中设置一些记录,但是如果本身表中就存在某些key,lua是不会执行元表中的index和newindex的,这里就需要使用另外一个表来代理。
比如Programming in 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
function track (t)
local proxy = {} -- proxy table for 't'
-- create metatable for the proxy
local mt = {
__index = function (_, k)
print("*access to element " .. tostring(k))
return t[k] -- access the original table
end,
__newindex = function (_, k, v)
print("*update of element " .. tostring(k) ..
" to " .. tostring(v))
t[k] = v -- update original table
end,
__pairs = function ()
return function (_, k) -- iteration function
local nextkey, nextvalue = next(t, k)
if nextkey ~= nil then -- avoid last value
print("*traversing element " .. tostring(nextkey))
end
return nextkey, nextvalue
end
end,
__len = function () return #t end
}
setmetatable(proxy, mt)
return proxy
end
t = {x = 1} -- an arbitrary table
t = track(t)
可以看到在track函数中,返回的是空表proxy,其作为代理表,而在访问表的时候因为是空表,所有的操作均会走metamethod,而在metamethod之中会去方位原有表中有没有:t[k],只是按这样写又构成了闭包,我们仍按照前面提到的方法进行改进,作为练习,可以改成如下:
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
local key = {}
local mt_ = {
__index = function (t, k)
print("*access to element " .. tostring(k))
return t[key][k] -- access the original table
end,
__newindex = function (t, k, v)
print("*update of element " .. tostring(k) ..
" to " .. tostring(v))
t[key][k] = v -- update original table
end,
__len = function (t) return #t[key] end
}
function track (t)
local proxy = {} -- proxy table for 't'
proxy[key] = t
setmetatable(proxy, mt_)
return proxy
end
local trackTab = {10, 20}
trackTab = track(trackTab)
trackTab[1] = "hello"
print(trackTab[1])
print(getmetatable(trackTab))
local t2 = {}
t2 = track(t2)
t2[1] = "world"
print(t2[1])
print(getmetatable(t2))
--[[
Output:
*update of element 1 to hello
*access to element 1
hello
table:000001C5A425EE10
*update of element 1 to world
*access to element 1
world
table:000001C5A425EE10
]]
类似的方法,我们可以将表设置为只读的,在newindex中输出error,而不进行赋值即可。
More
Programming in lua 4th