lua中万能的table

Published On March 06, 2017

category lua | tags lua table oop


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
对于稀疏矩阵(绝大部分元素的值是nil),不管用哪种矩阵表示方式,只有非nil的值才会占用存储空间。

链表

以双向链表为例,每个结点用一个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)
b.lua
print(aa)
bb = "bbb"
执行a.lua
$ 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
不管哪种形式,package里面不要使用全局变量。

通过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)
实现多继承的方式是为类的__index元方法提供一个函数,在该函数内遍历所有父类查找属性或方法。

私有属性和方法

由于将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())

参考


qq email facebook github
© 2018 - Xurui Yan. All rights reserved
Built using pelican