Lua-元方法和元表基础


简介

通常,Lua中每种类型的值都有一套可预见的操作集合。例如,可以将数字相加,将字符串连接,还可以在表中插入键值对等。但是我们无法直接将两个表相加,无法对函数作比较。

元表(metatable)可以修改一个值在面对一个未知操作时的行为。例如,对于两个表a和b,可以通过元表定义a+b。当Lua试图将两个表相加时,会先检查两者之一是否有元表且该元表中是否有__add字段。如果找到了该字段,就调用它对应的值,即元方法(metamethod)进行计算。

可以通过setmetatable(tbl, t)getmetatable(tbl)设置、获取表的元表

Lua中只能为表设置元表,字符串标准库为所有的字符串都设置了同一个元表。为其他类型的值设置元表要修改C代码支持。


基础元方法使用举例


local Set = {}

function Set.new(t)
    local o = {}
    setmetatable(o, Set)
    for _,v in ipairs(t) do
        o[v] = true
    end
    return o
end

--  算术运算符元方法+,实现两个Set相加
function Set.__add(a, b)
    local result = Set.new({})
    for k in pairs(a) do result[k] = true end
    for k in pairs(b) do result[k] = true end
    return result
end

--  关系运算符元方法==,检测两个Set是否相等
function Set.__eq(a, b)
    for k in pairs(a) do
        if not b[k] then return false end
    end

    for k in pairs(b) do
        if not a[k] then return false end
    end
    return true
end

--  库元方法,实现Set格式化输出
function Set.__tostring(set)
    local tbl = {}
    for k in pairs(set) do
        tbl[#tbl+1] = k
    end
    return "{"..table.concat(tbl,",").."}"
end

-----------------
local set_1 = Set.new({1,25,36,1,40})
local set_2 = Set.new({2,2,25,3})
print(set_1 + set_2) -- 打印: {1,2,3,40,25,36}
set_1 = Set.new({1,1,2,4,4})
set_2 = Set.new({1,2,4})
print(set_1 == set_2)   -- 打印: true

正常情况下,我们可以继续为set_1设置元表,假如我们想限制这种操作,保护我们定义的Set,可以在元表中设置__metatable字段,如下所示:

--  其他代码同上
Set.__metatable = "禁止修改集合的元表"
print(getmetatable(set_1)) --   打印 "禁止修改集合的元表"
setmetatable(set_1,{})  -- 将会报错,提示 cannot change a protected metatable

表相关的元方法

以上算术运算符、关系运算符和库对应的元方法,都没有改变语言的正常行为。Lua还提供了一些方法来访问和修改表中不存在的字段。

__index 元方法

正常情况下,访问表中不存在的字段会得到nil。实际的流程是,这些访问会引发解释器查找一个名为__index的元方法,如果没有这个元方法,结果就是nil;否则由这个元方法来提供最终结果。

local property = {width = 250, height = 1000}
local Shape = {}
function Shape.new(o)
    setmetatable(o, Shape)
    return o
end

function Shape.__index(param, key)
    return property[key]
end

local s = Shape.new({name = "圆", ret = true})
--  s有name字段,直接返回,s没有width字段,会去找元表中__index的返回结果,最终得到 250
print(s.name, s.width) --   打印 圆    250

利用__index可以实现继承,当我们希望获得一个表原始的字段,即不调用__index方法,可以使用rawget(t, index),例如,对以上代码做修改:

--  第一个返回__index结果,第二个返回原始table的字段,不存在width所以为nil
print(s.width, rawget(s,"width")) --    打印  250 nil

当然,进行原始访问并不会加快代码的运行,只是有时候确实有这种需求。

__newindex 元方法

__newindex__index类似,不同之处在于前者用于表的更新而后者用于表的查询。当对一个表中不存在的索引赋值时,
解释器就会查找__newindex元方法,如果存在,解释器就调用它而不会执行赋值。用法如下:


local t = {
    name = "Name",
    age = 12
}
local mt = {
    __newindex = function(param, key)
        print("禁止赋值【"..key.."】")
    end
}
setmetatable(t, mt)
t.extra = 25 -- 打印 禁止赋值【extra】

同样我们可以调用rawset(t,k,v)绕过__newindex而直接赋值

组合使用元方法__index__newindex可以实现一些强大的结构,例如只读的表、具有默认值的表和继承。

创建具有默认值的表

正常一个空表所有字段默认值都是nil,通过元表可以设置默认值。

--  保证key的唯一性,创建一个新表作为默认值的key
local _key = {}
local mt = {__index = function(t) return t[_key]  end}
local function SetDefault(t, v)
    t[_key] = v
    setmetatable(t, mt)
end

local t = {a = 15, b = 20}
SetDefault(t, -1)
print(t.c)  --  打印 -1

设置只读的表

function SetReadOnly(t)
    local proxy = {}
    local mt = {
        __index = t;
        __newindex = function()
            error("table is read only", 2)
        end
    }
    setmetatable(proxy, mt)
    return proxy
end

声明:有无之境|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - Lua-元方法和元表基础


有无之境