Lua 程序设计:元表和元方法

元表与元方法

通常,Lua 中的每个值都有一套预定义的操作集合。例如,可以将数字相加,可以连接字符串,还可以在 table 中插入一对 key-value 等。但是我们无法将两个 table 相加,无法对函数做比较,也无法调用一个字符串。

但是我们可以通过元表来修改一个值的行为,使其执行一个非预定义的操作时能执行一个指定的操作。比如,如果 a 和 b 都是 table ,我们调用 setmetatable 函数将 a 的元表设置为 table t,其中 t 的 __add 字段 (也称key为"__add",或者 index 为“__add”) 的值是一个函数,那么 Lua 在执行 a + b 表达式时就会调用这个函数执行相加的操作。

任何 table 都可以作为任何值的元表。在 Lua 代码中,只能设置 table 的元表,若要设置其他类型的值的元表,则必须通过 C 代码来完成。标准字符串库为所有的字符串都设置了一个元表,而其他类型的值默认都没有元表。

算术类和关系类的元方法

下面是一个用 table 表示集合的示例,说明如何使用元表。

Set = {}

-- 集合的元表
local mt = {}

-- 根据参数列表中的值创建一个集合
function Set.new(l)
    local set = {}
    -- 设置元表为mt
    setmetatable(set, 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

-- 集合的交集
function Set.intersection(a,b)
    local res = Set.new{}
    for k in pairs(a) do res[k] = b[k] end
    return res
end

-- 集合b是否包含集合a
function Set.contain(a,b)
    for k in pairs(a) do
        if not b[k] then return false end
    end
    return true
end

-- 打印集合
function Set.tostring(set)
    local l = {}
    for e in pairs(set) do l[#l + 1] = e end
    return "{" .. table.concat(l, ",") .. "}"
end

-- 为元表mt设置元方法
-- a + b,集合并集
mt.__add = Set.union

-- a * b,集合交集
mt.__mul = Set.intersection

-- a <= b,集合b是否包含集合a
mt.__le = Set.contain

-- a < b,集合b是否包含集合b(a,b集合不相同)
mt.__lt = function (a,b)
    return a <= b and not (b <= a)
end

-- a == b,集合是否相等
mt.__eq = function (a,b)
    return a <= b and b <= a
end

-- print打印集合
mt.__tostring = Set.tostring

s1 = Set.new{1,2,3}
s2 = Set.new{3}
-- 输出并集
print(s1 + s2)
-- 输出交集
print(s1 * s2)
print(s2 < s1)
print(s1 == s2)

当两个集合相加时,可以使用任意一个集合的元表。然而,如果一个表达式中混合了具有不同元表的值时,例如:

s = Set.new{1,2,3}
s = s + 8

Lua 会按照如下步骤来查找元表:如果第一个值有元表,并且元表中有 __add 元方法,那么就使用这个元方法。如果第一个值的元表中没有 __add 元方法,则使用第二个值的 __add 元方法。如果元表中都没有 __add 元方法,则报错。

函数 setmetatablegetmetatable 也会用到元表中的一个字段,用于保护元表。假设想要保护集合的元表,使用户既不能看到也不能修改集合元表,那么就需要为元表设置 __metatable 字段,当设置了该字段时,getmetatable 会返回该字段的值,而 setmetatable 则会引发一个错误。

mt.__metatable = "not your business"

s1 = Set.new{}
print(getmetatable(s1))
setmetatable(s1, {})

上述代码会有如下输出:

not your business
lua: set.lua:80: cannot change a protected metatable
stack traceback:
        [C]: in function 'setmetatable'
        set.lua:80: in main chunk
        [C]: ?

table 访问的元方法

__index 元方法

当访问一个 table 中不存在的字段时,得到的结果为 nil 。这并非完全正确。实际上,Lua 在找不到一个字段时,会去查找 __index 元方法,如果没有这个元方法,则返回 nil。否则,就由这个元方法提供返回结果

在 Lua 中,将 __index 元方法用于继承是很普遍的做法,因此 Lua 提供了一种更便捷的方法来实现此功能。那就是 __index 元方法不一定必须是一个函数,它还可以是一个 table 。当它是一个函数时,Lua 以 table 和不存在的 key 作为参数来调用该函数。而当它是一个 table 时,Lua 就以相同的方式来访问这个 table 。

如果不想在访问一个 table 时触发它的 __index 元方法,可以使用 rawget 函数。

__newindex 元方法

__index 元方法用于 table 的查找,而 __newindex 元方法用于 table 的更新。当对一个 table 中不存在的字段赋值时,Lua 就会查找 __newindex 元方法,并调用它,而不是执行赋值。如果这个元方法是一个 table ,Lua 就在这个 table 中执行赋值操作。使用 rawset 函数可以绕过 __newindex 元方法,直接设置 key-value。

具有默认值的 table

常规 table 中的任何字段的默认值都是 nil。通过元表可以很容易地修改这个默认值:

function setDefault(t,d)
    local mt = {__index = function () return d end}
    setmetatable(t, mt)
end

t = {x=10,y=20}
print(t.x, t.z)   ---> 10      nil
setDefault(t, 0)
print(t.x, t.z)   ---> 10      0

若要让具有不同默认值的 table 都使用一个元表,那么就需要将每个 table 的默认值放到 table 中,这个默认值的字段必须是唯一的。为了保持默认值字段的唯一性,我们可以创建一个新的 table ,并使用它作为 key

local key = {}
local mt = {__index = function (t) return t[key] end}
function setDefault(t,d)
    t[key] = d
    setmetatable(t, mt)
end

跟踪 table 的访问

__index__newindex 元方法都是在 table 中没有所需访问的字段时才发挥作用。为了监视一个 table 中的所有访问,我们可以为真正的 table 创建一个代理,这个代理是一个空 table,其中 __index__newindex 元方法可用于跟踪所有的访问,并将访问重定向到真正的 table 上。假设我们要跟踪 table t 的所有访问,我们可以这么做:

-- t 是在某个地方创建的 table
t = {} 

-- 保持对原有 table 的一个私有访问
local _t = t 

-- 创建代理
t = {}

-- 创建元表

local mt = {
    __index = function (t, k)
        print("access element " .. tostring(k))
        -- 返回真正的 table 中的值
        return _t[k]
    end,
    
    __newindex = function (t, k, v)
        print("update element " .. tostring(k) .. " to " .. tostring(v))
        -- 更新真正的 table 中的值
        _t[k] = v
    end
}

-- 设置元表
setmetatable(t, mt)

这个方法存在一个问题就是无法遍历原来的 table 。

如果想要同时监视几个 table ,只需要以某种形式将每个代理与原 table 关联起来,并且所有的代理都共享一个元表。我们仍可以使用在具有默认值的 table 一节中的方法,将原来的 table 保存在代理 table 中的一个唯一字段中。代码如下:

-- 创建唯一的 index
local index = {}

-- 创建元表

local mt = {
    __index = function (t, k)
        print("access element " .. tostring(k))
        -- 返回真正的 table 中的值
        return t[index][k]
    end,
    
    __newindex = function (t, k, v)
        print("update element " .. tostring(k) .. " to " .. tostring(v))
        -- 更新真正的 table 中的值
        t[index][k] = v
    end
}

function track(t)
    local proxy = {}
    proxy[index] = t
    setmetatable(proxy, mt)
    return proxy
end

-- 原来的 table
t = {}
t = track(t)

只读的 table

要实现只读的 table 很容易,只需要创建一个代理,跟踪所有对 table 的更新操作,并引发一个错误就可以了。由于无需跟踪字段的访问,我们可以直接将 __index 元方法设置称原来的 table 。不过这种方法要求为每个代理创建一个新的元表。代码如下:

function readOnly(t)
    local proxy = {}
    local mt = {
        __index = t,
        __newindex = function (t, k , v)
            error("attempt to update a read-only table", 2)
        end
    }
    setmetatable(proxy, mt)
    return proxy
end

days = readOnly{"Sunday", "Monday", "Tuesday", "Wednesday", 
                "Thursday", "Friday", "Saturday"}
print(days[1])
days[2] = "Noday"
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值