table在lua中占据着举足轻重的地位,它不仅仅是一种数据结构,也是实现其他更高级数据结构的基础。除了作为数据结构使用,运算符重载,全局变量,弱引用都依赖于table,table也是lua中实现面向对象技术的关键。可以说,lua中的table是万能的。
本文由基本语法到高级用法,详细讲解了lua中的table,有关代码细节的解释写成了注释。 示例代码在lua5.3上测试通过,建议直接贴到lua解释器中执行。
数据结构
table是lua中惟一的数据结构,其它的一切数据结构都是基于table。
关联数组
table最基本的用法是作为关联数组。
健和值可以是任何nil以外的值
-- 创建table的唯一方式是使用constructor表达式
-- 最简单的形式就是{},每次都会创建一个新的table
a = {}
-- 同一个table中的健和值可以是任何nil以外的值,包括function和其他table
a["x"] = "x"
a[-1] = -1
a[true] = true
print(a["x"]) -->x
-- 没有赋值的健对应nil
print(a["y"]) -->nil
print(a[-1]) -->-1
print(a[true]) -->true
-- 赋值为nil可以删除某个健
a[true] = nil
-- lua提供的语法糖:a.name等价于a["name"]
-- 不要把a.x和a[x]混淆
print(a.x) -->x
-- 0和'0'在表中代表不同的健,一定要小心否则很容易引入bug
a[0] = 'integer 0'
a['0'] = 'string 0'
print(a[0]) -->integer 0
print(a['0']) -->string 0
table是对象,通过引用操作
-- 在lua中,table是对象
a = {}
-- 对象通过引用来操作,不会发生拷贝
-- a和b指向同一个对象
b = a
print(b == a) -->true
b["x"] = "y"
print(a["x"]) -->y
-- a还在引用该table,如果一个table没有引用的时候,lua会删掉它并收回内存
b = nil
print(a["x"]) -->y
-- 创建两个独立的对象
t1 = {}
t2 = {}
-- t1和t2引用不同的对象
print(t1 == t2) -->false
a={}
a[t1] = 1
print(a[t2]) -->nil
使用table constructor创建table
-- constructor提供了两种创建table的形式
-- list-style
{"red", "green", "blue"}
-- record-style
{x=0, y=0}
-- 等价于
a = {}; a.x=0; a.y=0
-- 两种写法可以混合起来用
-- 可以嵌套,one的索引是1,two的索引是2,不受record的影响
{"one", x={1,'x'}, y=2, "two"}
-- 当健不是字符串或是某些特殊字符的时候,需要将健写成表达式——用[]扩起来
opnames = {[0] = 0, ["+"] = "add"}
-- 上面的list-style和record-style例子分别等价于以下写法
{[1]="red", [2]="green", [3]="blue"}
{["x"]=0, ["y"]=0}
-- ,也可以用代替;替代,常用来分割不同的部分
-- 末尾出现,也是允许的
{x=10, y=45; "one", "two", "three",}
函数作为值
-- functions are first-class values, they can be stored in table
a = {}
a.foo = function (x,y) return x + y end
print(a.foo(1,2)) -->3
-- another syntax to define such functions
function a.bar(x,y)
return x*y
end
print(a.bar(2,3)) -->6
数组
实现数组只需要使用从1开始递增的整数索引table,从1开始是一种惯例,lua的很多函数都从1开始索引一个数组。
-- ipairs函数会从1开始遍历,遇到一个nil值就停止
-- 遍历数组不要用pairs,因为table是无序的,用table实现的数组也是无序的
-- ipairs使用1,2,...作为健按顺序索引,而pairs使用一种无法预期的顺序索引
arr = {}
for i=1, 5 do
arr[i] = i
end
arr[0] = 0
arr[10] = 10
for i,v in ipairs(arr) do
print(i, v)
end
-- 使用constructors创建数组
-- 第一个元素的索引是1
arr = {1, '2', 3.0, false}
for i,v in ipairs(arr) do
print(i, v)
end
table库包含了操作 数组 的函数:concat,insert,remove,sort,unpack等,后面会用到。
table的长度
当table作为数组时,即索引从1开始递增,使用#
操作符可以返回数组的长度。
如果要计算所有元素数,只能使用pair遍历计数。
-- #返回从1开始非nil值的元素数
t={1,2,3}
print(#t)
--> 3
t[-1]=-1
t[0]=0
print(#t)
--> 3
t[5] = 5
print(#t)
--> 3
t['a'] = 'aaa'
print(#t)
--> 3
function tablelength(T)
local count = 0
for _ in pairs(T) do count = count + 1 end
return count
end
print(tablelength(t))
--> 7
栈和队列
基于table库提供的insert和sort操作,可以很容易实现简单的堆和栈。
-- ipairs函数会从1开始遍历,遇到一个nil值就停止
-- 遍历数组不要用pairs,因为table是无序的,用table实现的数组也是无序的
-- ipairs使用1,2,...作为健按顺序索引,而pairs使用一种无法预期的顺序索引
arr = {}
for i=1, 5 do
arr[i] = i
end
arr[0] = 0
arr[10] = 10
for i,v in ipairs(arr) do
print(i, v)
end
-- 使用constructors创建数组
-- 第一个元素的索引是1
arr = {1, '2', 3.0, false}
for i,v in ipairs(arr) do
print(i, v)
end
矩阵
实现矩阵有两种方式:
数组的数组(二维数组)
将两个索引组成一个值(一维数组)
- 如果两个索引分别是是整数i,j,则新的索引是i*M+j,M是列数
- 如果索引是字符串,可以用一个特殊字符作为分隔符将它们拼起来
-- N行M列的全零矩阵
mt = {} -- create the matrix
for i=1,N do
mt[i] = {} -- create a new row
for j=1,M do
mt[i][j] = 0
end
end
-- 用一维数组创建同样的矩阵
mt = {} -- create the matrix
for i=1,N do
for j=1,M do
mt[i*M + j] = 0
end
end
链表
以双向链表为例,每个结点用一个table表示,包含next,previous和value三个健,next和previous分别引用下一个和上一个结点,value保存结点的值。
-- init an empty linked list
head = nil
-- insert a node at the head
node = {value = 1}
if head then
node.next = head
head.previous = node
head = node
else
head = node
end
-- insert a node at the rear
local rear = head
while rear and rear.next do
rear = rear.next
end
node = {value = 2}
if rear then
node.previous = rear
rear.next = node
else
rear = node
end
-- insert a node before node p
p = node
node = {next = p, previous = p.previous, value = 3}
if p.previous then
p.previous.next = node
end
p.previous = node
-- traverse the list
local l = head
while l do
print(l.value)
l = l.next
end
队列
使用insert和remove对于大量数据来说效率很低,更高效的方式是使用两个索引,分别对应最左边和最右边的元素。 为了不污染全局空间,下面的示例把所有的队列操作定义在一个List table中。
List = {}
function List.new()
return {first = 0, last = -1}
end
function List.pushleft(list, value)
local first = list.first - 1
list.first = first
list[first] = value
end
function List.pushright(list, value)
local last = list.last + 1
list.last = last
list[last] = value
end
function List.popleft(list)
local first = list.first
if first > list.last then error("list is emtpy") end
local value = list[first]
list[first] = nil -- to allow garbage collection
list.first = first + 1
return value
end
function List.popright(list)
local last = list.last
if list.first > last then error("list is empty") end
local value = list[last]
list[last] = nil
list.last = last - 1
return value
end
L = List.new()
List.pushleft(L, 1)
List.pushleft(L, 0)
List.pushright(L, 2)
List.pushright(L, 3)
List.popright(L)
List.popleft(L)
for i, v in pairs(L) do
print(i,v)
end
集合
function Set (list)
local set = {}
for _, l in ipairs(list) do set[l] = true end
return set
end
-- 如果函数的参数只有一个,并且是字面值字符串或constructor,那么函数的小括号可以省略
-- 例如,print [[hello]]
reserved = Set{"while", "end", "function", "local", }
identifiers = {"begin", "global", "local"}
for _, w in ipairs(identifiers) do
if reserved[w] then
print(w, "is a reserved word")
end
end
Metatables(元表)和Metamethods(元方法)
为table设置Metatables可以扩展table的行为,通过为Metatable添加一些特殊的方法,也就是Metamethods,就可以支持table进行数学运算、关系比较,自定义打印输出以及操作等,类似于python中的运算符重载。
运算符重载
- 为元表添加__add,__sub,__mul等方法,就可以使table支持+,-,*等数学运算。
- 为元表添加__le,__lt,__eq方法使table支持所有关系比较运算,lua会将a~=b转化成not (a==b),将a>b 转化成 b=b 转化成 b<=a。不能将 a<=b 用not (b<a)表示,比如集合a<=b表示a是b的子集,如果a<=b为false,b<a也可以为false,此时not (b<a)为true。所以有必要同时定义_le和__lt。
- 为元表添加__tostring方法可以自定义print的输出。
Set = {}
Set.mt = {} -- metatable
function Set.new(t)
local set = {}
setmetatable(set, Set.mt) --
for _, l in ipairs(t) do set[l] = true end
return set
end
-- Arithmetic Metamethods
-- 并集
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
Set.mt.__add = Set.union -- __add元方法支持+运算
Set.mt.__mul = Set.intersection -- __mul元方法支持*运算
-- Relational Metamethods
-- 集合包含
Set.mt.__le = function (a,b)
for k in pairs(a) do
if not b[k] then return false end
end
return true
end
-- 真子集
Set.mt.__lt = function(a,b)
return a <= b and not( b <= a)
end
-- 相等
Set.mt.__eq = function (a,b)
return a <= b and b <= a
end
-- Library-Defined Metamethods
-- 支持print打印set
-- print函数总是会调用tostring函数格式化输出
-- 当tostring格式化一个对象的时候,会调用对象的__tostring元方法
Set.mt.__tostring = function(set)
local s = "{"
local sep = ""
for e in pairs(set) do
s = s .. sep .. e
sep = ", "
end
return s .. "}"
end
-- Test --
s1 = Set.new{10, 20, 30, 50}
s2 = Set.new{10, 20, 40}
print(s1)
print(s2)
s3 = s1 + s2
print(s3)
s3 = s1 * s2
print(s3)
-- lua进行+运算时
-- 如果左操作数有__add元方法,则使用左右两个操作数作为参数调用该方法
-- 否则,如果右操作数有__add元方法,就调用该方法
-- 否则,出错
-- 下面的例子会调用Set的元方法Set.union,同样的10 + s and "hy" + s也会调用该方法,因为number和string都没有__元方法
s = Set.new{1,2,3}
s = s + 8
-- 我们的实现会在pairs的时候出错,所以需要显示的检查类型
function Set.union(a,b)
if getmetatable(a) ~= Set.mt or
getmetatable(b) ~= Set.met then
error("attempt to `add` a set with a non-set value", 2)
end
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
-- 需要重新设置元方法
Set.mt.__add = Set.union
print(10 + s)
print("hy" + s)
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
-- 关系运算不支持混合类型
-- 如果两个对象的关系运算元方法不一样,比较运算会出错,与10 <= "10" 的行为一致
-- 相等比较是个例外,不会出错但结果是false
print(s1 == s2 * s1) --> true
属性管理
通过添加__index元方法,索引table中不存在的健时会调用__index元方法,使用当前的table和健作为参数。__index也可以是table,索引不存在的健时会访问该table。 下面的例子使用__index为新创建的window对象的属性提供缺省值。
-- new windows inherit any absent field from a prototype window
-- create a namespace
Window = {}
-- create the prototype with default values
Window.prototype = {x=0, y=0, width=100, height=100}
-- create a metatable
Window.mt = {}
-- declare the constructor function
function Window.new(o)
setmetatable(o, Window.mt)
return o
end
Window.mt.__index = function(table, key)
return Window.prototype[key]
end
-- 上面的__index元方法等价于
-- Window.mt.__index = Window.prototype
w = Window.new{x=10, y=20}
print(w.width) --> 100
print(rawget(w, width)) --> nil
与获取值对应,也有一个__newindex元方法控制为table赋值,在后面的例子中会看到。
__index的典型用法是实现代理模式:
-- create private index
local index = {}
-- create metatable
local mt = {
__index = function (t,k)
print("*access to element " .. tostring(k))
return t[index][k] -- access the original table
end,
__newindex = function (t,k,v)
print("*update of element " .. tostring(k) ..
" to " .. tostring(v))
t[index][k] = v -- update original table
end
}
function track (t)
local proxy = {}
proxy[index] = t
setmetatable(proxy, mt)
return proxy
end
-- original table
t = {}
t = track(t)
t[2] = 'hello'
print(t[2])
-- unfortunately, pairs will not traverse the original table
for k,v in pairs(t) do
print(k,v)
end
-- a private key that nobody else can access
print(t[{}])
借助代理模式,可以实现只读的table:
function readOnly (t)
local proxy = {}
local mt = { -- create metatable
__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]) --> Sunday
days[2] = "Noday"
弱表
lua使用引用计数进行垃圾回收,当一个对象(表或函数)没有被任何变量引用就会被自动回收,对象回收的时候不考虑弱引用,弱引用就是通过弱表实现的。
如果一个对象只存在于弱表中,该对象就可以被回收。创建弱表使用metatable中的特殊域__mode,该值是一个字符串,如果包含字符k
,则健是弱引用,如果包含v
,则值是弱引用。
a = {}
b = {}
setmetatable(a, b)
b.__mode = "k" -- now `a' has weak keys
key = {} -- creates first key
a[key] = 1
key = {} -- creates second key
a[key] = 2
collectgarbage() -- forces a garbage collection cycle
for k, v in pairs(a) do print(v) end
--> 2
为table设置默认值
利用__index和弱表都可以实现默认值非nil的表,下面对比了两种实现方式:
local key = {} -- unique key
local mt = {__index = function (t) return t[key] end}
function setDefault (t, d)
t[key] = d
setmetatable(t, mt)
end
tab = {x=10, y=20}
print(tab.x, tab.z) --> 10 nil
setDefault(tab, 0)
print(tab.x, tab.z) --> 10 0
-- another implemention by key weak table
local defaults = {} -- unique key
setmetatable(defaults, {__mode = "k"})
local mt = {__index = function (t) return defaults[t] end}
function setDefault2 (t, d)
defaults[t] = d
setmetatable(t, mt)
end
tab = {x=10, y=20}
print(tab.x, tab.z) --> 10 nil
setDefault(tab, 0)
print(tab.x, tab.z) --> 10 0
tab = {x=10, y=20}
print(tab.x, tab.z) --> 10 nil
setDefault2(tab, 0)
print(tab.x, tab.z) --> 10 0
环境
lua中的全局变量保存在table中。
_ENV
从lua5.2开始取消了setfenv,增加了_ENV的概念,它是一个局部变量,对未声明变量var的引用将自动转换成_ENV.var。_G的概念依然保留着,_ENV和_G的区别是_ENV可以被替换。
每个lua文件或交互式解释器中执行的每一行代码(如果是if和for的话,一直到end结束)都是一个chunk,每个chunk编译的时候都会有一个局部环境_ENV。 load一个chunk的时候,如果没有提供env参数,就会用全局变量表_G作为_ENV,相当于:
local _ENV = _G
return function (...) -- this function is what's returned from load
-- code you passed to load goes here, with all global variable names replaced with _ENV lookups
-- so, for example "a = b" becomes "_ENV.a = _ENV.b" if neither a nor b were declared local
end
设置环境
下面的例子通过为_ENV设置一个新的table来替换当前的环境。
print(_ENV == _G) -- prints true, since the default _ENV is set to the global table
a = 1
local function f(t)
-- since we will change the environment, standard functions will not be visible
local print = print
-- change the environment. without the local, this would change the environment for the entire chunk
local _ENV = t
-- prints nil, since global variables (including the standard functions) are not in the new env
print(getmetatable)
-- create a new entry in t, doesn't touch the original "a" global
a = 2
b = 3
end
local t = {}
f(t)
print(a, b) --> 1 nil
print(t.a, t.b) --> 2 3
local sandbox_env = {
print = print,
}
local chunk = load("print('inside sandbox'); os.execute('echo unsafe')",
"sandbox string", "bt", sandbox_env)
-- prevents os.execute from being called, instead raises an error saying that os is nil
chunk()
环境的传递
a.lua
aa = 'aaa'
require "b"
print(bb)
print(aa)
bb = "bbb"
$ lua a.lua
aaa
bbb
包
在lua中使用table表示一个package,package,library或module是一个意思。
package的两种常用写法
一种形式是所有函数都使用local声明,最后返回一个包含public函数的table。好处是在package里面调用public和private函数的方式完全一致。
local function private()
print("in private function")
end
local function foo()
print("Hello World!")
end
local function bar()
private()
foo() -- do not prefix function call with module
end
return {
foo = foo,
bar = bar,
}
另一种形式是public的函数定义成table的域,private函数使用local声明。
local P = {}
-- 使用全局变量来暴露package
-- 这种写法在项目中非常不推荐
-- mymodule = P
local function private()
print("in private function")
end
function P.foo()
print("Hello Lua!")
end
function P.bar()
private()
P.foo() -- need to prefix function call with module
end
return P
通过require加载package
常常把package写到一个文件里,通过require加载。require的作用相当于把包文件的内容作为一个匿名函数执行并返回。因此,包名和文件名没有对应关系,我们可以用任意变量名引用package。
-- 包名和文件名没有对应关系
-- require "mymodule1"的作用就是执行mymodule1.lua,返回一个表示包的table
-- 在实际脚本中,mymodule通常是局部变量
mymodule = require "mymodule1"
mymodule.foo()
mymodule.bar()
print("---------------")
mymodule = require "mymodule2"
mymodule.foo()
mymodule.bar()
面向对象
lua中的table本身就是对象,类、继承、访问控制等面相对象技术的实现依赖于table的__index元方法。
方法
下面创建了一个银行账户对象,具有存钱的方法。
-- define an object with a method
Account = {balance = 100}
function Account.deposit (self, v)
self.balance = self.balance + v
end
-- an equivalent form
-- self can be omitted by the colon syntax
-- function Account:deposit (v)
-- self.balance = self.balance - v
-- end
-- call the method
Account.deposit(Account, 0)
print(Account.balance)
-- the same
Account:deposit(100)
print(Account.balance)
类
把一个对象b设置成另一个对象a的__index,a会到b中查找a中没有的域,包括属性(数据或状态)和方法(函数或操作),此时就可以将前者视为类,在某些通过原型实现继承的语言中叫做prototype。类也是一个对象。
下面将银行账户抽象成类,方便不同的银行账户对象复用。
Account = {balance = 100}
function Account:new (o)
o = o or {} -- create object if user does not provide one
setmetatable(o, self)
self.__index = self
return o
end
function Account.deposit (self, v)
self.balance = self.balance + v
end
a = Account:new{balance = 0}
-- a中没有deposit,所以就到a的metatable的__index中去查找,
-- a的metatable是Account,Account的__index是Account本身,
-- Account中定义了deposit,所以最终调用了Account.deposit(a, 100.00),
-- 也就是a.balance = a.balance + v,因为self此时是a
a:deposit(100.00)
-- 属性的继承与方法完全一样
print(a.balance)
继承
因为对象继承了类的所有属性和方法,该对象也可以作为其他对象的类,这样就实现了继承。通过继承,可以定制某个类,比如覆盖父类的方法。 下面的例子通过继承实现可以透支的用户。
-- paremt class
Account = {balance = 0}
function Account:new (o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
function Account:deposit (v)
self.balance = self.balance + v
end
function Account:withdraw (v)
if v > self.balance then error"insufficient funds" end
self.balance = self.balance - v
end
-- subclass
SpecialAccount = Account:new()
-- redefine method
function SpecialAccount:withdraw (v)
if v - self.balance >= self:getLimit() then
error"insufficient funds"
end
self.balance = self.balance - v
end
-- add method
function SpecialAccount:getLimit ()
return self.limit or 0
end
-- create an instance of SpecialAccout
-- SpecialAccount调用了从Account继承来的new方法,self是SpecialAccount。
-- 所以s的meltable是SpecialAccount,并且__index也是SpecialAccount,
-- 所以s直接继承了SpecialAccount类
s = SpecialAccount:new{limit=1000.00}
-- s中没有deposit方法,所以就到SpecialAccount类中查找,同样找不到,
-- 就到父类Account中查找,结果存在,所以就执行了Account类中定义的deposit方法
s:deposit(100.00)
-- s在SpecialAccount中找到了withdraw方法,就不会调用Account中的同名方法,
-- 也就是子类覆盖了父类中的同名方法
s:withdraw(200)
print(s.balance)
私有属性和方法
由于将table作为对象的缘故,lua本身不提供属性私有化的功能。但是可以借助方法的闭包模拟,这种技术很少用,但作为面向对象的一部分在这里也给出了示例代码。
function newAccount (initialBalance)
-- private properties
local self = {balance = initialBalance}
-- private method
local format = function (balance) return "$" .. balance end
local getBalance = function () return format(self.balance) end
-- interface
return {
deposit = deposit,
getBalance = getBalance
}
end
a = newAccount(100)
print(a.getBalance())