lua metatable

Lua中每个值都有一个元表metatable,这个元表metatable是一个原始的Lua表table,元表metatable用来定义原始值在特定操作下的行为。通过在元表metatable中的特定域设一些值来改变拥有这个元表metatable的值的指定操作的行为。我们称元表metatable中的键名为事件event,将其中的值叫做元方法metamethod

Lua5.1中元表metatable的表现行为类似于C++语言中的操作符重载,例如:重载__add元方法metamethod,来计算两个Lua数组的并集。重载__index元方法来定义自己的Hash函数。

Lua提供两个用来处理元表的方法

  • setmetatable(table, metatable) 为表设置元表metatable,不能从Lua中改变其它任何类型的值的元表metatable(使用debug库例外),要这样做的话必须使用C语言的API。
  • getmetatable(table) 获取表的元表metatable对象

设置元表

local tbl = {}
local metatbl = {}
local mytbl = setmetatable( tbl, metatbl )

元表(metatable)与 元方法(metamethod)

通常,Lua中每个值都有一套预定义的操作集合。例如,数字相加、连接字符串、表插入key-value对等,但无法将两个表相加,无法对函数比较等。此时,可以通过元表来修改一个值的行为,使其在面对一个非预定义的操作时执行一个指定的操作。

  • Lua中每个值都有一个元表metatable
    tableuserdata可以有各自独立的元表metatable,其他类型的值共享其类型所属的单一元表。
  • Lua中只能设置表table的元表metatable,设置其他类型值得元表必须通过C代码实现。
  • Lua在创建新的表table时不会创建元表metatable

表与元表

  • table可作为任何值得元表metatable,一组相关的表可以共享一个通用的元表,通用的元表描述了它们共同的行为。
  • table可作为自己的元表metatable,用于描述其特有的行为。

表的元表获取与修改

  • 使用setmettable来设置或修改表table的元表metatable
  • 使用getmettable来获取元表

Lua提供的所有操作符都可以被重载

  • __add 加法操作
  • __sub 减法操作
  • __mul 乘法操作
  • __div 除法操作
  • __mod 求模取余操作
  • __pow 乘幂操作
  • __unm 一元负数操作
  • __concat 字符串连接操作
  • __len 长度获取
  • __eq 相等比较,getcomphandler函数定义了Lua怎样选择一个处理器来做比较操作,仅在两个对象类型相同且有对应操作相同的元方法时才起效。
  • __lt 小于比较
  • __le 小于等于比较

除了操作符之外,如下元方法也可以被重载。

  • __index 取下标操作用于访问table[key]
  • __newindex 赋值给指定下标table[key]=value
  • __tostring 转换成字符串
  • __call 当Lua调用一个值时调用
  • __mode 用于弱表week table
  • __metatable 用于保护metatable不被访问

算术运算

local x = 10
local y = 10
print(x+y) -- 20

表的加法运算

当Lua试图将两个表相加时,会首先检查两个表之一是否有元素,然后检查该元表中是否具有一个叫做__add的字段。如果Lua找到了该字段,则会调用该字段对应的值。这个值就是所谓的“元方法”。

