第八课 编译、执行与错误
尽管Lua称为是一种解释型的语言,但Lua确实允许在运行源代码前,先将源代码预编译为一种中间形式。其实,区别解释型语言的主要特征并不在于是否能编译他们,而是在于编译器是否是语言运行时库的一部分,即是否有能力执行动态生成的代码。可以说正是因为存在了诸如dofile这样的函数,才可以将Lua称为一种解释型语言。编译dofile函数,是一种内置的操作,用于运行Lua代码块。但实际上dof
·
尽管Lua称为是一种解释型的语言,但Lua确实允许在运行源代码前,先将源代码预编译为一种中间形式。其实,区别解释型语言的主要特征并不在于是否能编译他们,而是在于编译器是否是语言运行时库的一部分,即是否有能力执行动态生成的代码。可以说正是因为存在了诸如dofile这样的函数,才可以将Lua称为一种解释型语言。
编译
dofile函数,是一种内置的操作,用于运行Lua代码块。但实际上dofile是 一个辅助函数,loadfile才做了真正核心的工作。类似于dofile,loadfile会从一个文件加载Lua代码块,但它不会运行代码块,只是编译代码,然后将编译结果作为一个函数返回。此外,与dofile不同的还有loadfile不会引发错误,它只是返回错误值并不会处理它。一般 dofile可以这样来定义:
function dofile (filename)
local f = assert(loadfile(filename))
return f()
end
如果需要多次运行一个文件,那么只需要在调用一次loadfile之后,多次调用返回结果就可以了。相对于多次调用dofile来说,由于只编译一次文件,开销就小的多了。
函数loadstring与loadfile类似,不同之处在于它是从一个字符串中读取代码。
f = loadstring("i = i + 1")
i = 0
f(); print(i) -->1
f(); print(i) -->2
一般将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
loadstring总是在全局环境中编译它的字符串。
loadstring最典型的 用处就是执行外部代码。例如,若让用户来参与一个函数的定义,那么这时就需要让用户输入函数代码,然后调用loadstring来对其求值。如果要对一个表达式求值,则必须在其之前添加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
print(string.rep("*", f())) --打印多少个*星号
end
Lua将所有独立的程序块视为一个匿名函数的函数体,并且该匿名函数还具有可变长实参。例如,loadstring("a = 1")返回的结果等价于以下表达式:
function (...) a = 1 end
与所有其他函数一样,程序块中可以声明局部变量:
f = loadstring("local a = 10; print(a + 20)")
f() -->30
通过这个特性,就可以重写那个用户参与的实例,在其中避免使用全局变量x:
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
这些函数不会带来任何副作用。它们只是将程序块编译为一种中间表示,然后将结果作为一个匿名函数来返回。常见的误解是认为加载了一个程序块,也就是定义了其中的函数。其实在Lua中,函数定义是一种赋值操作。也就是说,它们是在运行时才完成的操作。
foo.lua文件:
function foo (x)
print(x)
end
f = loadfile("foo.lua")
print(foo) -->nil
f() --定义foo
foo("ok") -->ok
C代码
Lua所提供的所有关于动态链接的功能都聚集在一个函数中,即package.loadlib。该函数有两个字符串参数:动态库的完整路径和一个函数名称。如:
local path = "/usr/local/lib/lua/5.1/socket.so"
local f = package.loadlib(path, "luaopen_socket")
loadlib加载指定的库,并将其链接入Lua。不过,它并没有调用库中的任何函数。相反,它将一个C函数作为Lua函数返回。如果在加载库或者查找 初始化函数时发生任何错误,loadlib返回nil及一条错误消息。
错误
由于Lua是一种扩展语言,通常嵌入在应用程序中,因此在发生错误时它不能简单地崩溃或退出。相反,只要发生了一个错误,Lua就应该结束当前的程序块并返回应用程序。
也可以显示地引发一个错误,通过调用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函数检查其第一个参数是否为true。若为true,则简单地返回该参数;否则(为false或者nil)就引发一个错误。Lua会在调用该函数前对其参数求值。
当一个函数遭遇了一种未预期的状况(即‘异常’),它可以采取两种基本的行为:返回错误代码(通常是nil)或引发一个错误(调用error)。
错误处理与异常
对于大多数应用程序而言,无须在Lua代码中作任何错误处理,应用程序本身会负责这类问题。
如果需要在Lua中处理错误,则必须使用函数pcall来包装需要执行的代码。
假设在执行一段Lua代码时,捕获所有执行中引发的错误,那么第一步就是将这段代码封装到一个函数中,将其称为foo:
function foo ()
<一些代码>
if 未预期的条件 then error() end
<一些代码>
print(a[i]) --潜在的错误:a可能不是一个table
<一些代码>
end
然后使用pcall来调用foo:
if pcall(foo) then
--在执行foo时没有发生错误
<常规代码>
else
--foo引发了一个错误,采取适当的行为
<错误处理代码>
end
当然,在调用pcall时可以传入一个匿名函数:
if pcall (function ()
<受保护的代码>
end) then
<常规代码>
else
<异常代码>
end
pcall函数会以一种“保护模式”来调用它的第一个参数,因此pcall可以捕获函数执行中的任何错误。如果没有发生错误,pcall会返回true以及函数调用的返回值;否则,返回false及错误消息。
任何类型的Lua值都可以作为错误消息传递给error函数,并且这些值也会成为pcall的返回值:
local status, err = pcall(function () error({code = 121}) end)
print(err.code) -->121
错误消息与追溯
虽然可以使用任何类型的值作为错误消息,但是错误消息通常是一个描述了出错内容的字符串。只要错误消息是一个字符串,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)以及行号(上例中的1)
error函数还有第二个附加参数level(层),用于指出应由调用层级中的哪个层函数来报告当前的错误,也就是说明了谁应该为错误负责。
如:
function foo (str)
if type(str) ~= "string" then
error("string expected")
end
<常规代码>
end
然后,其他人在调用函数时传入了错误的参数:
foo ({x = 1})
由于foo是调用了error,所以Lua会认为是函数发生了错误。但实际上却是foo的调用者造成的错误。为了纠正这个问题,就要告知error函数错误是发生在调用层级的第二层中(第一层是读函数):
function foo (str)
if type(str) ~= "string" then
error("string expected", 2)
end
<常规代码>
end
通常在错误发生时,希望得到更多的调试信息,而不是只有发生错误的位置。至少,能够追溯到发生错误时的函数调用情况,显示一个完整的函数调用栈。当pcall返回其错误消息时,它已经销毁了调用栈的部分内容。因此,如果希望的到一个有意义的调用栈,那么就必须在pcall返回前获取该信息。为了达成这一要求,Lua提供了xpcall函数。该函数除了接受一个需要被调用的函数外,还接受第二个参数----一个错误处理函数。当发生错误时,Lua会在调用栈展开前调用错误处理函数。于是就可以在这个函数中使用debug库来获取关于错误的额外的信息了。debug库提供了两个通用的错误处理函数,一个是debug.debug,它会提供一个Lua提示符,让用户来检查错误原因;另一个是debug.traceback,它会根据调用栈来构建一个扩展的错误消息。另外,也可以在任何时候调用这个函数来获取当前执行 的调用栈。
print(debug.traceback())
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
已为社区贡献1条内容
所有评论(0)