最近在尝试配置 awesome WM,因此粗略地学习了一下 lua 。 在学习过程中,我完全被 表 ( 表 ) 在 lua 中的应用所镇住了。
表在 lua 中真的是无处不在:首先,它可以作为字典和数组来用;此外,它还可以被用于设置闭包环境、模块;甚至可以用来模拟对象和类。
字典 表最基础的作用就是当成字典来用。 它的键可以是除了 nil
之外的任何类型的值。
1 2 3 4 5 6 7 8 9 10 t={} t[{}] = "table" t[1 ] = "int" t[1.1 ] = "double" t[function () end ] = "function" t[true ] = "Boolean" t["abc" ] = "String" t[io .stdout ] = "userdata" t[coroutine .create (function () end )] = "Thread"
当把表当成字典来用时,可以使用 pairs
函数来进行遍历。
1 2 3 4 for k,v in pairs (t) do print (k,"->" ,v)end
运行结果为:
1 2 3 4 5 6 7 8 9 1 -> int1.1 -> double thread: 0x220bb08 -> Thread table: 0x220b670 -> table abc -> String file (0x7f34a81ef5c0 ) -> userdata function: 0x220b340 -> function true -> Boolean
从结果中你还可以发现,使用 pairs
进行遍历时的顺序是随机的,事实上相同的语句执行多次得到的结果是不一样的。
表 中的键最常见的两种类型就是整数型和字符串类型。 当键为字符串时,表 可以当成结构体来用。同时形如 t["field"]
这种形式的写法可以简写成 t.field
这种形式。
数组 当键为整数时,表 就可以当成数组来用。而且这个数组是一个 索引从 1 开始 、没有固定长度、可以根据需要自动增长的数组。
1 2 3 4 5 a = {}for i=0 ,5 do -- 注意,这里故意写成了i从0 开始 a [i] = 0 end
当将表当成数组来用时,可以通过长度操作符 #
来获取数组的长度:
结果为:
你会发现, lua 认为数组 a
中只有 5 个元素,到底是哪 5 个元素呢?我们可以使用使用 ipairs
对数组进行遍历:
1 2 3 4 for i,v in ipairs (a) do print (i,v)end
结果为:
从结果中你会发现 a
的 0 号索引并不认为是数组中的一个元素,从而也验证了 lua 中的数组是从 1 开始索引的 。
另外,将表当成数组来用时,一定要注意索引不连贯的情况,这种情况下 #
计算长度时会变得很诡异。
1 2 3 4 5 6 7 8 9 a = {}for i=1 ,5 do a [i] = 0 end a [8 ] = 0 -- 虽然索引不连贯,但长度是以最大索引为准print (#a)a [100 ] = 0 -- 索引不连贯,而且长度不再以最大索引为准了print (#a)
结果为:
而使用 ipairs
对数组进行遍历时,只会从 1 遍历到索引中断处。
1 2 3 4 for i,v in ipairs (a) do print (i,v)end
结果为:
环境(命名空间) lua 将所有的全局变量/局部变量保存在一个常规表中,这个表一般被称为全局或者某个函数(闭包)的环境。
为了方便,lua 在创建最初的全局环境时,使用全局变量 _G
来引用这个全局环境。因此,在未手工设置环境的情况下,可以使用 -G[varname]
来存取全局变量的值。
1 2 3 4 for k,v in pairs (_G ) do print (k,"->" ,v)end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 rawequal -> function: 0 x41c2a0require -> function: 0 x1ea4e70 _VERSION -> Lua 5.3 debug -> table: 0 x1ea8ad0string -> table: 0 x1ea74b0xpcall -> function: 0 x41c720select -> function: 0 x41bea0package -> table: 0 x1ea4820assert -> function: 0 x41cc50pcall -> function: 0 x41cd10next -> function: 0 x41c450tostring -> function: 0 x41be70 _G -> table: 0 x1ea2b80coroutine -> table: 0 x1ea4ee0unpack -> function: 0 x424fa0loadstring -> function: 0 x41ca00setmetatable -> function: 0 x41c7e0rawlen -> function: 0 x41c250bit32 -> table: 0 x1ea8fc0utf8 -> table: 0 x1ea8650math -> table: 0 x1ea7770collectgarbage -> function: 0 x41c650rawset -> function: 0 x41c1b0os -> table: 0 x1ea6840pairs -> function: 0 x41c950arg -> table: 0 x1ea9450table -> table: 0 x1ea5130tonumber -> function: 0 x41bf40io -> table: 0 x1ea5430loadfile -> function: 0 x41cb10error -> function: 0 x41c5c0load -> function: 0 x41ca00print -> function: 0 x41c2e0dofile -> function: 0 x41cbd0rawget -> function: 0 x41c200type -> function: 0 x41be10getmetatable -> function: 0 x41cb80module -> function: 0 x1ea4e00ipairs -> function: 0 x41c970
从 lua 5.2 开始,可以通过修改 _ENV
这个值(lua 5.1 中的 setfenv
从 5.2 开始被废除 )来设置某个函数的环境,从而让这个函数中的执行语句在一个新的环境中查找全局变量的值。
1 2 3 4 5 6 7 8 9 10 11 12 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)
1 2 3 4 in f1:a= 10 globally :a= 1 env .a= 100
另外,新创建的闭包都继承了创建它的函数的环境。
模块 lua 中的模块也是通过返回一个表来供模块使用者来使用的。 这个表中包含的是模块中所导出的所有东西,包括函数和常量。
定义模块的一般模板为:
1 2 module (模块名, package .seeall )
其中 module(模块名)
的作用类似于:
1 2 3 4 5 6 7 8 9 10 local modname = 模块名local M = {} _G [modname] = Mpackage .loaded [modname] = Msetmetatable (M,{__index =_G }) local _ENV = M <函数定义以及常量定义>return M
对象 lua 中之所以可以把表当成对象来用是因为:
函数在 lua 中是一类值,你可以直接存取表中的函数值。 这使得一个表既可以有自己的状态,也可以有自己的行为:
1 2 3 4 5 Account = {balance = 0 } function Account.withdraw(v) Account.balance = Account.balance - v end
lua 支持闭包,这个特性可以用来模拟对象的私有成员变量:
1 2 3 4 5 6 7 8 9 10 11 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())
不过,上面第一种定义对象的方法有一个缺陷,那就是方法与 Account
这个名称绑定死了。 也就是说,这个对象的名称必须为 Accout
否则就会出错。
1 2 3 4 a = AccountAccount = nil a.withdraw(10 ) -- 会报错,因为Accout.balance不再存在
为了解决这个问题,我们可以给 withdraw
方法多一个参数用于指向对象本身。
1 2 3 4 5 6 7 8 9 Account = {balance=100 }function Account.withdraw (self,v) self .balance = self .balance - vend a = Account Account = nil a.withdraw(a,10 ) print (a.balance)
不过由于第一个参数 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 来说,对象和类并不存在一个严格的划分。
当一个对象被另一个表的 __index
元方法所引用时,表就能引用该对象中所定义的方法,因此也就可以理解为对象变成了表的类。
类定义的一般模板为:
1 2 3 4 5 6 function 类名:new (o) o = o or {} setmetatable (o,{__index = self }) return oend
或者:
1 2 3 4 5 6 7 function 类名:new (o) o = o or {} setmetatable (o,self ) self .__index = self return oend
相比之下,第二种写法可以多省略一个表。
另外有一点我觉得有必要说明的就是 lua 中的元方法是在元表中定义的,而不是对象本身定义的,这一点跟其他面向对象的语言比较不同。