lua 中神奇的表(table) | Linux 中国

Posted Linux开源社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了lua 中神奇的表(table) | Linux 中国相关的知识,希望对你有一定的参考价值。

最近在尝试配置 awesome WM,因此粗略地学习了一下 lua 。 在学习过程中,我完全被 table 在 lua 中的应用所镇住了。
-- Lujun9972



致谢
转载自 | 
https://github.com/lujun9972/lujun9972.github.com/blob/source/%E7%BC%96%E7%A8%8B%E4%B9%8B%E6%97%85/lua%E4%B8%AD%E7%A5%9E%E5%A5%87%E7%9A%84table.org

 作者 | Lujun9972

最近在尝试配置 awesome WM,因此粗略地学习了一下 lua 。 在学习过程中,我完全被在 lua 中的应用所镇住了。

表在 lua 中真的是无处不在:首先,它可以作为字典和数组来用;此外,它还可以被用于设置闭包环境、模块;甚至可以用来模拟对象和类。

字典

表最基础的作用就是当成字典来用。 它的键可以是除了 nil 之外的任何类型的值。

  
    
    
  
  1. t={}

  2. t[{}] = "table"                 -- key 可以是表

  3. t[1] = "int"                    -- key 可以是整数

  4. t[1.1] = "double"               -- key 可以是小数

  5. t[function () end] = "function" -- key 可以是函数

  6. t[true] = "Boolean"             -- key 可以是布尔值

  7. t["abc"] = "String"             -- key 可以是字符串

  8. t[io.stdout] = "userdata"       -- key 可以是userdata

  9. t[coroutine.create(function () end)] = "Thread" -- key可以是thread

当把表当成字典来用时,可以使用 pairs 函数来进行遍历。

  
    
    
  
  1. for k,v in pairs(t) do

  2.  print(k,"->",v)

  3. end

运行结果为:

  
    
    
  
  1. 1   ->  int

  2. 1.1 ->  double

  3. thread: 0x220bb08   ->  Thread

  4. table: 0x220b670    ->  table

  5. abc ->  String

  6. file (0x7f34a81ef5c0)   ->  userdata

  7. function: 0x220b340 ->  function

  8. true    ->  Boolean

从结果中你还可以发现,使用 pairs 进行遍历时的顺序是随机的,事实上相同的语句执行多次得到的结果是不一样的。

表 中的键最常见的两种类型就是整数型和字符串类型。 当键为字符串时,表 可以当成结构体来用。同时形如 t["field"] 这种形式的写法可以简写成 t.field 这种形式。

数组

当键为整数时,表 就可以当成数组来用。而且这个数组是一个 索引从 1 开始 、没有固定长度、可以根据需要自动增长的数组。

  
    
    
  
  1. a = {}

  2. for i=0,5 do                    -- 注意,这里故意写成了i0开始

  3.  a[i] = 0

  4. end

