咱们可使用操做符对 Lua 的值进行运算,例如对数值类型的值进行加减乘除的运算操做以及对字符串的链接、取长操做等(在 Lua 学习笔记(三)—— 表达式 中介绍了许多相似的运算)。元表正是定义这些操做行为的地方。segmentfault
元表本质上是一个普通 Lua 表。元表中的键用来指定操做,称为“事件名”;元表中键所关联的值称为“元方法”,定义操做的行为。函数
仅表(table)类型值对应的元表可由用户自行定义。其余类型的值所对应的元表仅能经过 Debug 库进行修改。学习
元表中的事件名均以两条下划线 __
做为前缀,元表支持的事件名有以下几个:code
__index -- 'table[key]',取下标操做,用于访问表中的域 __newindex -- 'table[key] = value',赋值操做,增改表中的域 __call -- 'func(args)',函数调用,(参见 《Lua 学习笔记(三)—— 表达式》中的函数部分介绍) -- 数学运算操做符 __add -- '+' __sub -- '-' __mul -- '*' __div -- '/' __mod -- '%' __pow -- '^' __unm -- '-' -- 链接操做符 __concat -- '..' -- 取长操做符 __len -- '#' -- 比较操做符 __eq -- '==' __lt -- '<' -- a > b 等价于 b < a __le -- '<=' -- a >= b 等价于 b <= a
还有一些其余的事件,例如 __tostring
和 __gc
等。对象
下面进行详细介绍。three
每一个值均可以拥有一个元表。对 userdata 和 table 类型而言,其每一个值均可以拥有独立的元表,也能够几个值共享一个元表。对于其余类型,一个类型的值共享一个元表。例如全部数值类型的值会共享一个元表。除了字符串类型,其余类型的值默认是没有元表的。事件
使用 getmetatable 函数能够获取任意值的元表。
使用 setmetatable 函数能够设置表类型值的元表。(这两个函数将在[基础函数库]部分进行介绍)字符串
只有字符串类型的值默认拥有元表:get
a = "5" b = 5 c = {5} print(getmetatable(a)) --> table: 0x7fe221e06890 print(getmetatable(b)) --> nil print(getmetatable(c)) --> nil
事先提醒 Lua 使用 raw
前缀的函数来操做元方法,避免元方法的循环调用。数学
例如 Lua 获取对象 obj 中元方法的过程以下:
rawget(getmetatable(obj)or{}, "__"..event_name)
index 是元表中最经常使用的事件,用于值的下标访问 -- table[key]
。
事件 index 的值能够是函数也能够是表。当使用表进行赋值时,元方法可能引起另外一次元方法的调用,具体可见下面伪码介绍。
当用户经过键值来访问表时,若是没有找到键对应的值,则会调用对应元表中的此事件。若是 index 使用表进行赋值,则在该表中查找传入键的对应值;若是 index 使用函数进行赋值,则调用该函数,并传入表和键。
Lua 对取下标操做的处理过程用伪码表示以下:
function gettable_event (table, key) -- h 表明元表中 index 的值 local h if type(table) == "table" then -- 访问成功 local v = rawget(table, key) if v ~= nil then return v end -- 访问不成功则尝试调用元表的 index h = metatable(table).__index -- 元表不存在返回 nil if h == nil then return nil end else -- 不是对表进行访问则直接尝试元表 h = metatable(table).__index -- 没法处理致使出错 if h == nil then error(···); end end -- 根据 index 的值类型处理 if type(h) == "function" then return h(table, key) -- 调用处理器 else return h[key] -- 或是重复上述操做 end end
使用表赋值:
t = {[1] = "cat",[2] = "dog"} print(t[3]) --> nil setmetatable(t, {__index = {[3] = "pig", [4] = "cow", [5] = "duck"}}) print(t[3]) --> pig
使用函数赋值:
t = {[1] = "cat",[2] = "dog"} print(t[3]) --> nil setmetatable(t, {__index = function (table,key) key = key % 2 + 1 return table[key] end}) print(t[3]) --> dog
newindex 用于赋值操做 -- talbe[key] = value
。
事件 newindex 的值能够是函数也能够是表。当使用表进行赋值时,元方法可能引起另外一次元方法的调用,具体可见下面伪码介绍。
当操做类型不是表或者表中尚不存在传入的键时,会调用 newindex 的元方法。若是 newindex 关联的是一个函数类型之外的值,则再次对该值进行赋值操做。反之,直接调用函数。
~~不是太懂:一旦有了 "newindex" 元方法, Lua 就再也不作最初的赋值操做。 (若是有必要,在元方法内部能够调用 rawset 来作赋值。)~~
Lua 进行赋值操做时的伪码以下:
function settable_event (table, key, value) local h if type(table) == "table" then -- 修改表中的 key 对应的 value local v = rawget(table, key) if v ~= nil then rawset(table, key, value); return end -- h = metatable(table).__newindex -- 不存在元表,则直接添加一个域 if h == nil then rawset(table, key, value); return end else h = metatable(table).__newindex if h == nil then error(···); end end if type(h) == "function" then return h(table, key,value) -- 调用处理器 else h[key] = value -- 或是重复上述操做 end end
元方法为表类型:
t = {} mt = {} setmetatable(t, {__newindex = mt}) t.a = 5 print(t.a) --> nil print(mt.a) --> 5
经过两次调用 newindex 元方法将新的域添加到了表 mt 。
+++
元方法为函数:
-- 对不一样类型的 key 使用不一样的赋值方式 t = {} setmetatable(t, {__newindex = function (table,key,value) if type(key) == "number" then rawset(table, key, value*value) else rawset(table, key, value) end end}) t.name = "product" t[1] = 5 print(t.name) --> product print(t[1]) --> 25
call 事件用于函数调用 -- function(args)
。
Lua 进行函数调用操做时的伪代码:
function function_event (func, ...) if type(func) == "function" then return func(...) -- 原生的调用 else -- 若是不是函数类型,则使用 call 元方法进行函数调用 local h = metatable(func).__call if h then return h(func, ...) else error(···) end end end
因为用户只能为表类型的值绑定自定义元表,所以,咱们能够对表进行函数调用,而不能把其余类型的值当函数使用。
-- 把数据记录到表中,并返回数据处理结果 t = {} setmetatable(t, {__call = function (t,a,b,factor) t.a = 1;t.b = 2;t.factor = factor return (a + b)*factor end}) print(t(1,2,0.1)) --> 0.3 print(t.a) --> 1 print(t.b) --> 2 print(t.factor) --> 0.1
运算操做符相关元方法天然是用来定义运算的。
以 add 为例,Lua 在实现 add 操做时的伪码以下:
function add_event (op1, op2) -- 参数可转化为数字时,tonumber 返回数字,不然返回 nil local o1, o2 = tonumber(op1), tonumber(op2) if o1 and o2 then -- 两个操做数都是数字? return o1 + o2 -- 这里的 '+' 是原生的 'add' else -- 至少一个操做数不是数字时 local h = getbinhandler(op1, op2, "__add") -- 该函数的介绍在下面 if h then -- 以两个操做数来调用处理器 return h(op1, op2) else -- 没有处理器:缺省行为 error(···) end end end
代码中的 getbinhandler 函数定义了 Lua 怎样选择一个处理器来做二元操做。 在该函数中,首先,Lua 尝试第一个操做数。若是这个操做数所属类型没有定义这个操做的处理器,而后 Lua 会尝试第二个操做数。
function getbinhandler (op1, op2, event) return metatable(op1)[event] or metatable(op2)[event] end
+++
对于一元操做符,例如取负,Lua 在实现 unm 操做时的伪码:
function unm_event (op) local o = tonumber(op) if o then -- 操做数是数字? return -o -- 这里的 '-' 是一个原生的 'unm' else -- 操做数不是数字。 -- 尝试从操做数中获得处理器 local h = metatable(op).__unm if h then -- 以操做数为参数调用处理器 return h(op) else -- 没有处理器:缺省行为 error(···) end end end
加法的例子:
t = {} setmetatable(t, {__add = function (a,b) if type(a) == "number" then return b.num + a elseif type(b) == "number" then return a.num + b else return a.num + b.num end end}) t.num = 5 print(t + 3) --> 8
取负的例子:
t = {} setmetatable(t, {__unm = function (a) return -a.num end}) t.num = 5 print(-t) --> -5
对于 tostring 操做,元方法定义了值的字符串表示方式。
例子:
t = {num = "a table"} print(t) --> table: 0x7f8e83c0a820 mt = {__tostring = function(t) return t.num end} setmetatable(t, mt) print(tostring(t)) --> a table print(t) --> a table
对于三种比较类操做,均须要知足两个操做数为同类型,且关联同一个元表时才能使用元方法。
对于 eq (等于)比较操做,若是操做数所属类型没有原生的等于比较,则调用元方法。
对于 lt (小于)与 le (小于等于)两种比较操做,若是两个操做数同为数值或者同为字符串,则直接进行比较,不然使用元方法。
对于 le 操做,若是元方法 "le" 没有提供,Lua 就尝试 "lt",它假定 a <= b 等价于 not (b < a) 。
等于比较操做:
t = {name="number",1,2,3} t2 = {name = "number",4,5,6} mt = {__eq = function (a,b) return a.name == b.name end} setmetatable(t,mt) -- 必需要关联同一个元表才能比较 setmetatable(t2,mt) print(t==t2) --> true
对于链接操做,当操做数中存在数值或字符串之外的类型时调用该元方法。
对于取长操做,若是操做数不是字符串类型,也不是表类型,则尝试使用元方法(这致使自定义的取长基本没有,在以后的版本中彷佛作了改进)。
取长操做:
t = {1,2,3,"one","two","three"} setmetatable(t, {__len = function (t) local cnt = 0 for k,v in pairs(t) do if type(v) == "number" then cnt = cnt + 1 print(k,v) end end return cnt end}) -- 结果是 6 而不是预期中的 3 print(#t) --> 6