尽管Lua是一门解析型的语言,但是在运行前也会被编译成某个中间状态。一门解析型的语言需要编译,这听起来有点不合常理。但是,实际上,解析型语言的与众不同,不是说它不需要编译,而是说它把编译作为其运行时的一部分,因此,它就可以执行各种来自外部的代码(例如网上的)。也许因为Lua中存在的如*dofile* 这样的函数,才使Lua可以被称为一门解析型语言。
1. 编译
之前我们介绍了*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())
~~~
看一下运行情况:(注意第一个为什么报错了,想想什么才叫表达式)
![](https://box.kancloud.cn/2016-09-06_57ce5ef02e398.PNG)
*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给定的次数),下面是运行结果:
![](https://box.kancloud.cn/2016-09-06_57ce5ef0464c6.PNG)
如果我们在深究一下,其实不管*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:
![](https://box.kancloud.cn/2016-09-06_57ce5ef05f4e3.PNG)
一个很常见的误解:将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
~~~
在生产环境中的程序,在运行外部代码的时候,要尽可能的捕获所有的错误并作出处理。甚至,如果这些代码不被信任,那就应该在一个安全的环境中运行,避免在运行这些外来代码的时候,产生一些不愉快的事情。
## 2. C代码
不像Lua代码,C代码必须先跟程序链接才能被使用。在大多数系统中,做这个链接动作的做简单的方法是:使用动态链接功能。那么怎么检测你的环境是否已经支持这个功能呢?
运行 *print(package.loadlib("a", "b"))*。如果出现类似说文件不存在这样的错误,那么就说明你的环境支持这个动态链接功能啦。否则,出现的错误信息会告诉你这个功能不支持,或者没有被安装。下面是我的环境的表现:
![](https://box.kancloud.cn/2016-09-06_57ce5ef077024.PNG)
*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* 函数。我们后面在讨论这个函数,这里只需知道有这么个东西就可以了。
## 3. 错误
是个人就难免会犯错。因此我们要尽可能的处理可捕获的错误。因为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*失败了,就会产生一个错误。
## 4. 错误处理和异常
对大多数程序来说,不需要在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))
~~~
运行结果如下:
![](https://box.kancloud.cn/2016-09-06_57ce5ef091498.PNG)
## 5. 错误信息和堆栈
像上面说的,任何类型的值都可以作为err_msg,但是,通常err_msg还是string类型的,说明发生了什么错误。当遇到了内部错误(例如试图索引一个非table值),Lua负责产生err_msg;否则,err_msg就是传递给error 函数的值。另外,Lua总是在错误发生的地方添加一些位置信息, 如下示例:
~~~
local status, err = pcall(function () a = "a"+1 end)
print(err)
~~~
![](https://box.kancloud.cn/2016-09-06_57ce5ef0acc62.PNG)
~~~
local status, err = pcall(function () error("my error") end)
print(err)
~~~
![](https://box.kancloud.cn/2016-09-06_57ce5ef0c46ab.PNG)
位置信息指明了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)
~~~
运行结果:
![](https://box.kancloud.cn/2016-09-06_57ce5ef0d8604.PNG)
~~~
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)
~~~
运行结果:
![](https://box.kancloud.cn/2016-09-06_57ce5ef0f1a7c.PNG)
~~~
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)
~~~
运行结果:
![](https://box.kancloud.cn/2016-09-06_57ce5ef114199.PNG)
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* ;具体用法,后续会详细讨论。
终于写完这篇了,关电脑睡觉去。
水平有限,如果有朋友发现错误,欢迎留言交流