《Programming in Lua中文版》 -- 8.Compilation, Execution, and Errors

《Programming in Lua中文版》 -- 8.Compilation, Execution, and Errors

转自http://www.lifangjin.com/ (凉风集 李方进的个人BLOG)

虽然我们把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可以定义局部变量也可以返回值:

f = loadstring(”local a = 10; return a + 20″)

print(f()) –> 30

loadfile和loadstring都不会抛出错误,如果发生错误他们将返回nil加上错误信息:

print(loadstring(”i i”))

–> nil [string “i i”]:1: `=’ expected near `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

如果你想快捷的调用dostring (比如加载并运行)可以调用loadstring 返回的结果loadstring(s)(),然而如果加载的内容存在语法错误的话,loadstring返回nil和错误信息(attempt 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 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

8.1 require函数

Lua提供高级的require函数来加载运行库.粗略的说require和dofile完成同样的功能但有两点不同:1.require会搜索目录加载 文件2.require会判断是否文件已经加载避免重复加载同一文件.由于上述特征,require在Lua中是加载库的更好的函数.

require使用的路径和普通我们看到的路径还有些区别,我们一般见到的路径都是一个目录列表.require的路径是一个模式列表,每一个模式指明一 种由虚文件名(require的参数)转成实文件名的方法.更明确地说,每一个模式是一个包含可选的问号的文件名.匹配的时候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-包

Lua和C是很容易结合的,使用C为Lua写包.与Lua中写包不同,C包在使用以前必须首先加载并连接.在大多数系统中最容易的实现方式是通过动态连接库机制,然而动态连接库不是ANSI C的一部分,也就是说在标准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和错误信息.我们可以修改前面一段代码,使其检测错 误然后调用初始化函数:

local path = “/usr/local/lua/lib/libluasocket.so”

— or path = “C:\\windows\\luasocket.dll”

local f = assert(loadlib(path, “luaopen_socket”))

f() — actually open the library

一般情况下我们期望二进制的发布库包含一个与前面代码段相似的stub文件,安装二进制库的时候可以随便放在某个目录,只需要修改stub文件对应二进制 库的实际路径即可.将stub文件所在的目录加入到LUA_PATH,这样设定后就可以使用require函数加载C库了.

8.3 错误

Errare humanum est(拉丁谚语:犯错是人的本性).所以我们要尽可能的防止错误的发生,Lua经常作为扩展语言嵌入在别的应用中,所以不能当错误发生时简单的崩溃或者退出.相反,当错误发生时Lua结束当前的chunk并返回到应用中.

当Lua遇到不期望的情况时就会抛出错误,比如:两个非数字进行相加;调用一个非函数的变量;访问表中不存在的值等(可以通过metatables修改这 种行为,后面介绍).你也可以通过调用error函数显示的抛出错误,error的参数是要抛出的错误信息.

print “enter a number:”

n = io.read(”*number”)

if not n then error(”invalid input”) end

Lua提供了专门的内置函数assert来完成上面类似的功能:

print “enter a number:”

n = assert(io.read(”*number”), “invalid input”)

assert首先检查第一个参数是否返回错误,如果不返回错误assert简单的返回,否则assert以第二个参数抛出错误信息.第二个参数是可选的.注意assert是普通的函数,他会首先计算两个参数然后再调用函数,所以以下代码:

n = io.read()

assert(tonumber(n),

“invalid input: ” .. n .. ” is not a number”)

将会总是进行连接操作,使用显示的test可以避免这种情况.

当函数遇到异常有两个基本的动作:返回错误代码或者抛出错误.这两种方式选择哪一种没有固定的规则,但有一般的原则:容易避免的异常应该抛出错误否则返回错误代码.

例如我们考虑sin函数,如果以一个table作为参数,假定我们返回错误代码,我们需要检查错误的发生,代码可能如下:

local res = math.sin(x)

if not res then — error

然而我们可以在调用函数以前很容易的判断是否有异常:

if not tonumber(x) then — error: x is not a number

然而通常情况下我们既不是检查参数也不是检查返回结果,因为参数错误可能意味着我们的程序某个地方存在问题,这种情况下,处理异常最简单最实际的方式是抛出错误并且终止代码的运行.

再来看一个例子io.open函数用来打开一个文件,如果文件不存在结果会怎么样呢?很多系统中,通过试着去打开文件来判断是否文件存在.所以如果 io.open不能打开文件(由于文件不存在或者没有权限),函数返回nil和错误信息.以这种方式我们可以通过与用户交互(比如:是否要打开另一个文 件)合理的处理问题:

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

如果你想偷懒不想处理这些情况,又想代码安全的运行,可以简单的使用assert:

file = assert(io.open(name, “r”))

Lua中有一个习惯:如果io.open失败,assert将抛出错误.

file = assert(io.open(”no-file”, “r”))

–> stdin:1: no-file: No such file or directory

注意:io.open返回的第二个结果(错误信息)作为assert的第二个参数.

8.4 异常和错误处理

很多应用中,不需要在Lua进行错误处理,一般有应用来完成.通常应用要求Lua运行一段chunk,如果发生异常,应用根据Lua返回的错误代码进行处理.在控制台模式下的Lua解释器如果遇到异常,打印出错误然后继续显示提示符等待下一个命令.

如果在Lua中需要处理错误,需要使用pcall函数封装你的代码.

假定你想运行一段Lua代码,这段代码运行过程中可以捕捉所有的异常和错误,第一步:将这段代码封装在一个函数内,

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

当然也可以用匿名函数的方式调用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: my error

例子中错误信息给出了文件名(stdin)加上行号.

函数error还可以有第二个参数,表示错误的运行级别.有了这个参数你就无法抵赖错误是别人的了,比如,加入你写了一个函数用来检查error是否被正确的调用:

function foo (str)

if type(str) ~= “string” then

error(”string expected”)

end

end

可能有人这样调用这个函数:

foo({x=1})

Lua会指出发生错误的是foo而不是error,实际的错误是调用error时产生的,为了纠正这个问题修改前面的代码让error报告错误发生在第二级(你自己的函数是第一级)如下:

function foo (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())