尽管Lua是一门解析型的语言,但是在运行前也会被编译成某个中间状态。一门解析型的语言需要编译,这听起来有点不合常理。但是,实际上,解析型语言的与众不同,不是说它不需要编译,而是说它把编译作为其运行时的一部分,因此,它就可以执行各种来自外部的代码(例如网上的)。也许因为Lua中存在的如dofile 这样的函数,才使Lua可以被称为一门解析型语言。
之前我们介绍了dofile 来执行代码块,但是dofile 只是一个辅助函数。这里介绍一下loadfile 函数,它会从一个file中加载语句块,但是不运行;而是仅仅编译并作为一个函数返回。loadfile 不会像dofile 那样在运行时直接报错退出,而是返回错误码,这样我们就可以根据错误码做相应的处理。我们可以像下面这样定义dofile ,这也可以看出dofile 和loadfile 的区别
function dofile (filename) local f = assert(loadfile(filename)) return f() end
注意,assert 可以使loadfile 发生错误时,报错退出。
dofile 在处理有些简单的任务时,使用起来比较方便,只需要一次调用,它会完成所有的操作(编译,运行啥的)。但是,loadfile更加灵活。发生错误的时候,loadfile 会返回nil + 'err_msg',我们可以根据实际情况对错误做出相应的处理。除此之外,如果想要运行一个file多次,可以先调用一次loadfile ,然后调用loadfile 返回的结果多次,就可以了。这比调用几次dofile 开销要小很多,因为loadfile 只会执行一次编译,而dofile 每次都用都要编译。
loadstring 跟loadfile 差不多,区别是从一个string中加载代码块,而不是从file中。例如:
f = loadstring("i = i + 1")
f 是loadstring 的返回值,应该是function类型, 调用的时候,会执行i = i + 1 :
i = 0 f(); print(i) --> 1 f(); print(i) --> 2
loadstring 函数功能非常强大,但是运行起来,开销也不小,并且有时会导致产生一些莫名其妙的代码。因此,在用loadstring 之前,先考虑下有没有更简单的办法。
下面这行代码,不太好看,但是很方便。(不鼓励这种调用方法)
loadstring(s)()
如果有语法错误,loadstring 会返回nil + 类似‘attempt to call a nil value’这样的err_msg。如果想获取更详细的err_msg,那就需要用assert :
assert(loadstring(s))()
下面这样的使用方式(对一个字面值string使用loadstring),没什么意思,
f = loadstring("i = i + 1")
粗略的等价于:
f = function () i = i + 1 end
但是,也不是完全相同,且继续往下看。第二种方式的代码运行起来更快,因为它只需要编译一次,而loadstring 每次都需要编译。下面我们来看看,上面两段代码到底有什么不同,如下示例:
i = 32 local i = 0 f = loadstring("i = i + 1; print(i)") g = function () i = i + 1; print(i) end f() --> 33 g() --> 1
g 函数处理的事局部变量i , 而f 函数处理的是全局变量i ,loadstring 总是在全局环境中进行编译。
loadstring 最典型的用途是:运行外来代码,例如网络上的,别人的。。。注意,loadsting 只能load语句,不能load表达式, 如果你想算一个表达式的值,那么前面要加上一个return 来返回给定表达式的值。下面是一个示例帮助理解:
print "enter your expression:" local l = io.read() local func = assert(loadstring("return " .. l)) print("the value of your expression is " .. func())
看一下运行情况:(注意第一个为什么报错了,想想什么才叫表达式)
loadstring 返回的就是一个普通的函数,可以多次调用:
print "enter function to be plotted (with variable 'x'):" local l = io.read() local f = assert(loadstring("return " .. l)) for i=1,20 do x = i -- global 'x' (to be visible from the chunk) print(string.rep("*", f())) end
(string.rep 函数复制一个string给定的次数),下面是运行结果:
如果我们在深究一下,其实不管loadstring 也好,loadfile 也好,Lua中最基础的函数是load 。loadfile 从一个file中加载代码块,loadstring 从一个string中加载代码块,而load 调用一个reader 函数来获取代码块,这个reader 函数分块返回代码块,load 调用它,直到它返回nil 。我们很少使用load 函数;通常只有在代码块不是位于一个file中,但是又太大了,不适合放到内存中(如果适合放到内存中,那就可以用loadstring 了)的时候,才会用load 。
Lua将每一个独立的代码块看作是一个含有不定数量参数的匿名函数。例如,loadstring("a = 1")跟下面的表达式基本等价:
function (...) a = 1 end
跟其他函数一样,代码块也可以声明局部变量:
f = loadstring("local a = 10; print(a + 20)") f() --> 30
利用这个特性,我们可以重写上面的一个例子:
print "enter function to be plotted (with variable 'x'):" local l = io.read() local f = assert(loadstring("local x = ...; return " .. l)) for i=1,20 do print(string.rep("*", f(i))) end
在代码的开始处,加了“local x = ...”,将x声明为局部变量。调用f 函数的时候,实参i 就变成了变参表达式"..."的值。运行结果,跟上面一样,就不截图了。
load 函数不会发生运行时错误崩溃。如果出错了,总是返回nil+err_msg:
一个很常见的误解:将load代码块与定义函数划等号。在Lua中,函数定义实际上是赋值行为,而且是在运行时才发生,而不是在编译时。例如,我们有个文件foo.lua:
function foo (x) print(x) end
接着运行下面这条cmd:
f = loadfile("foo.lua")
这个时候,foo被编译了,但是还没有被定义。要定义它,必须运行这个代码块:
print(foo) --> nil f() -- defines 'foo' foo("ok") --> ok
在生产环境中的程序,在运行外部代码的时候,要尽可能的捕获所有的错误并作出处理。甚至,如果这些代码不被信任,那就应该在一个安全的环境中运行,避免在运行这些外来代码的时候,产生一些不愉快的事情。
不像Lua代码,C代码必须先跟程序链接才能被使用。在大多数系统中,做这个链接动作的做简单的方法是:使用动态链接功能。那么怎么检测你的环境是否已经支持这个功能呢?
运行 print(package.loadlib("a", "b"))。如果出现类似说文件不存在这样的错误,那么就说明你的环境支持这个动态链接功能啦。否则,出现的错误信息会告诉你这个功能不支持,或者没有被安装。下面是我的环境的表现:
package.loadlib 有两个string类型的参数,第一个是库的完整路径,第二个是函数的名字。因此,一个典型的调用应该跟下面很相似:
local path = "/usr/local/lib/lua/5.1/socket.so" local f = package.loadlib(path, "luaopen_socket")
loadlib 函数,加载一个给定的库,并链接,但是并没有去调用那个函数。而是将这个C 函数当作Lua函数返回。如果这个过程出现什么错误,那么loadlib 会返回nil + err_msg。
要使用loadlib 函数,我们必须要给出库的完整路径和准确的函数名字,好像有点麻烦。这里有个替代方案,require 函数。我们后面在讨论这个函数,这里只需知道有这么个东西就可以了。
是个人就难免会犯错。因此我们要尽可能的处理可捕获的错误。因为Lua是一个扩展语言,通常是被嵌入到别的程序(姑且叫做宿主程序吧)中,在出错的时候,不能简单的让它崩溃掉或者退出。而是结束执行当前的代码块,并返回到宿主程序中。
Lua遇到任何非期望的条件,都会产生一个错误。例如,对非数值进行加运算,调用一个非函数的值,索引一个非table的值,等等。可以显式地调用error 函数来产生一个错误。例如:
print "enter a number:" n = io.read("*number") if not n then error("invalid input") end
if not condition then error end,在Lua中被封装成了assert 函数:
print "enter a number:" n = assert(io.read("*number"), "invalid input")
assert 检查它的第一个参数,如果为nil或者false,就产生一个error。第二个参数是可选的。
在函数发现错误时,有两种处理方式,一个是返回error code,另一个是产生错误(联想下C语言中的assert)。如何选择呢?建议如下:可以轻松避免的错误,可以通过编码来修改并规避的,产生错误;否则返回errcode。
举个例子,sin 函数,如果参数用了一个table,假设它返回了一个error code,如果我们需要去检查一下这个错误,那么代码应该像下面这样写:
local res = math.sin(x) if not res then -- error? <error-handling code>
但是,实际上,我们可以在调用sin 函数之前就检查一下x 是否合法:
if not tonumber(x) then -- x is not a number? <error-handling code>
如果参数x不是一个数值,那么意味着你的程序中某个地方出错了。这种情况下,停止运行并产生一个错误信息,是最简单有效的方式来处理这个错误。
我们再来看下io.open 函数,当我去open一个并不存在的file时,会怎么样呢?这个情况,并不能提前检查这个file是否存在,因为在很多系统中,要想知道某个file是否存在,只有去尝试打开它。因此,如果函数io.open 因为一些外部原因(例如file does not exist, permisson denied)而不能打开一个file,它会返回nil + err_msg。这样的话,我们就可以进行一些处理,例如要求用户重新输入一个文件名:
local file, msg repeat print "enter a file name:" local name = io.read() if not name then return end -- no input file, msg = io.open(name, "r") if not file then print(msg) end until file
如果仅仅希望保证io.open 能够正常工作,可以简单的使用:
file = assert(io.open(name, "r"))
这是Lua中的一个习惯用法,如果io.open失败了,就会产生一个错误。
对大多数程序来说,不需要在Lua代码中进行错误处理,宿主程序本身会对错误进行相应处理。所有的Lua动作基本都是由宿主程序调用起来的,如果发生错误,Lua代码块只需要返回相应的err_code,宿主程序本身针对err_code做出相应的处理。在独立的Lua解析器中,出错的时候,也只是打印出相应的错误信息,然后继续提示用户继续进行运行其他的命令。
如果想要在Lua中对错误进行处理,那么必须用pcall (protected call)函数来封装代码。
假设我们运行一段lua代码,并捕获到运行过程中出现的错误。我们首先要做的就是封装这段代码,假设封装成函数foo :
function foo () <some code> if unexpected_condition then error() end <some code> print(a[i]) -- potential error: 'a' may not be a table <some code> end
然后用pcall 去调用这个函数:
if pcall(foo) then -- no errors while running 'foo' <regular code> else -- 'foo' raised an error: take appropriate actions <error-handling code> end
上面的foo 函数也可以替换成匿名函数的。
pcall 会在protected mode下调用它的第一个参数,以便能够在函数运行过程中捕获到出现的错误。如果函数运行正常,没有错误产生,pcall 返回true + 函数的返回值;如果出现错误,pcall 返回false + err_msg。
err_msg不一定非得是string,任何传递给error 的值都会被pcall 返回,例如下面的示例:
local status, err = pcall(function () error({code=121}) end) print(status, err.code, type(err))
运行结果如下:
像上面说的,任何类型的值都可以作为err_msg,但是,通常err_msg还是string类型的,说明发生了什么错误。当遇到了内部错误(例如试图索引一个非table值),Lua负责产生err_msg;否则,err_msg就是传递给error 函数的值。另外,Lua总是在错误发生的地方添加一些位置信息, 如下示例:
local status, err = pcall(function () a = "a"+1 end) print(err)
local status, err = pcall(function () error("my error") end) print(err)
位置信息指明了filename和line number。
error 函数有一个额外的参数level, 说明如何获取错误发生的位置。level默认为1,返回error 函数被调用的位置; level 为2, 返回调用error 函数的函数被调用的位置; level 为0,不获取位置信息。对比下下面三段代码的的区别和执行结果就明白了。
function foo(str) if type(str) ~= "string" then error("string expected", 0) --level 0 end print("foo success") end local status, err = pcall(foo(3)) print(err)
运行结果:
function foo(str) if type(str) ~= "string" then error("string expected", 1) --level 1 end print("foo success") end local status, err = pcall(foo(3)) print(err)
运行结果:
function foo(str) if type(str) ~= "string" then error("string expected", 2) --level 2 end print("foo success") end local status, err = pcall(foo(3)) print(err)
运行结果:
err_test03.lua, err_test04.lua是我code文件的名字而已,可能每个人起名字都不同的。注意一下level 1 和level 2的错误行号是不同的。通过这3个例子,很明白了吧。
通常情况下,在程序出错的时候,可能仅仅知道错误发生的位置是不够的。至少,还需要函数的调用堆栈吧。但是当pcall 函数返回的时候,堆栈信息已被部分破坏了。因此,为了获取堆栈信息,必须在pcall 返回之前就建立它。Lua为我们提供了xpcall 函数, 它比pcall 函数多一个参数 error handler function。 一旦发生错误,Lua在堆栈被损坏之前调用这个handler 函数,在handler 中,我们可以用debug 库来收集尽可能的有关错误的信息。两个常用的handler 函数是debug.debug 和 debug.traceback ;具体用法,后续会详细讨论。
终于写完这篇了,关电脑睡觉去。
水平有限,如果有朋友发现错误,欢迎留言交流