Skip to Content

深入理解 Lua 元表

Posted on 2 mins read

metatable Lua 元表

元表(metatable)作为 Lua 语言的一大经典语言特性,对于 Lua 语言本身有着重要的意义,本文试图随着日常开发中对 metatable 的深入使用,将我对其理解记录于此。

元表,仅仅关乎元方法

提到元表,就必须要知道元方法,下面是元方法的一个简要列表:

__add(a, b) --加法
__sub(a, b) --减法
__mul(a, b) --乘法
__div(a, b) --除法
__mod(a, b) --取模
__pow(a, b) --乘幂
__unm(a) --相反数
__concat(a, b) --连接
__len(a) --长度
__eq(a, b) --相等
__lt(a, b) --小于
__le(a, b) --小于等于
__index(a, b) --索引查询
__newindex(a, b, c) --索引更新
__call(a, ...) --执行方法调用
__tostring(a) --字符串输出
__metatable --保护元表

元表就是通过在一个表中通过对元方法的实现,来设定元表对应元方法的行为(比如给表实现自定义的 “+” 加法行为),再通过 setmetatable 方法的调用,将这些定义好的行为附加给需要的表,下面通过一个简单的例子来说明:

-- my.lua
local setmetatable = setmetatable

local data = {name="idevz",site="idevz.org"}
local mt = {__index = data,
	        __tostring=function()
				return 'custom_table_name'
			end
	}
local my_table = {}

setmetatable(mt, mt)
setmetatable(my_table, mt)
return my_table

上例中,通过在 mt 表中实现了 __index 元方法(此次,直接将 index 方法指定为一个以定义好的 data 表,index 方法自定义了对表的检索行为,及当检索不存在的 key 时,直接到 data 表中继续检索。),通过在 mt 表中实现了 __tostring 元方法,自定义了对表的输出行为,再通过 setmetatable 方法,将定义好的检索行为附加给需要的 my_table 表中,下面我们来看下用法:

local my = require( './my' )
print( my['name'] )
print(rawget( my, 'name' ))
print( my )
print( getmetatable( my ) )

运行结果为:

idevz
nil
custom_table_name
custom_table_name

require('./my') 返回的是上面定义的 my_table 表,这是一个空表,当通过 my['name'] 试图对 my_table 进行检索的时候,因为 my_table 设置了元表 mtmt 表中定义的 __index 元方法将检索行为指向了 data 表,这时 my['name'] 检索到了 key 为 name 的值为 ”idevz“。 而当通过 rawget 方法来获取 my_table 表中原始的 “name” 字段,发现结果为 nil , rawget 的意思是检索不借助元表,及不通过指定的元方法 __index。 因为在 my_table 的元表 mt 中我们自定义了 __tostring 元方法,所以我们可以直接打印 my_table 的值即为 __tostring 方法的返回值 “custom_table_name” 最后我们通过 getmetatable 方法获取表 my 的元表,直接打印这个表也得到 “custom_table_name”,这是因为我们将 mt 自己设置成了自己的元表。 到这里,我们可以回头仔细体会下 “元表仅仅关乎元方法这句话”。

Vanilla 中基于元表实现的寄存器

这是一个稍微复杂点的例子,元表中就引用了 Registry 自身的 __index 和 __newindex 两个元方法。

local setmetatable = setmetatable

local Registry = {}

function Registry:common_func()
	return true
end

function Registry:new()
    local data = {}
    local instance = {
        common_func = self.common_func,
        _data = data
    }
    return setmetatable(instance, {__index = self.__index, __newindex = self.__newindex})
end

function Registry:__newindex(index, value)
    local data = rawget(self, "_data")
    if data[self.namespace] == nil then data[self.namespace] = {} end
    if index ~= nil then
        data[self.namespace][index]=value
    end
end

function Registry:__index(index)
    local data = rawget(self, "_data")
    if data[self.namespace] == nil then data[self.namespace] = {} end
    local out = data[self.namespace][index]
    if out then return out else return false end
end

return Registry
comments powered by Disqus