快速掌握Lua 5.3 —— "metatables" and "metamethods" (1)

Q:什么是”metatable”?

A:”metatable”是一张定义了一些特殊功能的”table”,当他被分配给某个”table”(另一个”table”,与”metatable”不是同一个”table”)时,这个”table”就具有了这些特殊功能。
当在Lua中创建一个”table”时,默认不会创建它的”metatable”。使用setmetatable()为一个”table”分配”metatable”;使用getmetatable()获取分配给”table”的”metatable”。

t = {}
print(getmetatable(t))    --> nil
mt = {}
setmetatable(t, mt)
print(getmetatable(t) == mt)    --> true

Q:什么是”metamethod”?

A:”metatable”所具有的特殊功能就是”metamethod”,比如”metatable”的”__add”,”__sub”,”__eq”等等。
举个例子,通常在Lua中对”table”的操作是:插入一个”key-value”对,通过”key”查找”value”,遍历所有的”key-value”对。但是通常不能将两个”table”相加,比较两个”table”的大小等等,而”metamethod”就能够完成这些操作。

mt.__add = function () ... end    -- 定义两个"table"相加的逻辑。
mt.__sub = function () ... end    -- 定义两个"table"相减的逻辑。
mt.__eq = function () ... end    -- 定义判断两个"table"是否相等的逻辑。
...

Q:如何定义算数运算的”metamethods”?

A:以计算两个集合之间的算数运算为例,

Set = {}    -- 存储对于集合的所有操作函数。
Set.mt = {}    -- "metatable"。

-- 创建一个新的集合。
function Set.new (t)
    local set = {}    -- 新的集合。
    setmetatable(set, Set.mt)    -- 所有的集合共享同一个"metatable"。
    -- "key-value",集合中的元素值-"true"。
    for _, l in ipairs(t) do set[l] = 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]    -- 很聪明的方法,如果集合"b"中没有元素"k"的话就会返回"nil"。
    end
    return res
end

-- 我们规定"s1 - s2"相当于求"(s1 * s2)"在"s1"中的补集。
function Set.sub(a, b)
    local res = Set.new{}
    for k in pairs(a) do
        if not b[k] then
            res[k] = true
        end
    end
    return res
end

-- 我们规定"s1 / s2",仅是打印一句话,以证明调用了正确的"metamethod"。
function Set.div(a, b)
    local res = Set.new{"__div"}
    return res
end

-- 我们规定"-s"仅是打印一句话,以证明调用了正确的"metamethod"。
function Set.unm(a)
    local res = Set.new{"__unm"}
    return res
end

-- 我们规定"s1 ^ s2"仅是打印一句话,以证明调用了正确的"metamethod"。
function Set.pow(a, b)
    local res = Set.new{"__pow"}
    return res
end

-- 将集合转换为字符串形式。
function Set.tostring (set)
    local s = "{"
    local sep = ""
    for e in pairs(set) do
        s = s .. sep .. e
        sep = ", "
    end
    return s .. "}"
end

-- 打印集合。
function Set.print (s)
    print(Set.tostring(s))
end

Set.mt.__add = Set.union    -- 定义两个"table"相加的逻辑,并集。
Set.mt.__mul = Set.intersection    -- 定义两个"table"相乘的逻辑,交集。
Set.mt.__sub = Set.sub    -- 定义两个"table"相减的逻辑。
Set.mt.__div = Set.div    -- 定义两个"table"相除的逻辑。
Set.mt.__unm = Set.unm    -- 定义一个"table"取反的逻辑。
Set.mt.__pow = Set.pow    -- 定义两个"table"相幂的逻辑。

s1 = Set.new{10, 20, 30, 50}
s2 = Set.new{30, 1}
-- 两个集合共享同一个"metatable"。
print(getmetatable(s1) == getmetatable(s2))    --> true

Set.print(s1 + s2)    --> {1, 10, 20, 30, 50}
Set.print(s1 * s2)    --> {30}
Set.print(s1 - s2)    --> {20, 50, 10}
Set.print(s1 / s2)    --> {__div}
Set.print(-s2)    --> {__unm}
Set.print(s1 ^ s2)    --> {__pow}

Q:如何定义关系运算的”metamethods”?

A:继续借用上面”Q & A”中的例子,

-- 定义两个"table"比较大小的逻辑。"a"是否是"b"的子集。
Set.mt.__le = function (a,b)
  for k in pairs(a) do
    if not b[k] then return false end
  end
  return true
