第8 章 编译·运行·调试
虽然我们把 Lua当作解释型语言,但是 Lua 会首先把代码预编译成中间码然后再执 行(很多解释型语言都是这么做的)。在解释型语言中存在编译阶段昕起来不合适,然而, 解释型语言的特征不在于他们是否被编译,而是编译器是语言运行时的一部分,所以,执行编译产生的中间码速度会更快。我们可以说函数 dofile 的存在就是说明可以将 Lua作为一种解释型语言被调用。
前面我们介绍过 dofile,把它当作 Lua 运行代码的 chunk 的一种原始的操作。dofile 实际上是一个辅助的函数。真正完成功能的函数是 loadfile;与 dofile 不同的是 loadfile 编译代码成中间码并且返回编译后的 chunk作为一个函数,而不执行代码;另外 loadfile 不会抛出错误信息而是返回错误代。.我们可以这样定义 dofile:
function dofile (filename) local f = assert(loadfile(filename)) return f() end
如果 loadfile 失败 assert会抛出错误。
完成简单的功能 dofile 比较方便,他读入文件编译并且执行。然而 loadfile 更加灵活。 在发生错误的情况下,loadfile 返回 nil 和错误信息,这样我们就可以自定义错误处理。 另外,如果我们运行一个文件多次的话,loadfile 只需要编译一次,但可多次运行。dofile 却每次都要编译。
loadstring 与 loadfile 相似,只不过它不是从文件里读入 chunk,而是从一个串中读入。 例如:
f =loadstring("i = i + 1")
f 将是一个函数,调用时执行 i=i+1。
i = 0 f(); print(i) --> 1 f(); print(i) --> 2
loadstring 函数功能强大,但使用时需多加小心。确认没有其它简单的解决问题的方 法再使用。
Lua把每一个 chunk 都作为一个匿名函数处理。例如:chunk "a= 1",loadstring 返 回与其等价的 function() a = 1 end。与其他函数一样,chunks 可以定义局部变量也可以返回值:
</pre></p></div><pre name="code" class="plain">f =loadstring("local a = 10; returna + 20") print(f()) --> 30
loadfile 和 loadstring 都不会抛出错误,如果发生错误他们将返回 nil 加上错误信息:
print(loadstring("i i")) --> nil [string "ii"]:1: '=' expectednear 'i'
另外,loadfile 和 loadstring 都不会有边界效应产生,他们仅仅编译 chunk 成为自己 内部实现的一个匿名函数。通常对他们的误解是他们定义了函数。Lua中的函数定义是发生在运行时的赋值而不是发生在编译时。假如我们有一个文件 foo.lua:
-- file `foo.lua' function foo (x) print(x) end
当我们执行命令 f = loadfile("foo.lua")后,foo 被编译了但还没有被定义,如果要定 义他必须运行 chunk:
f() --defines `foo' foo("ok") --> ok
如果你想快捷的调用 dostringC比如加载并运行),可以这样
loadstring(s)()
调用 loadstring 返回的结果,然而如果加载的内容存在语法错误的话,loadstring 返 回 nil 和错误信息Cattempt to call a nil value);为了返回更清楚的错误信息可以使用 assert:
assert(loadstring(s))()
通常使用 loadstring 加载一个字串没什么意义,例如:
f =loadstring("i = i + 1")
大概与 f = function() i = i + 1 end 等价,但是第二段代码速度更快因为它只需要编译 一次,第一段代码每次调用loadstring 都会重新编译,还有一个重要区别:loadstring 编 译的时候不关心词法范围:
local i = 0 f = loadstring("i = i +1") g = function () i = i + 1 end
这个例子中,和想象的一样 g 使用局部变量 i,然而 f 使用全局变量 i;loadstring 总 是在全局环境中编译他的串。
loadstring 通常用于运行程序外部的代码,比如运行用户自定义的代码。注意: loadstring 期望一个 chunk,即语句。如果想要加载表达式,需要在表达式前加 return, 那样将返回表达式的值。看例子:
print "enter your expression:" local l = io.read() local func = assert(loadstring("return " .. l)) print("the value ofyour 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 thechunk) print(string.rep("*", f())) end
8.1require 函数
Lua 提供高级的 require 函数来加载运行库。粗略的说 require和 dofile 完成同样的功 能但有两点不同:
1. require 会搜索目录加载文件
2. require会判断是否文件己经加载避免重复加载同一文件。由于上述特征,require 在 Lua 中是加载库的更好的函数。
require 使用的路径和普通我们看到的路径还有些区别,我们一般见到的路径都是一 个目录列表。require 的路径是一个模式列表,每一个模式指明一种由虚文件名Crequire 的参数)转成实文件名的方法。更明确地说,每一个模式是一个包含可选的问号的文件 名。匹配的时候 Lua会首先将问号用虚文件名替换,然后看是否有这样的文件存在。如 果不存在继续用同样的方法用第二个模式匹配。例如,路径如下:
?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua
调用 require "lili"时会试着打开这些文件:
lili lili.lua c:\windows\lili /usr/local/lua/lili/lili.lua
require 关注的问题只有分号(模式之间的分隔符)和问号,其他的信息(目录分隔 符,文件扩展名)在路径中定义。为了确定路径,Lua 首先检查全局变量 LUA_PATH 是否为一个字符串,如果是则认 为这个串就是路径;否则 require 检查环境变量 LUA_PATH 的值,如果两个都失败 require 使用国定的路径(典型的"?;?.lua")
require 的另一个功能是避免重复加载同一个文件两次。Lua 保留一张所有己经加载 的文件的列表(使用 table 保存)。如果一个加载的文件在表中存在 require 简单的返回; 表中保留加载的文件的虚名,而不是实文件名。所以如果你使用不同的虚文件名 require同一个文件两次,将会加载两次该文件。比如require "foo"和 require "foo.lua",路径为 "?;?.lua"将会加载 foo.lua 两次。我们也可以通过全局变量_LOADED 访问文件名列表, 这样我们就可以判断文件是否被加载过;同样我们也可以使用一点小技巧让 require 加载 一个文件两次。比如,require "foo"之后_LOADED["foo"]将不为 nil,我们可以将其赋值 为 nil,require "foo.lua"将会再次加载该文件。
一个路径中的模式也可以不包含问号而只是一个国定的路径,比如:
?;?.lua;/usr/local/default.lua
这种情况下,require 没有匹配的时候就会使用这个国定的文件(当然这个国定的路 径必须放在模式列表的最后才有意义)。在 require运行一个 chunk 以前,它定义了一个 全局变量_REQUIREDNAME 用来保存被 required 的虚文件的文件名。我们可以通过使 用这个 技 巧扩展 require 的功能 。 举个极 端 的例子 , 我们可 以 把路径 设 为"/usr/local/lua/newrequire.lua",这样以后每次调用 require 都会运行 newrequire.lua,这种 情况下可以通过使用_REQUIREDNAME 的值去实际加载 required的文件。
8.2 C Packages
Lua 和 C 是很容易结合的,使用 C 为 Lua 写包。与 Lua 中写包不同,C 包在使用以 前必须首先加载并连接,在大多数系统中最容易的实现方式是通过动态连接库机制,然而动态连接库不是 ANSIC 的一部分,也就是说在标准 C中实现动态连接是很困难的。
通常 Lua 不包含任何不能用标准 C 实现的机制,动态连接库是一个特例。我们可以 将动态连接库机制视为其他机制之母:一旦我们拥有了动态连接机制,我们就可以动态 的加载 Lua 中不存在的机制。所以,在这种特殊情况下,Lua打破了他平台兼容的原则而通过条件编译的方式为一些平台实现了动态连接机制。标准的 Lua 为 windows、Linux、 FreeBSD、Solaris 和其他一些 Unix 平台实现了这种机制,扩展其它平台支持这种机制也 是不难的。在 Lua提示符下运行 print(loadlib())看返回的结果,如果显示 bad arguments则说明你的发布版支持动态连接机制,否则说明动态连接机制不支持或者没有安装。
Lua在一个叫 loadlib 的函数内提供了所有的动态连接的功能。这个函数有两个参数:库的绝对路径和初始化函数。所以典型的调用的例子如下:
local path = "/usr/local/lua/lib/libluasocket.so" local f = loadlib(path, "luaopen_socket")
loadlib 函数加载指定的库并且连接到 Lua,然而它并不打开库(也就是说没有调用 初始化函数),反之他返回初始化函数作为 Lua的一个函数,这样我们就可以直接在 Lua中调用他。如果加载动态库或者查找初始化函数时出错,loadlib将返回 nil 和错误信息。我们可以修改前面一段代码,使其检测错误然后调用初始化函数:
<pre name="code" class="plain"><pre name="code" class="plain">local path = "/usr/local/lua/lib/libluasocket.so" -- or path = "C:\\windows\\luasocket.dll" local f = assert(loadlib(path, "luaopen_socket")) f()-- actually openthe library
一般情况下我们期望二进制的发布库包含一个与前面代码段相似的 stub 文件,安装 二进制库的时候可以随便放在某个目录,只需要修改stub 文件对应二进制库的实际路径 即可。将 stub文件所在的目录加入到 LUA_PATH,这样设定后就可以使用 require 函数 加载 C 库了。
8.3 错误
Errare humanum est(拉丁谚语:犯错是人的本性)。所以我们要尽可能的防止错误 的发生,Lua经常作为扩展语言嵌入在别的应用中,所以不能当错误发生时简单的崩溃或者退出。相反,当错误发生时 Lua结束当前的 chunk 并返回到应用中。
当 Lua 遇到不期望的情况时就会抛出错误,比如:两个非数字进行相加;调用一个 非函数的变量;访问表中不存在的值等(可以通过 metatables 修改这种行为,后面介绍)。 你也可以通过调用 error 函数显示的抛出错误,error 的参数是要抛出的错误信息。
print "entera number:" n =io.read("*number") if not nthen error("invalid input")end
Lua 提供了专门的内置函数 assert来完成上面类似的功能:
print "entera number:" n =assert(io.read("*number"),"invalid input")
assert 首先检查第一个参数是否返回错误,如果不返回错误 assert简单的返回,否则 assert以第二个参数抛出错误信息。第二个参数是可选的。注意 assert 是普通的函数,他会首先计算两个参数然后再调用函数,所以以下代码:
n =io.read() assert(tonumber(n), "invalidinput:" .. n .. " is not a number")
将会总是进行连接操作,使用显示的 test 可以避免这种情况。 当函数遇到异常有两个基本的动作:返回错误代码或者抛出错误。这两种方式选择,哪一种没有国定的规则,但有一般的原则:容易避免的异常应该抛出错误否则返回错误代码。例如我们考虑 sin 函数,如果以一个 table 作为参数,假定我们返回错误代码,我们 需要检查错误的发生,代码可能如下:
localres = math.sin(x) if not resthen -- error ...
然而我们可以在调用函数以前很容易的判断是否有异常:
if not tonumber(x)then -- error: x is not anumber ...
然而通常情况下我们既不是检查参数也不是检查返回结果,因为参数错误可能意味 着我们的程序某个地方存在问题,这种情况下,处理异常最简单最实际的方式是抛出错误并且终止代码的运行。
再来看一个例子 io.open 函数用来打开一个文件,如果文件不存在结果会怎么样呢? 很多系统中,通过试着去打开文件来判断是否文件存在。所以如果io.open 不能打开文件 (由于文件不存在或者没有权限),函数返回 nil 和错误信息。以这种方式我们可以通过 与用户交互(比如:是否要打开另一个文件)合理的处理问题:
<pre name="code" class="plain">localfile, msg repeat print "entera file name:" localname = io.read() if not namethen return end -- no input file, msg = io.open(name, "r") if not filethen print(msg) end until file
如果你想偷懒不想处理这些情况,又想代码安全的运行,可以简单的使用 assert:
file =assert(io.open(name, "r"))
Lua 中有一个习惯:如果 io.open 失败,assert 将抛出错误。
file =assert(io.open("no-file","r")) --> stdin:1: no-file:No such fileor directory
注意:io.open 返回的第二个结果(错误信息)作为 assert 的第二个参数。
8.4 异常和错误处理
很多应用中,不需要在 Lua 进行错误处理,一般有应用来完成。通常应用要求 Lua运行一段 chunk,如果发生异常,应用根据 Lua返回的错误代码进行处理。在控制台模 式下的 Lua解释器如果遇到异常,打印出错误然后继续显示提示符等待下一个命令。
如果在 Lua 中需要处理错误,需要使用 pcall 函数封装你的代码。 假定你想运行一段 Lua 代码,这段代码运行过程中可以捕捉所有的异常和错误。第一步:将这段代码封装在一个函数内
functionfoo () ... if unexpected_condition then error() end ... print(a[i]) -- potential error:`a' may not be atable ... end
第二步:使用 pcall 调用这个函数
if pcall(foo) then -- no errorswhile running `foo' ... else -- `foo' raised an error: take appropriate actions ... end
当然也可以用匿名函数的方式调用 pcall:
if pcall( function () ... end) then... else...
pcall 在保护模式下调用他的第一个参数并运行,因此可以捕获所有的异常和错误。如果没有异常和错误,pcall 返回 true 和调用返回的任何值;否则返回 nil 加错误信息。错误信息不一定非要是一个字符串(下面的例子是一个 table),传递给 error的任何 信息都会被 pcall 返回:
local status, err = pcall (function() error ({code=121}) end) print(err.code) --> 121
这种机制提供了我们在 Lua 中处理异常和错误的所需要的全部内容。我们通过 error
抛出异常,然后通过 pcall 捕获他。
8.5错误信息和回跟踪(Tracebacks)
虽然你可以使用任何类型的值作为错误信息,通常情况下,我们使用字符串来描述 遇到的错误信息。如果遇到内部错误(比如对一个非 table 的值使用索引下表访问)Lua 将自己产生错误信息,否则 Lua 使用传递给 error 函数的参数作为错误信息。不管在什么 情况下,Lua 都尽可能清楚的描述发生的错误。
local status, err =pcall (function() a = 'a'+1 end) print(err) -->stdin:1: attempt to perform arithmetic on a string value local status, err = pcall (function() error("my error") end) print(err) --> stdin:1: myerror
例子中错误信息给出了文件名Cstdin)加上行号。
函数 error 还可以有第二个参数,表示错误的运行级别。有了这个参数你就无法抵赖 错误是别人的了,比如,加入你写了一个函数用来检查 error 是否被正确的调用:
functionfoo (str) if type(str) ~= "string"then error("string expected") end ... end
可能有人这样调用这个函数:
foo({x=1})
Lua 会指出发生错误的是 foo 而不是 error,实际的错误是调用 error 时产生的,为了纠正这个问题修改前面的代码让 error 报告错误发生在第二级(你自己的函数是第一级) 如下:
functionfoo (str) if type(str) ~= "string"then error("string expected", 2) end ... end
当错误发生的时候,我们常常需要更多的错误发生相关的信息,而不单单是错误发 生的位置。至少期望有一个完整的显示导致错误发生的调用栈的 tracebacks,当 pcall 返 回错误信息的时候他己经释放了保存错误发生情况的枝的信息。因此,如果我们想得到 tracebacks 我们必须在 pcall 返回以前获取。Lua 提供了 xpcall 来实现这个功能,xpcall 接受两个参数:调用函数和错误处理函数。当错误发生时。Lua 会在枝释放以前调用错 误处理函数,因此可以使用 debug 库收集错误相关的信息。有两个常用的 debug 处理函 数:debug。debug 和 debug.traceback,前者给出 Lua 的提示符,你可以自己动于察看错误 发生时的情况;后者通过 traceback 创建更多的错误信息,后者是控制台解释器用来构建 错误信息的函数。你可以在任何时候调用 debug.traceback 获取当前运行的 traceback 信息:
print(debug.traceback())