local tbl1 = {1,2,3}
local tbl2 = {3,2,1}
-- print(#tbl1, #tbl2)

-- 无法直接相加两个表
-- printf(tbl1 + tbl2)

-- 实现表的相加操作
mt = {}
mt.__add = function(x, y)
    local result = {}
    local length = #x
    for i=1,length do
        result[i] = x[i] + y[i]
    end
    return result
end
-- test
-- 设置表的元表
setmetatable(tbl1, mt)
setmetatable(tbl2, mt)
-- 执行表的相加操作
local result = tbl1  + tbl2
-- 循环输出
for k,v in ipairs(result) do
    print(k, v)
end

表的连接

-- 表的连接
local tbl1 = {1,2,3}
local tbl2 = {3,2,1}

-- 实现表的相加操作
mt = {}
mt.__add = function(x, y)
    local result = {}
    local length = #x
    for i=1,length do
        result[i] = x[i] + y[i]
    end
    return result
end
-- 实现表的连接操作
mt.__concat = function(x, y)
    local length = #x
    local result = {}
    for i=1,length do
        result[i] = x[i]..y[i]
    end
    return result
end
-- 设置表的元表
setmetatable(tbl1, mt)
setmetatable(tbl2, mt)
-- 执行表的连接操作
local result = tbl1..tbl2
-- 循环输出
for k,v in ipairs(result) do
    print(k, v)
end
--[[
1   13
2   22
3   31
--]]

集合运算

使用表table表示集合,实现集合计算中的并集、交集等。

-- 定义集合
Set = {}
-- 定义集合的元表
local mt = {}


-- 创建集合,根据参数列表值创建新集合
function Set.new(arr)
    local set = {}
    setmetatable(set, mt) --创建集合均具有一个相同的元表
    for i,v in ipairs(arr) do
        set[v] = true
    end
    return set
end
-- 求集合并集
function Set.union(x, y)
    local result = Set.new{}
    for k,v in pairs(x) do
        result[k] = true
    end
    for k,v in pairs(y) do
        result[k] = true
    end
    return result
end
-- 求集合交集
function Set.intersection(x, y)
    -- 创建空集合
    local result = Set.new{}
    for k in pairs(x) do
        result[k] = y[k]
    end
    return result
end
-- 将集合转换为字符串
function Set.tostring(set)
    -- 定义存放集合中所有元素的列表
    local result = {}
    for k in pairs(set) do
        result[#result + 1] = k
    end
    return "{"..table.concat(result, ", ").."}"
end
-- 打印输出集合元素
function Set.print(set)
    print(Set.tostring(set))
end

-- 设置集合元方法
mt.__add = Set.union

-- 测试
local set1 = Set.new{10,20,30}
local set2 = Set.new{30, 1}

-- 判断集合是否具有相同元表
-- print(getmetatable(set1), getmetatable(set2))
-- table: 0000000000bb9af0  table: 0000000000bb9af0

Set.print(set1 + set2) -- {20, 1, 10, 30}
Set.print(Set.intersection(set1, set2)) -- {30}

__index

当访问表中不存在字段时会得到nil的结果,实际上访问表时解释器会去查找__index元方法。如果没有__index原方法则返回nil,否则输出结果。

实现在表中查找键不存在时转而在元表中查找该键

-- 原始表
local oriTable = {key = "value"}
-- 元表
local metaTable = {
    -- 重载函数
    __index = function(self, key)
        if key=="key" then
            return "metatable value"
        end
    end
}
local mytbl = setmetatable( oriTable, metaTable )
print(mytbl.key, mytbl.key2)-- value    nil

场景:继承

示例:创建描述窗口的表,每个表具有窗口的必备属性和默认值,例如大小、颜色、位置等。我们希望创建窗口对象时,仅指定不同于默认值的参数。

实现方式1:使用构造函数,输入不存在的字段。

实现方式2:新窗口从原型窗口处继承不存在的字段

-- 声明原型
Window = {} -- 创建命名空间
-- 使用默认值创建原型
Window.prototype = {x=0, y=0, width=0, height=0}
-- 创建原型的元表
Window.mt = {}
-- 定义访问表的方法
-- 当Lua检测到当前对象中不存在某字段,但在其metatable中却存在__index字段时,Lua会以指定函数来调用__index。
Window.mt.__index = function(table, key)
    return Window.prototype[key]
end
-- 声明原型的构造函数
function Window.new(tbl)
    setmetatable(tbl, Window.mt)
    return tbl
end

-- 测试:创建新窗口并查询字段
local window = Window.new{x=100, y=100}
print(window.x, window.y, window.width, window.height) -- 100 100 0   0

--判断表中是否存在元素
print(window.x) -- 100

-- 获取元表的值
print(rawget(window, x)) -- nil

Lua中,将__index用于继承是很普遍的的方式,Lua还提供一种更为便捷的方式实现继承。__index不必一定是一个函数,可以是一个table

  • 当是函数时,Lua以table和不存在的key作为参数来调用该函数。
  • 当是表时,Lua以相同的方式重新访问这个表。
-- 声明原型
Window = {} -- 创建命名空间
-- 使用默认值创建原型
Window.prototype = {x=0, y=0, width=0, height=0}
-- 创建原型的元表
Window.mt = {}
-- 定义访问表的方法
-- 当Lua查找metatable的__index字段时,若__index字段值是一个table,则会在原型中继续查找,也就是说Lua会在table中重复这个访问过程。
Window.mt.__index = Window.prototype
-- 声明原型的构造函数
function Window.new(tbl)
    setmetatable(tbl, Window.mt)
    return tbl
end

-- 测试:创建新窗口并查询字段
local window = Window.new{x=100, y=100}
print(window.x, window.y, window.width, window.height) -- 100 100 0   0

--判断表中是否存在元素
print(window.x) -- 100

table作为__index是一种快捷的实现单一继承的方式,虽然将函数作为__index来实现的方式会带来性能上的较大开销,但函数更加灵活,可通过函数来实现多继承、缓存等功能。

Window.mt.__index = Window.prototype

Window.mt.__index = function(table, key)
    return Window.prototype[key]
end

如果不想在访问table时涉及__index,可使用rawget(table, key)进行原始的访问,也就是不考虑metatable的简单访问,一次原始访问并不会加速代码执行,但有时会使用到。

-- 获取原始值即元表中的值
print(rawget(window, x)) -- nil

设置表字段的默认值

local tbl = {x=1, y=2}
-- table中字段默认值为nil
print(tbl.x, tbl.y, tbl.z) -- 1 2 nil
-- 通过metatable修改table的默认值
function setTableDefault(tbl, default)
    local mt = {
        __index = function()
            return default 
        end
    }
    setmetatable(tbl, mt)
end
-- 调用setTableDefault后,任何对tbl中存在的字段的访问都回调用它的__index
setTableDefault(tbl, 0)
print(tbl.x, tbl.y, tbl.z) -- 1 2 0

__newindex

__newindex__index类似,不同之处在于__newindex用于table的更新,__index用于table的查询。

table中不存在的索引赋值时,解释器会查找__nexindex,若存在则直接调用而不执行赋值。若__newindex是一个table,解释器会在此table中执行赋值,而非原始的table

-- 定义原表
local mt = {}
mt.__index = function(tbl, key)
    return mt[key]
end
mt.__newindex = function(tbl, key, value)
    mt[key] = value
    print(string.format("modify: key=%s value=%s", key, val))
end

local window = {x=1}
setmetatable(window, mt)

print(window.x) -- 1
print(rawget(window, x)) -- nil

-- 添加属性
window.y = 2
print(window.y) -- 2
print(rawget(window, y)) -- nil

可通过调用rawset(table, key, value)绕过__newindex(),即可不涉及任何原方法而直接设置table中的键值对。

__tostring

-- 定义原表
local mt = {}
mt.__tostring = function(tbl)
    local result = {}
    for k,v in pairs(tbl) do
        result[#result+1] =  k.."="..v
    end
    return "{"..table.concat(result,",").."}"
end

local window = {x=1}
setmetatable(window, mt)

print(window.x) -- 1
print(rawget(window, x)) -- nil

-- 添加属性
window.y = 2
print(window.y) -- 2
print(rawget(window, y)) -- nil
-- 打印
-- print(window) -- tab-- le: 0000000000b994f0
for k,v in pairs(window) do
    print(k, v)
end
print(window) -- {x=1,y=2}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容

  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,770评论 0 38
  • 前言 元表对应的英文是metatable,元方法是metamethod。我们都知道,在C++中,两个类是无法直接相...
    BobWong阅读 1,036评论 0 9
  • Lua 元表(Metatable) 作者:杨梦鸽 校对:翟舒青 在 Lua table 中我们可以访问对应的key...
    木易林1阅读 509评论 0 2
  • table 作为 Lua 中唯一的数据结构,我们可以利用 table 实现面向对象编程中的类、继承、多重继承等等。...
    eddy_wiki阅读 4,148评论 0 7
  • 元表这个词听起来就觉得抽象,我开始接触Lua的时候就是这种感觉。其实不要被表面吓到。 元表就是,如果一个table...
    程序员喜欢自嘲阅读 11,873评论 0 6