最近在尝试配置 awesome WM,因此粗略地学习了一下 lua 。 在学习过程中,我完全被 table 在 lua 中的应用所镇住了。table 在 lua 中真的是无处不在:首先,它可以作为字典和数组来用; 此外,它还可以被用于设置闭包环境、module,甚至可以用来模拟对象和类。
table 最基础的作用就是当成字典来用。 它的 key 值可以是除了 nil 之外的任何类型的值。
- t={}
- t[{}] = "table" -- key 可以是 table
- t[1] = "int" -- key 可以是整数
- t[1.1] = "double" -- key 可以是小数
- t[function () end] = "function" -- key 可以是函数
- t[true] = "Boolean" -- key 可以是布尔值
- t["abc"] = "String" -- key 可以是字符串
- t[io.stdout] = "userdata" -- key 可以是userdata
- t[coroutine.create(function () end)] = "Thread" -- key可以是thread
-
当把 table 当成字典来用时,可以使用 pairs 函数来进行遍历。
- for k,v in pairs(t) do
- print(k,"->",v)
- end
-
运行结果为:
- 1 -> int
- 1.1 -> double
- thread: 0x220bb08 -> Thread
- table: 0x220b670 -> table
- abc -> String
- file (0x7f34a81ef5c0) -> userdata
- function: 0x220b340 -> function
- true -> Boolean
-
从结果中你还可以发现,使用 pairs 进行遍历时的顺序是随机的,事实上相同的语句执行多次得到的结果是不一样的。
table 中的 key 最常见的两种类型就是整数型和字符串类型。 当 key 为字符串时,table 可以当成结构体来用。同时形如 t["field"] 这种形式的写法可以简写成 t.field 这种形式。
当 key 为整数时,table 就可以当成数组来用。而且这个数组是一个 索引从1开始 ,没有固定长度,可以根据需要自动增长的数组。
- a = {}
- for i=0,5 do -- 注意,这里故意写成了i从0开始
- a[i] = 0
- end
-
当将 table 当成数组来用时,可以通过 长度操作符 # 来获取数组的长度
- print(#a)
-
结果为
- 5
-
你会发现, lua 认为 数组 a 中只有5个元素,到底是哪5个元素呢?我们可以使用使用 ipairs 对数组进行遍历:
- for i,v in ipairs(a) do
- print(i,v)
- end
-
结果为
- 1 0
- 2 0
- 3 0
- 4 0
- 5 0
-
从结果中你会发现 a 的 0 号索引并不认为是数组中的一个元素,从而也验证了 lua 中的数组是从 1开始索引的,另外,将table当成数组来用时,一定要注意索引不连贯的情况,这种情况下 # 计算长度时会变得很诡异
- a = {}
- for i=1,5 do
- a[i] = 0
- end
- a[8] = 0 -- 虽然索引不连贯,但长度是以最大索引为准
- print(#a)
- a[100] = 0 -- 索引不连贯,而且长度不再以最大索引为准了
- print(#a)
-
结果为:
- 8
- 8
-
而使用 ipairs 对数组进行遍历时,只会从1遍历到索引中断处
- for i,v in ipairs(a) do
- print(i,v)
- end
-
结果为:
- 1 0
- 2 0
- 3 0
- 4 0
- 5 0
-
lua将所有的全局变量/局部变量保存在一个常规table中,这个table一般被称为全局或者某个函数(闭包)的环境。
为了方便,lua在创建最初的全局环境时,使用全局变量 _G 来引用这个全局环境。因此,在未手工设置环境的情况下,可以使用 _G[varname] 来存取全局变量的值.
- for k,v in pairs(_G) do
- print(k,"->",v)
- end
-
- rawequal -> function: 0x41c2a0
- require -> function: 0x1ea4e70
- _VERSION -> Lua 5.3
- debug -> table: 0x1ea8ad0
- string -> table: 0x1ea74b0
- xpcall -> function: 0x41c720
- select -> function: 0x41bea0
- package -> table: 0x1ea4820
- assert -> function: 0x41cc50
- pcall -> function: 0x41cd10
- next -> function: 0x41c450
- tostring -> function: 0x41be70
- _G -> table: 0x1ea2b80
- coroutine -> table: 0x1ea4ee0
- unpack -> function: 0x424fa0
- loadstring -> function: 0x41ca00
- setmetatable -> function: 0x41c7e0
- rawlen -> function: 0x41c250
- bit32 -> table: 0x1ea8fc0
- utf8 -> table: 0x1ea8650
- math -> table: 0x1ea7770
- collectgarbage -> function: 0x41c650
- rawset -> function: 0x41c1b0
- os -> table: 0x1ea6840
- pairs -> function: 0x41c950
- arg -> table: 0x1ea9450
- table -> table: 0x1ea5130
- tonumber -> function: 0x41bf40
- io -> table: 0x1ea5430
- loadfile -> function: 0x41cb10
- error -> function: 0x41c5c0
- load -> function: 0x41ca00
- print -> function: 0x41c2e0
- dofile -> function: 0x41cbd0
- rawget -> function: 0x41c200
- type -> function: 0x41be10
- getmetatable -> function: 0x41cb80
- module -> function: 0x1ea4e00
- ipairs -> function: 0x41c970
-
从lua 5.2开始,可以通过修改 _ENV 这个值(lua5.1中的setfenv从5.2开始被废除)来设置某个函数的环境,从而让这个函数中的执行语句在一个新的环境中查找全局变量的值。
- a=1 -- 全局变量中a=1
- local env={a=10,print=_G.print} -- 新环境中a=10,并且确保能访问到全局的print函数
- function f1()
- local _ENV=env
- print("in f1:a=",a)
- a=a*10 -- 修改的是新环境中的a值
- end
-
- f1()
- print("globally:a=",a)
- print("env.a=",env.a)
-
- in f1:a= 10
- globally:a= 1
- env.a= 100
-
另外,新创建的闭包都继承了创建它的函数的环境
lua 中的模块也是通过返回一个table来供模块使用者来使用的。 这个 table 中包含的是模块中所导出的所有东西,包括函数和常量。
定义 module 的一般模板为
- module(模块名, package.seeall)
-
其中 module(模块名) 的作用类似于
- local modname = 模块名
- local M = {} -- M即为存放模块所有函数及常数的table
- _G[modname] = M
- package.loaded[modname] = M
- setmetatable(M,{__index=_G}) -- package.seeall可以使全局环境_G对当前环境可见
- local _ENV = M -- 设置当前的运行环境为 M,这样后续所有代码都不需要限定模块名了,所定义的所有函数自动变成M的成员
- <函数定义以及常量定义>
-
- return M -- module函数会帮你返回module table,而无需手工返回
-
lua 中之所以可以把 table 当成对象来用是因为:
1、函数在 lua 中是一类值,你可以直接存取 table 中的函数值。 这使得一个table既可以有自己的状态,也可以有自己的行为:
- Account = {balance = 0}
- function Account.withdraw(v)
- Account.balance = Account.balance - v
- end
-
2、lua 支持闭包,这个特性可以用来模拟对象的私有成员变量
- function new_account(b)
- local balance = b
- return {withdraw = function (v) balance = balance -v end,
- get_balance = function () return balance end
- }
- end
-
- a1 = new_account(1000)
- a1.withdraw(10)
- print(a1.get_balance())
-
- 990
-
不过,上面第一种定义对象的方法有一个缺陷,那就是方法与 Account 这个名称绑定死了。 也就是说,这个对象的名称必须为 Accout 否则就会出错
- a = Account
- Account = nil
- a.withdraw(10) -- 会报错,因为Accout.balance不再存在
-
为了解决这个问题,我们可以给 withdraw 方法多一个参数用于指向对象本身
- Account = {balance=100}
- function Account.withdraw(self,v)
- self.balance = self.balance - v
- end
- a = Account
- Account = nil
- a.withdraw(a,10) -- 没问题,这个时候 self 指向的是a,因此会去寻找 a.balance
- print(a.balance)
-
- 90
-
不过由于第一个参数 self 几乎总是指向调用方法的对象本身,因此 lua 提供了一种语法糖形式 object:method(...) 用于隐藏 self 参数的定义及传递. 这里冒号的作用有两个,其在定义函数时往函数中地一个参数的位置添加一个额外的隐藏参数 sef, 而在调用时传递一个额外的隐藏参数 self 到地一个参数位置。 即 function object:method(v) end 等价于 function object.method(self,v) end, object:method(v) 等价于 object.method(object,v)
当涉及到类和继承时,就要用到元表和元方法了。事实上,对于 lua 来说,对象和类并不存在一个严格的划分。当一个对象被另一个table的 __index 元方法所引用时,table 就能引用该对象中所定义的方法,因此也就可以理解为对象变成了 table 的类。
类定义的一般模板为:
- function 类名:new(o)
- o = o or {}
- setmetatable(o,{__index = self})
- return o
- end
-
或者
- function 类名:new(o)
- o = o or {}
- setmetatable(o,self)
- self.__index = self
- return o
- end
-
相比之下,第二种写法可以多省略一个 table,另外有一点我觉得有必要说明的就是 lua 中的元方法是在元表中定义的,而不是对象本身定义的,这一点跟其他面向对象的语言比较不同。