Lua的metatable使用初步

什么是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实际上并没有setget的键,但是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

可以看到通过两级元表,将类实例结构分为三层:

  1. 最底层是基础模块,这部分是所有类实例共享的,其中的函数或元素是不可修改的;
  2. 中间层包含自定义的元素或函数,可以在初始化时进行自定义,如果需要在初始化以后进行修改,就需要配合__newindex元方法;
  3. 最上层是类实例本身,用于存储类实例的数据。

完整代码请参考

总结

通过使用两层元表的方法,我们较好的构造了类,包含不同类中的内置方法、实例可定制的方法以及可控的实例空间。如果仅使用一层元表,将自定义的方法配置到模块内部,则会导致不同的实例之间配置了相同的的方法。

猜你喜欢

转载自my.oschina.net/ureyishere/blog/1811390