当将表当成数组来用时,可以通过长度操作符 # 来获取数组的长度:

  
    
    
  
  1. print(#a)

结果为:

  
    
    
  
  1. 5

你会发现, lua 认为数组 a 中只有 5 个元素,到底是哪 5 个元素呢?我们可以使用使用 ipairs 对数组进行遍历:

  
    
    
  
  1. for i,v in ipairs(a) do

  2.  print(i,v)

  3. end

结果为:

  
    
    
  
  1. 1   0

  2. 2   0

  3. 3   0

  4. 4   0

  5. 5   0

从结果中你会发现 a 的 0 号索引并不认为是数组中的一个元素,从而也验证了 lua 中的数组是从 1 开始索引的

另外,将表当成数组来用时,一定要注意索引不连贯的情况,这种情况下 # 计算长度时会变得很诡异。

  
    
    
  
  1. a = {}

  2. for i=1,5 do

  3.  a[i] = 0

  4. end

  5. a[8] = 0                        -- 虽然索引不连贯,但长度是以最大索引为准

  6. print(#a)

  7. a[100] = 0                      -- 索引不连贯,而且长度不再以最大索引为准了

  8. print(#a)

结果为:

  
    
    
  
  1. 8

  2. 8

而使用 ipairs 对数组进行遍历时,只会从 1 遍历到索引中断处。

  
    
    
  
  1. for i,v in ipairs(a) do

  2.  print(i,v)

  3. end

结果为:

  
    
    
  
  1. 1   0

  2. 2   0

  3. 3   0

  4. 4   0

  5. 5   0

环境(命名空间)

lua 将所有的全局变量/局部变量保存在一个常规表中,这个表一般被称为全局或者某个函数(闭包)的环境。

为了方便,lua 在创建最初的全局环境时,使用全局变量 _G 来引用这个全局环境。因此,在未手工设置环境的情况下,可以使用 -G[varname] 来存取全局变量的值。

  
    
    
  
  1. for k,v in pairs(_G) do

  2.  print(k,"->",v)

  3. end

  
    
    
  
  1. rawequal    ->  function: 0x41c2a0

  2. require ->  function: 0x1ea4e70

  3. _VERSION    ->  Lua 5.3

  4. debug   ->  table: 0x1ea8ad0

  5. string  ->  table: 0x1ea74b0

  6. xpcall  ->  function: 0x41c720

  7. select  ->  function: 0x41bea0

  8. package ->  table: 0x1ea4820

  9. assert  ->  function: 0x41cc50

  10. pcall   ->  function: 0x41cd10

  11. next    ->  function: 0x41c450

  12. tostring    ->  function: 0x41be70

  13. _G  ->  table: 0x1ea2b80

  14. coroutine   ->  table: 0x1ea4ee0

  15. unpack  ->  function: 0x424fa0

  16. loadstring  ->  function: 0x41ca00

  17. setmetatable    ->  function: 0x41c7e0

  18. rawlen  ->  function: 0x41c250

  19. bit32   ->  table: 0x1ea8fc0

  20. utf8    ->  table: 0x1ea8650

  21. math    ->  table: 0x1ea7770

  22. collectgarbage  ->  function: 0x41c650

  23. rawset  ->  function: 0x41c1b0

  24. os  ->  table: 0x1ea6840

  25. pairs   ->  function: 0x41c950

  26. arg ->  table: 0x1ea9450

  27. table   ->  table: 0x1ea5130

  28. tonumber    ->  function: 0x41bf40

  29. io  ->  table: 0x1ea5430

  30. loadfile    ->  function: 0x41cb10

  31. error   ->  function: 0x41c5c0

  32. load    ->  function: 0x41ca00

  33. print   ->  function: 0x41c2e0

  34. dofile  ->  function: 0x41cbd0

  35. rawget  ->  function: 0x41c200

  36. type    ->  function: 0x41be10

  37. getmetatable    ->  function: 0x41cb80

  38. module  ->  function: 0x1ea4e00

  39. ipairs  ->  function: 0x41c970

从 lua 5.2 开始,可以通过修改 _ENV 这个值(lua 5.1 中的 setfenv 从 5.2 开始被废除)来设置某个函数的环境,从而让这个函数中的执行语句在一个新的环境中查找全局变量的值。

  
    
    
  
  1. a=1                             -- 全局变量中a=1

  2. local env={a=10,print=_G.print} -- 新环境中a=10,并且确保能访问到全局的print函数

  3. function f1()

  4.  local _ENV=env

  5.  print("in f1:a=",a)

  6.  a=a*10                        -- 修改的是新环境中的a

  7. end

  8. f1()

  9. print("globally:a=",a)

  10. print("env.a=",env.a)

  
    
    
  
  1. in f1:a=    10

  2. globally:a= 1

  3. env.a=  100

另外,新创建的闭包都继承了创建它的函数的环境。

模块

lua 中的模块也是通过返回一个表来供模块使用者来使用的。 这个表中包含的是模块中所导出的所有东西,包括函数和常量。

定义模块的一般模板为:

  
    
    
  
  1. module(模块名, package.seeall)

其中 module(模块名) 的作用类似于:

  
    
    
  
  1. local modname = 模块名

  2. local M = {}                    -- M即为存放模块所有函数及常数的table

  3. _G[modname] = M

  4. package.loaded[modname] = M

  5. setmetatable(M,{__index=_G})    -- package.seeall可以使全局环境_G对当前环境可见

  6. local _ENV = M                  -- 设置当前的运行环境为 M,这样后续所有代码都不需要限定模块名了,所定义的所有函数自动变成M的成员

  7. <函数定义以及常量定义>

  8. return M                        -- module函数会帮你返回module table,而无需手工返回

对象

lua 中之所以可以把表当成对象来用是因为:

函数在 lua 中是一类值,你可以直接存取表中的函数值。 这使得一个表既可以有自己的状态,也可以有自己的行为:

    
      
      
    
  1. Account = {balance = 0}

  2. function Account.withdraw(v)

  3.  Account.balance = Account.balance - v

  4. end

lua 支持闭包,这个特性可以用来模拟对象的私有成员变量:

    
      
      
    
  1. function new_account(b)

  2.  local balance = b

  3.  return {withdraw = function (v) balance = balance -v end,

  4.          get_balance = function () return balance end

  5.  }

  6. end

  7. a1 = new_account(1000)

  8. a1.withdraw(10)

  9. print(a1.get_balance())

    
      
      
    
  1. 990

不过,上面第一种定义对象的方法有一个缺陷,那就是方法与 Account 这个名称绑定死了。 也就是说,这个对象的名称必须为 Accout 否则就会出错。

  
    
    
  
  1. a = Account

  2. Account = nil

  3. a.withdraw(10)                  -- 会报错,因为Accout.balance不再存在

为了解决这个问题,我们可以给 withdraw 方法多一个参数用于指向对象本身。

  
    
    
  
  1. Account = {balance=100}

  2. function Account.withdraw(self,v)

  3.  self.balance = self.balance - v

  4. end

  5. a = Account

  6. Account = nil

  7. a.withdraw(a,10)                  -- 没问题,这个时候 self 指向的是a,因此会去寻找 a.balance

  8. print(a.balance)

  
    
    
  
  1. 90

不过由于第一个参数 self 几乎总是指向调用方法的对象本身,因此 lua 提供了一种语法糖形式 object:method(...) 用于隐藏 self 参数的定义及传递。这里冒号的作用有两个,其在定义函数时往函数中地一个参数的位置添加一个额外的隐藏参数 sef, 而在调用时传递一个额外的隐藏参数 self 到地一个参数位置。 即 function object:method(v) end 等价于 function object.method(self,v) endobject:method(v) 等价于 object.method(object,v)

当涉及到类和继承时,就要用到元表和元方法了。事实上,对于 lua 来说,对象和类并不存在一个严格的划分。

当一个对象被另一个表的 __index 元方法所引用时,表就能引用该对象中所定义的方法,因此也就可以理解为对象变成了表的类。

类定义的一般模板为:

  
    
    
  
  1. function 类名:new(o)

  2.  o = o or {}

  3.  setmetatable(o,{__index = self})

  4.  return o

  5. end

或者:

  
    
    
  
  1. function 类名:new(o)

  2.  o = o or {}

  3.  setmetatable(o,self)

  4.  self.__index = self

  5.  return o

  6. end

相比之下,第二种写法可以多省略一个表。

另外有一点我觉得有必要说明的就是 lua 中的元方法是在元表中定义的,而不是对象本身定义的,这一点跟其他面向对象的语言比较不同。


以上是关于lua 中神奇的表(table) | Linux 中国的主要内容,如果未能解决你的问题,请参考以下文章

Lua中的表键和值 - 不知道如何

Lua表(table)的个人总结

Cocos2d-x 脚本语言Lua基本数据结构-表(table)

Lua——3.元表Metatable

openresty开发系列15--lua基础语法4表table和运算符

Lua中的weak表——weak table(转)