end

-- 定义两个"table"比较大小的逻辑。"a"是否是"b"的真子集。
Set.mt.__lt = function (a,b)
  return a <= b and not (b <= a)
end

-- 定义两个"table"比较大小的逻辑。两个集合是否相等。
Set.mt.__eq = function (a,b)
  return a <= b and b <= a
end

s1 = Set.new{2, 4}
s2 = Set.new{4, 10, 2}
print(s1 <= s2)       --> true
print(s1 < s2)        --> true
print(s1 >= s1)       --> true
print(s1 > s1)        --> false
print(s1 == s2 * s1)  --> true
print(s1 ~= s2)       --> true

Lua只提供__le为了<=__lt为了<__eq为了=这三个关系运算的”metamethod”。而例子中可以看到>=>~=也都能正常进行运算。这是因为Lua对他们进行了相应的转换,

a >= b    -->    b <= a
a > b     -->    b < a
a ~= b    -->    not (a == b)

Q:如何定义库函数相关的”metamethods”?

A:有两个库函数相关的”metamethod”可以在程序中操作,__tostring__metatable

--[[ 定义"__tostring"可以"print()"以什么样的格式输出数据。
     "print()"总是去调用"tostring()"以格式化它的输出。
     在格式化的过程中,"tostring()"首先查看被格式化的对象是否有一个"metatable",
     并且"metatable"中是否有"__tostring"(它必须是个函数)域。
     如果有,则"tostring()"以被格式化对象作为参数,调用这个"metamethod"。
     无论这个"metamethod"返回什么,都将作为"tostring()"的结果返回。]]
Set.mt.__tostring = Set.tostring
s1 = Set.new{10, 4, 5}
-- 定义了"__tostring"域,就无需使用"Set.print()"了,直接"print()"即可。
print(s1)    --> {4, 5, 10}
t = {}
--[[ 对象被分配了带有"__tostring"域的"metatable",
     才会被"Set.tostring()"格式化,其他的对象不受影响。]]
print(t)    --> table: 0x1a241e0

-- 定义"__metatable"可以保护"metatable"不被随意的修改。
--[[ 在这段代码之后的所有的"getmetatable()"会返回你设定的字符串
     (即"not your business"),
     而所有的"setmetatable()"会报错。]]
Set.mt.__metatable = "not your business"
s1 = Set.new{}
print(getmetatable(s1))    --> not your business
setmetatable(s1, {})    --> cannot change protected metatable

附加:

1、Lua中的每一个”table”都可以有他自己的”metatable”(之后还会看到”userdata”也可以有自己的”metatables”)。任何”table”都可以是其他”table”的”metatable”。一组有关系的”table”可以共享一个公用的”metatable”(公用的”metatable”描述他们共有的行为,比如”add”)。一个”table”也可以有自己独有的”metatable”,那么就是描述他自己独有的行为。
2、当我们对两个集合做算数运算时,Lua会调用正确的”metamethod”。但如果一个集合与一个常量做算数运算呢?比如,

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

当Lua遇到这种情况时会做以下选择(同时,这种选择适用于所有算数运算符,关系运算符以及..运算符的运算),
(1) 如果第一个值有”metatable”,并且其中有一个__add方法,那么Lua以此作为整个算数运算的”metamethod”。
(2) 如果第一种方法行不通,那么Lua查看第二个值中是否有”metatable”,并且其中有一个__add方法,如果有那么Lua以此作为整个算数运算的”metamethod”。
(3) 如果第二种方法也行不通,那么Lua报错。
所以上面的两行程序,Lua会调用”s”的”metatable”中的__add方法,之后会报错:

bad argument #1 to ‘pairs’ (table expected, got number)

Lua不会在意这种混合的类型(集合与常量),但是我们的程序应该在意(否则就像这样报错了)。所以程序中要对传入的参数之类型做检查,

function Set.union (a,b)
    if getmetatable(a) ~= Set.mt or
        getmetatable(b) ~= Set.mt then
        error("attempt to `add' a set with a non-set value", 2)
    end
    ...  -- same as before
end

3、”metatable”中还有个”metamethod”,”__concat”。如果设定了该”metamethod”,调用..时会被触发,

-- 我们规定"s1 .. s2"仅是打印一句话,以证明调用了正确的"metamethod"。
function Set.concat(a, b)
    local res = Set.new{"__concat"}
    return res
end

Set.mt.__concat = Set.concat

Set.print(s1 .. s2)    --> {__concat}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值