编译、运行、错误处理

最后更新于:2022-04-01 20:44:15

尽管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://docs.gechiui.com/gc-content/uploads/sites/kancloud/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://docs.gechiui.com/gc-content/uploads/sites/kancloud/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://docs.gechiui.com/gc-content/uploads/sites/kancloud/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://docs.gechiui.com/gc-content/uploads/sites/kancloud/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? ~~~ 但是,实际上,我们可以在调用sin 函数之前就检查一下x 是否合法: ~~~ if not tonumber(x) then -- x is not a number? ~~~ 如果参数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 () if unexpected_condition then error() end print(a[i]) -- potential error: 'a' may not be a table end ~~~ 然后用*pcall* 去调用这个函数: ~~~ if pcall(foo) then -- no errors while running 'foo' else -- 'foo' raised an error: take appropriate actions 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://docs.gechiui.com/gc-content/uploads/sites/kancloud/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://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-09-06_57ce5ef0acc62.PNG) ~~~ local status, err = pcall(function () error("my error") end) print(err) ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/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://docs.gechiui.com/gc-content/uploads/sites/kancloud/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://docs.gechiui.com/gc-content/uploads/sites/kancloud/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://docs.gechiui.com/gc-content/uploads/sites/kancloud/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* ;具体用法,后续会详细讨论。 终于写完这篇了,关电脑睡觉去。 水平有限,如果有朋友发现错误,欢迎留言交流
';