table 是 lua 的重要数据类型,lua 本身的很多特性都与 table 相关,如 lua 运行时的环境参数、lua 的 module 和 package 等。借助 metatable*,可使 *lua 具备面向对象的部分特性,提供诸如 class/object、inheritance 等功能,使 lua 能够胜任更多的应用场合。
table 基础
lua table 有两种表现形式,一是数组形式,二是 (key, value) 键值对形式,如下所示键值对 (key, value) 中,key 可以是任意 lua 数据类型,因此数组形式的 table 可理解为该 table 的 key 是一连串从1开始的连续的整型数字。(key, value) 是 table 一般表现形式。-- 数组形式的 table: t1
t1 = {}
t1[1] = 1
t1[2] = 2
t1[3] = 3
-- 键值对形式的 table: t2
t2 = {}
t2["k1"] = "hello"
t2["k2"] = "world"
虽然 key 可以是任意 lua 数据类型,但针对不同数据类型的 key,lua 环境提供了一些操作特性。
当 key 是从 1 开始的连续整型数字时,#t1
可获取 t1 内成员的数量,否则#t1
的值为 0。
当 key 是字符串时,可通过点 . 操作符来获取或设置该键所对应的值,如下所示最后,不要忽略 key 可以是任意 lua 数据类型的特性。在程序设计时,可能需要某些 object 作为 keyt2 = {}
t2["k1"] = "hello"
t2.k2 = "world"
print(t2.k1, t2.k2)table 的 metatable 的成员
metatable 也是普通的 lua table*,当 *metatable 设置了特定的成员后,它将决定某些 table 的行为,前提是这些 table 设置了该 metatable。
默认情况下,table 的 metatable 为 nil*,获取/设置 *metatable 的方法如下所示metatable 的成员被称为 metamethod*,在操作 *table 时,lua 会检查对应的 metatable 以及该 metatable 所包含的 metamethod*,当条件满足时,调用 *metamethod 完成相应操作。t = {}
m = {}
getmetatable(t) -- return nil
setmetatable(t, m) -- set m as t's metatable
按功能划分,metamethod 可分为三类,一类负责运算符重定义,一类负责系统库函数重定义,一类负责 table 成员的访问。metamethod 分类一:运算符重定义
此类 metathod 类似于 c++ 的运算符重载,使用加减乘除等运算符来替代函数调用,简化代码编写。
可重定义的运算符包括算术运算符,如数字运算:加、减、乘、除,位运算:按位取反、按位与、左移等;还包括逻辑运算,如大于、等于、小于、不等于等。
示例代码如下所示:m = {}
-- 重定义逻辑运算:< (小于)
m.__lt = function(a, b)
return a.v < b.v
end
t1 = { v = 20 }
t2 = { v = 5 }
setmetatable(t1, m)
setmetatable(t2, m)
res = t1 < t1 -- res 值为 falsemetamethod 分类二:系统库重定义
目前 lua 提供三个系统库的重定义,分别是__tostring
、__pairs
和__metatable
。__tostring
:如果print(t)
中 t 的 metatable 定义了__tostring
,则print
函数将自动调用__tostring
。__pairs
:如果for k, v in pairs(t) do
中 t 的 metatable 定义了__pairs
,则pairs
函数自动调用__pairs
。__metatable
:供getmetatable
和setmetatable
函数使用。调用getmetatable(t)
函数,如果 t 的 metatable 定义了__metatable
,则返回__metatable
的print
结果。调用setmetatable(t, m)
函数,如果 t 的 metatable 定义了__metatable
,则返回报错信息,提示 t 的 metatable 不允许修改。
需要注意的是,__pairs
和__tostring
必须定义成 function 类型,而__metatable
可以是任意 lua 数据类型。metamethod 分类三:table 成员访问
__index
和__newindex
控制 table 的成员访问。__index
负责 table 成员读,如print(t.a)
将读取 table t 的成员 a。__newindex
负责 table 成员的写,如t.a = 3
把 table t 的成员 a 设置为 3。注,lua 使用赋值来创建和删除 table 成员,如t.a = 3
时,如果t.a
不存在,则 lua 自动创建t.a
并赋值;又如t.a = nil
,则 lua 自动删除t.a
成员。
虽然称__index
和__newindex
为 metamethod*,但是__index
和__newindex
可以是 *function 类型,也可以是 table 类型。table 成员访问行为
table 成员访问行为分为 table 成员的读、写和行为传导。读 table 成员
当 table 的 metatable 为nil
或 metatable 的__index
成员为nil
时,将按照 table 成员的实际情况访问 table 的成员。如:当 table 的 metatable 的t = {}
t.a = "a" -- 写
print(t.a, t.b) -- 读,t.a = "a", t.b = nil__index
值为 function 时,并且读取的 table 成员不存在时,lua 将自动调用__index
所指向的函数。如:注,t1 = {}
t1.a = 5
m = {}
m.__index = function(t, k)
print(t, k)
end
setmetatable(t1, m)
print(t1.a) -- 读,t1.a 存在,不触发 __index,t1.a == 5
print(t1.b) -- 读,t1.b 不存在,将调用 m.__index 所指向的函数__index
指向函数的声明形式为function(t, k)
,其中,t 为设置了该 metatable 的 table,k 为所需访问该 table 成员的名称,或理解为 table (key, value) 键值对中的 key*,可通过t[k]
引用。
当 *table t1 的 metatable 的__index
值为另一 table(设为 *x) 时,并且读取的 *t1 成员 t1.b 不存在时,lua 将自动查找 x 对应成员名称,并返回之,即x.b
。如:-- metatable settings
x = {}
x.b = "hello"
m = {}
m.__index = x
-- table settings
t1 = {}
t1.a = 5
setmetatable(t1, m)
print(t1.a) -- 读,t1.a 存在,不触发 __index,t1.a == 5
print(t1.b) -- 读, t1.b 不存在,return value == x.b == "hello"
print(t1.c) -- 读, t1.c 不存在,return value == x.c == nil
for k, v in pairs(t1) do
print(k, v)
end
-- result: a 5rawget
函数的使用。当读取 table t1 成员t1.a
时,无论采取t1.a
或t1["a"]
形式,都会触发 metatable 的__index
机制。有些场合不需要该机制(或者说不能使用该机制),此时rawget
将派上用场。rawget(t1, "a")
将获取t1.a
但不触发__index
。如:正确做法是:m = {}
m.__index = function(t, k)
return t[k] -- 此方式触发 t 的 metatable 的 __index 机制,会出现死循环
end
t1 = {}
setmetatable(t1, m)
print(t1.a) -- stdin:2: C stack overflowm = {}
m.__index = function(t, k)
return rawget(t, k) -- rawget 单纯读取 t1.a,不会触发 t 的 metatable 的 __index 机制
end
t1 = {}
setmetatable(t1, m)
print(t1.a)写 table 成员
当 table 的 metatable 为nil
或 metatable 的__newindex
成员为nil
时,将按照 table 成员的实际情况访问 table 的成员。如:当 table 的 metatable 的t = {}
t.a = "a" -- 写
print(t.a, t.b) -- 读,t.a = "a", t.b = nil__newindex
值为 function 时,并且访问的 table 成员不存在时,lua 将自动调用__newindex
所指向的函数。如:注,t1 = {}
t1.a = 5 -- 写,在设置 metatable 前 t1 的 metatable 为 nil,此时写不会触发 __newindex
m = {}
m.__newindex = function(t, k, v)
print(t, k, v) -- 打印 t, k, v,并未做实际写,此时 t["k"] 值为 nil
end
setmetatable(t1, m)
t1.a = 10 -- 写,t1.a 存在,不触发 __newindex,t1.a == 10
t1.b = "hello" -- 写,t1.b 不存在,将调用 m.__newindex 所指向的函数__newindex
指向函数的声明形式为function(t, k, v)
,其中,t 为设置了该 metatable 的 table,k 为所需访问该 table 成员的名称,v 为所需设置的值,或理解为 table (key, value) 键值对中的 key 和 value*。
当 *table t1 的 metatable 的__newindex
值为另一 table(设为 *x) 时,并且写入的 *t1 成员 t1.b 不存在时,lua 将自动查找 x 对应成员名称,并写入之,即x.b=?
。如:-- metatable settings
x = {}
x.b = "hello"
m = {}
m.__newindex = x
-- table settings
t1 = {}
t1.a = 5 -- 写,在设置 metatable 前 t1 的 metatable 为 nil,此时写不会触发 __newindex
setmetatable(t1, m)
t1.a = 10 -- 写,t1.a 存在,不触发 __newindex,t1.a == 10
t1.b = "HELLO" -- 写, t1.b 不存在,写入x,x.b == "HELLO"
t1.c = "world" -- 写,t1.c 不存在,写入x,x.c = "world"
print(t1.c) -- 读,return value == x.c == nil, t1.c 不存在
for k, v in pairs(t1) do
print(k, v)
end
-- result: a 10
for k, v in paris(x) do
print(k, v)
end
-- result: b HELLO
-- c worldrawset
函数的使用。当写入 table t1 成员t1.a
时,无论采取t1.a=?
或t1["a"]=?
形式,都会触发 metatable 的__newindex
机制。有些场合不需要该机制(或者说不能使用该机制),此时rawset
将派上用场。rawset(t1, "a", ?)
将写入t1.a=?
但不触发__newindex
。如:正确做法是:m = {}
m.__newindex = function(t, k, v)
t[k] = v -- 此方式触发 t 的 metatable 的 __newindex 机制,会出现死循环
end
t1 = {}
setmetatable(t1, m)
t1.a = 5 -- stdin:2: C stack overflowm = {}
m.__newindex = function(t, k, v)
rawset(t, k, v) -- rawset 单纯写入 t1.a,不会触发 t 的 metatable 的 __newindex 机制
end
t1 = {}
setmetatable(t1, m)
t1.a = 5行为传导
当__index
和__newindex
是 lua 的 table 类型时,__index
和__newindex
也能够设置 metatable*,在这种情况下,会出现__index
和__newindex
机制的传导现象。在 *lua 5.3 下做如下实验:
读,链式传导读,环型传导-- metatable1 settings
x1 = {}
x1.b = "b"
m1 = {}
m1.__index = x1
-- metatable2 settings
x2 = {}
x2.c = "c"
m2 = {}
m2.__index = x2
-- create link
setmetatable(x1, m2)
-- table settings
t1 = {}
t1.a = "a"
setmetatable(t1, m1)
print(t1.a, t1.b, t1.c, t1.d) -- result: a b c nil
for k, v in pairs(t1) do
print(k, v)
end
-- result: a a
-- 读,链式传导,正常,传导至最末端写,链式传导-- metatable settings
x = {}
x.b = "b"
m = {}
m.__index = x
-- create ring
setmetatable(x, m)
-- table settings
t = {}
t.a = "a"
setmetatable(t, m)
print(t.a, t.b)
-- result: a b
-- 读取存在的 key,正常
print(t.c)
-- result: 报错,stdin:1: '__index' chain too long ...
-- 读取不存在的 key,导致死循环写,环型传导-- metatable1 settings
x1 = {}
x1.b = "b"
m1 = {}
m1.__newindex = x1
-- metatable2 settings
x2 = {}
x2.c = "c"
m2 = {}
m2.__newindex = x2
-- create link
setmetatable(x1, m2)
-- table settings
t1 = {}
t1.a = "a"
setmetatable(t1, m1)
t1.a = "aa"
t1.b = "bb"
t1.c = "cc"
t1.d = "dd"
for k, v in pairs(t1) do
print(k, v)
end
-- result: a aa
for k, v in pairs(x1) do
print(k, v)
end
-- result: b bb
for k, v in pairs(x2) do
print(k, v)
end
-- result: c cc
-- d dd
-- 写,链式传导,正常,传导至最末端结论如下:-- metatable settings
x = {}
x.b = "b"
m = {}
m.__newindex = x
-- create ring
setmetatable(x, m)
-- table settings
t = {}
t.a = "a"
setmetatable(t, m)
t.a = "aa"
t.b = "bb"
for k, v in pairs(t) do
print(k, v)
end
-- result: a aa
for k, v in pairs(x) do
print(k, v)
end
-- result: b bb
-- 写入存在的 key,正常
t.c = "cc"
-- 报错:stdin:1: '__index' chain too long ...
-- 写入不存在的 key,导致死循环
把 metatable 的__index
和__newindex
作为 table 来使用时,metatable 行为在__index
和__newindex
上也会出现,并层层传导至最末端。应尽量避免出现环型传导。面向对象基础(读,链式传导的紧凑形式)
从面向对象的角度观察上述代码,t 继承了 m2 的成员,m2 继承了 m1 成员,实现了简单的继承特性。m1 = {}
m1.b = "b"
m1.__index = m1 -- 自身作为 __index
m2 = {}
m2.c = "c"
m2.__index = m2 -- 自身作为 __index
setmetatable(m2, m1)
t = {}
t.a = "a"
setmetatable(t, m2)
print(t.a, t.c, t.b)
结束语
注意 table、metatable 和 metamethod 之间的关系:table 可设置 metatable, metatable 是普通的 table 数据类型,metamethod 是 metatable 的成员。
可利用 metamethod*:__index
和__newindex
控制 *table 成员的访问,并实现面向对象特性。