值和类型(Values and Types)
Lua是一门动态类型语言,这意味着变量没有类型,只有值有类型。语言没有类型定义,所有值携带自己的类型。
Lua中所有的值都是一等公民,这意味着所有的值都可以存储在变量中,作为参数传递给其他函数,作为函数的结果返回。
值得注意的是这点对函数也成立,在Java中函数是没这个待遇的。比如对一个列表排序,需要一个比较函数,Lua可以直接传递比较函数,而Java需要为这个比较函数创建一个类型,然后传递这个类型的实例。
来看一个例子,将一个列表从大到小排列:
Lua
list = {1, 2, 3, 4, 5}
function comp(i, j)
return i > j
end
table.sort(list, comp)
Java
List<Integer> list = new ArrayList<Integer>(Arrays.asList(new Integer[] { 1, 2, 3, 4, 5 }));
Comparator<Integer> comp = new Comparator<Integer>()
{
@Override
public int compare(Integer o1, Integer o2)
{
return o1 < o2 ? 1 : -1;
}
};
Collections.sort(list, comp);
Lua提供了八种基础数据类型:nil、boolean、number、string、function、userdata、thread、table。
nil:表示空。
boolean:布尔类型,只有nil和false表示false,其他都为true。0和1都表示true。
number:数值类型,两种内部表示,integer和float,默认为64位整型和双精度浮点型。
string:字符串类型,值是不可变的,每次修改都会创建一个新的string对象。同Java的StringBuilder一样,Lua提供了table.concat来拼接字符串。
function:函数类型。他是一等公民。
userdata:允许C中的数据保存在Lua中。这有什么作用呢?Lua作为一种嵌入式语言,自身的API是很少的,但是可以很方便的通过C来扩展功能。比如为Lua提供socket功能,使它能够像如下方式使用:
connection = socket.connect(host, port)
connection.write(msg)
这个connection是一个连接对象,它是由CAPI创建的一个userdata,可以在Lua中使用。
thread:独立的执行线程,用于实现协程。Lua的thread不同于操作系统的thread,即时操作系统不支持thread,Lua能在这种操作系统上支持协程。(协程后面介绍)
table:表,Lua仅有的数据结构。不过可以用于表示数组,集合,链表,树,图等数据结构。
环境与全局环境(Environmets and the Global Environment)
当引用一个未被声明的变量var,句法上表示_ENV.var。_ENV的值是一个table,这个值被称为环境。Lua中每个被编译的chunk都有一个额外的local变量,称为_ENV。 当访问一个非local变量,会从_ENV中查找,当定义一个非local变量,会存储在_ENV中。
Lua中有个一个特殊的环境叫做全局环境(_G),_ENV的默认值就是全局环境。所以当你定义一个非local的变量,默认为全局变量。
如果我们定义自己的环境_ENV,那么非local变量就会存储在自定义的环境中,来看一个例子(lua5.2以上版本):
employee1 = {_G = _G} -->保留全局环境的引用
_ENV = employee1
id = 1 -->不会作为全局变量
name = "xiaoming"
salary = 3000
_ENV = _G
employee2 = {_G = _G}
_ENV = employee2
id = 2
name = "daming"
salary = 20000
_ENV = _G
print(employee1.id, employee1.name, employee1.salary)
print(employee2.id, employee2.name, employee2.salary)
错误处理(Error Handing)
Lua作为一门嵌入式扩展语言,所有的行为都是从宿主程序的一次C函数调用开始的。当Lua编译或者运行发生错误时,会把控制权交给宿主程序,由宿主程序处理错误。
Lua可以通过error函数来抛出一个错误。如果Lua想捕获错误,则需要在保护模式下运行代码,即通过pcall或xpcall来执行方法。
当使用xpcall时,需要提供一个错误处理函数,他可以在错误发生且栈还未展开时执行。如果栈还未展开,这意味你可以获得调用上下文的所有信息,比如当前环境中的一个本地变量。不过不能错误处理函数中直接使用上下文中信息,需要借助debug库,来看一个例子:
function get1()
local i = 1
get2()
end
function get2()
local j = 2
get3()
end
function get3()
local k = 3
error("throw error")
end
function handler(msg)
--print(debug.traceback()) -->通过打印栈回溯信息确定方法所在的level
name, value = debug.getlocal(3, 1) -->获取get3方法的本地变量k
print(name, value)
name, value = debug.getlocal(4, 1) -->获取get2方法的本地变量j
print(name, value)
name, value = debug.getlocal(5, 1) -->获取get1方法的本地变量i
print(name, value)
return "warp "..msg
end
xpcall(get1, handler)
输出:
k 3
j 2
i 1
元表和元方法(Metatables and Metamethods)
Lua为每一个值都提供了元表,元表就是一个table,它定义了原始值在特定操作下的行为。行为由方法提供,这种方法称为元方法。元表可以控制很多操作,比如加法,取长度,比较,索引等等。
table和userdata类型的值有独立的元表,其他类型的值分别共享一张元表。
比如我们的变量可以存储在三个地方,global域,session域,page域,查找顺序为page,session,global。用元表可以这样来实现:
global = {a = 1}
session = {b = 2}
session_mt = {__index = global} -->定义元表session_mt和元方法__index
setmetatable(session, session_mt) -->session中查找不到的key会去global中查找
page = {c = 3}
page_mt = {__index = session}
setmetatable(page, page_mt)
print(page.c, page.b, page.a)
输出:
3 2 1
协程(Coroutines)
Lua中的协程又叫协同式多线程,协程之间不会抢占执行,只有在协程内部调用yield才会让出执行权。
Lua中的每个协程都是独立的执行序列,协程之间不共享内存。
Lua协程很多特点,比如yield之后调用resume继续执行,会从上次yield的地方继续执行,并且可以访问到yield之前的那些局部变量。
闭包(Closure)
当函数A调用函数B时,函数B可以操作函数A中的局部变量,这种形式叫做闭包。看一个例子:
function retfoo()
local i = 0
return function ()
i = i + 1
return i
end
end
foo = retfoo()
print(foo(), foo())
输出:
1 2
闭包的作用在于可以保存状态,让多次调用之间产生关系。
For语句
Lua中提供了两种形式的for语句,numeric for 和 generic for。
- numeric for 的语法如下
stat ::= for Name ‘=’ exp ‘,’ exp [ ‘,’ exp ] do block end
block把Name作为循环变量,起始值为第一个exp,直到第二个exp为止,步长为第三个exp。
一个numeric for像这样:
for v = e1, e2, e3 do block end
它等价于下面的代码
do
local var, limit, step = tonumber(e1), tonumber(e2), tonumber(e3)
if not (var and limit and step) then error() end
var = var - step
while true do
var = var + step
if (step >= 0 and var > limit) or (step < 0 and var < limit) then
break
end
local v = var
block
end
end
- generic for 的语法如下
stat ::= for namelist in explist do block end
namelist ::= Name {‘,’ Name}
一个generic for像这样:
for var_1, … , var_n in explist do block end
它等价于下面的代码
do
local f, s, var = explist
while true do
local var_1, … , var_n = f(s, var)
if var_1 == nil then break end
var = var_1
block
end
end
f, s, var 可以看作迭代函数,迭代不变量,迭代变量。
如此Lua的迭代器可以分成两种,一种是有状态的,一种是无状态的。
有状态的迭代器采用闭包来实现,比如编写一个迭代器来遍历table数组。
function ipairs(table) -->有状态的迭代器
i = 0
return function()
i = i + 1
if i > #table then return nil end
return i, table[i]
end
end
table = {"a", "b", "c", "d", "e"}
for i, v in ipairs(table) do
print(i, v)
end
有状态的迭代器的缺点在于每遍历一次都需要创建一个迭代器,无状态的迭代器则不需要。
function ipairs0(table, i) -->无状态的迭代器
i = i + 1
local v = table[i]
if not v then return nil end
return i, v
end
function ipairs(table)
return ipairs0, table, 0
end
table = {"a", "b", "c", "d", "e"}
for i, v in ipairs(table) do
print(i, v)
end
在这个例子中,ipairs0, table, i 分别对应f, s, var。在上下文中,i既是迭代变量,也是table数组的当前索引。
模块与包(Module and Package)
从用户的观点来看,一个模块就是一个程序库,可以通过require来加载。然后便得到一个全局变量,表示一个table。这个table就像一个名称空间,其内容就是模块中导出的所有东西,如函数和常量。
加载一个模块
require “mod”
mod.foo()
编写一个模块
把模块中要导出的内容放在一个table中,用模块名作为全局变量引用这个table,模块的最后返回这个table。
modulename = … -->模块名由require函数传入
local moduletable = {} -->模块table,存放要导出的内容
_G[modulename] = moduletable -->用模块名作为全局变量引用模块table
-- 定义模块中要导出的函数和常量
return moduletable -->相当于package.loaded[modulename] = moduletable
如果一个模块被加载过,require函数会返回已经加载的模块。如果模块需要热更新,可以这样实现:
package.loaded[modulename] = nil
require “modulename”
当然在实际的代码中,还有更多的问题需要考虑,比如闭包中的upvalue的更新。
面向对象
一个对象要有变量和方法,Lua可以用table来表示对象。
Girl = {age = 18}
function Girl.grow(self, v)
self.age = self.age + v
end
Girl.grow(Girl, 1)
print(Girl.age)
Girl是一个对象,age是Girl的变量,grow是Girl的方法。grow方法第一个参数为self,示例中Girl.grow(Girl, 1)调用grow方法时,传入了Girl。这个调用存在另外一种语法糖:
Girl:grow(1)
grow方法可以这样来定义:
function Girl:grow(v)
self.age = self.age + v
end
在Java、C#这样的面向对象语言中,有类和对象的区分。类是对象的抽象,对象通过类来创建,比如 Girl g = new Girl()。Lua中没有类的概念,如果需要表达类,可以采用原型语言的做法。对象a拥有一个原型b,对象a会在原型b上查找不存在的操作,原型b也是一个普通对象。
function Girl:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
function Girl:grow(v)
self.age = self.age + v
end
g = Girl:new({age = 18})
g:grow(1)
print(g.age)
利用元表,Lua也可以表达出继承,多态的概念。不过我觉得Lua拥有更灵活的语法,没必要像面向对象那样死板的编程。比如如果有girl1和girl2两个对象,它们的grow行为不同,那么在面向对象中需要创建Girl1和Girl2两个类,而在Lua中只需要简单的修改girl2的grow方法:
local oldGrow = g.grow
function g:grow(v)
oldGrow(g, v)
print("do other things...")
end
g:grow(1)
print(g.age)
参考资料
《Lua程序设计》第二版