什么是metatable(元表)
有关什么是元表我没办法比openresty的作者讲得更清楚,可以参考openresty最佳实践或是菜鸟教程来进行了解,在这篇文章中我只表达一些我自己使用中的方法和理解。
lua中的基础结构是比较少的,不过table结构的适用性确实非常的丰富,普通的模块也是使用table作为存储模块中函数的表结构。
元表可以用于弥补lua无法构建复杂的类型,我们来看一下如何使用吧~
__index 与 __newindex
__index
与__newindex
是使用元表最需要了解的两个元方法,在不够了解元表时,看到在openresty中对元表的使用通常有:
-- map.lua
local _M = {}
local mt = {__index = _M}
function _M.new()
return setmetatable({}, { __index = mt })
end
function _M.set(self, key, value)
self[key] = value
end
function _M.get(self, key)
return self[key]
end
return _M
再上面的模块中,可以使用new构建一个在元表中以模块_M
为__index
的table实例,例如:
-- test.lua
local map = require("map")
local tmap = map.new()
tmap:set('urey', 'hiker')
print(tmap:get('urey')) -- 输出 hiker
在此例中,tmap实际上并没有set
及get
的键,但是tmap配置了元表。在查找不到时,会在tmap的元表中的__index
进行查找,因此tmap:set()
实际调用了模块map中的set方法。
如果按照此例中对于__index
的理解去理解__newindex
则让我变得迷糊了。
实际上,不如优先认为__index
和__newindex
本身应当被配置为函数:
-- new_map.lua
local _M = {}
local _M.set(self, key, value)
self[key] = value
end
local _M.get(self, key)
return self[key]
end
function _M.new()
return setmetatable({}, {
__index = function(mytable, key)
return _M.key
end,
__newindex = function(mytable, key, value)
rawset(mytable, key, value)
end
}
end
return _M
如上所示,若有tmap = require('new_map').new()
,在tmap中查找某个未设置的键时,将调用__index
元方法,table[key]
中的table和key则化为元方法函数中的两个参数。当在tmap中设置某个未设置的键时,将调用__newindex
元方法,table[key] = value
中的table、key及value则化为元方法函数中的三个参数。
最后对map和new_map两个模块的使用可以获得基本相同的效果。
使用元表构建最小堆类
如果按照OOP,以上的map模块可以看作类的定义,而我们构建的带元表的tmap可以看作类的实例(Lua的模块本身也可以构建为可进行存储的实例,可以作为全局共享)。
最小堆类至少能够实现放入元素,弹出最小(小的定义应当可以自定义)元素的操作,因此有:
-- minheap.lua
local _M = {}
local mt = {__index = _M}
local function _less(a, b)
return a < b
end
local function _up(self, bt)
-- 上浮
local tmp, up
up = math.floor(bt / 2)
while up > 0 do
if _less(self[bt]._id, self[up]._id) then
self[bt], self[up] = self[up], self[bt]
else
break
end
bt = up
up = math.floor(bt / 2)
end
end
local function _down(self, up)
-- 下沉
end
function _M.push(self)
-- 入堆
end
function _M.pop(self)
-- 弹出
end
function _M.new()
return setmetatable({}, mt)
end
return _M
如上所示,minheap有了我们所期望的最小堆类的大体框架。但是使用了内部定义的_less
来定义小,如果要实现最大堆,就不得不用新的模块。
实际上我们在上浮和下沉函数中,self
即是类的实例,可以将小的定义函数放在实例中,可以在上浮下沉过程中进行调用,即有:
-- minheap1.lua
local function _less(a, b)
return a < b
end
local function _up(self, bt)
-- 上浮
local tmp, up
local less = self.less
up = math.floor(bt / 2)
while up > 0 do
if less(self[bt]._id, self[up]._id) then
self[bt], self[up] = self[up], self[bt]
else
break
end
bt = up
up = math.floor(bt / 2)
end
end
-- 省略其他与minheap相同的部分
function _M.new(newless)
return setmetatable({less = newless or _less}, mt)
end
这样就可以在堆操作中使用自定义的比较函数进行自定义。但是这样的方法使得类实例中不仅仅包含元素序列,还包含了less函数,使得可能在对类实例进行遍历的过程中,获得不想要的元素。
在此尝试使用了两级的元表结构来实现,对minheap1进行修改:
-- minheap2.lua
-- 省略其他与minheap及minheap1相同的部分
local mt = {__index = _M}
function _M.new(less)
return setmetatable({}, {__index = setmetatable({less = less or _less}, mt)})
end
可以看到通过两级元表,将类实例结构分为三层:
- 最底层是基础模块,这部分是所有类实例共享的,其中的函数或元素是不可修改的;
- 中间层包含自定义的元素或函数,可以在初始化时进行自定义,如果需要在初始化以后进行修改,就需要配合
__newindex
元方法; - 最上层是类实例本身,用于存储类实例的数据。
总结
通过使用两层元表的方法,我们较好的构造了类,包含不同类中的内置方法、实例可定制的方法以及可控的实例空间。如果仅使用一层元表,将自定义的方法配置到模块内部,则会导致不同的实例之间配置了相同的的方法。