一、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 可以避免这种情况。tonumber(n)可以将一个内容为数字的字符串转化为数字变量
---------------------------------------------------------------------------
当函数遇到异常有两个基本的动作:返回错误代码或者抛出错误。这两种方式选择哪一种没有固定的规则,但有一般的原则:容易避免的异常应该抛出错误否则返回错误代码。
例如我们考虑 sin 函数,如果以一个 table 作为参数,假定我们返回错误代码,我们需要检查错误的发生,代码可能如下:
local res = math.sin(x)
if not res then -- 错误
<错误处理代码>
然而我们可以在调用函数以前很容易的判断是否有异常:
if not tonumber(x) then -- 错误:x不是一个数字字符串
<错误处理代码>
然而通常情况下我们既不是检查参数也不是检查返回结果,因为参数错误可能意味着我们的程序某个地方存在问题,这种情况下,处理异常最简单最实际的方式是抛出错误并且终止代码的运行。
另一个例子 io.open 函数用来打开一个文件,如果文件不存在结果会怎么样呢?
很多系统中,通过试着去打开文件来判断是否文件存在。所以如果 io.open 不能打开文件(由于文件不存在或者没有权限),函数返回 nil 和错误信息。以这种方式我们可以通过与用户交互(比如:是否要打开另一个文件)合理的处理问题:
local file, msg
repeat
print "enter a file name:"
local name = io.read()
if not name then return end -- 不输入
file, msg = io.open(name, "r")
if not file then print(msg) end -->如果没有找到文件,则返回错误信息msg
until file -->repeat until的作用是让用户查找正确的文件名,直到找到真实的文件时才跳出循环
如果你想偷懒不想处理这些情况,又想代码安全的运行,可以简单的使用 assert:
file = assert(io.open(name, "r")) -->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 的第二个参数。
二、异常和错误处理:很多应用中,不需要在 Lua 进行错误处理,一般有应用来完成。
通常应用要求Lua运行一段程序块,如果发生异常,应用根据Lua返回的错误代码进行处理。在控制台模式下的Lua解释器如果遇到异常,打印出错误然后继续显示提示符并且等待下一个命令。
如果在 Lua 中需要处理错误,需要使用 pcall 函数封装你的代码。
假定你想运行一段 Lua 代码,这段代码运行过程中可以捕捉所有的异常和错误。
使用方式:
第一步:将这段代码封装在一个函数内
function foo ()
<代码块>
if 未预期的条件 then error() end
<代码块>
print(a[i]) -- a必须为一个table类型的变量,如果不为table变量,它也会打印出来
<代码块>
end
第二步:使用 pcall 调用这个函数
if pcall(foo) then
-- 如果没有错误,则会返回true、函数返回值并且执行以下代码
<常规代码块>
else
-- 如果foo函数出错则会返回false、错误消息,同时会执行以下代码
<错误处理代码块>
end
当然也可以用匿名函数的方式调用 pcall:
if pcall(function () <受保护的代码块> end) then
<常规代码块>
else <错误处理代码块>
end
-----------------------------------------------------------
pcall 在保护模式下调用他的第一个参数并运行,因此可以捕获所有的异常和错误。
如果没有异常和错误,pcall 返回 true 和调用返回的任何值;否则返回 nil 加错误信息。
错误信息不一定非要是一个字符串(下面的例子是一个 table),传递给 error 的任何信息都会被 pcall 返回
例:
do
local status, err = pcall(function () error({code=121}) end)
print(err.code) --> 121
end
这种机制提供了我们在 Lua 中处理异常和错误的所需要的全部内容。我们通过 error抛出异常,然后通过 pcall 捕获他。
三、错误信息和回跟踪(Tracebacks):虽然你可以使用任何类型的值作为错误信息,通常情况下,我们使用字符串来描述遇到的错误信息。如果遇到内部错误(比如对一个非table 的值使用索引下表访问)Lua将自己产生错误信息,否则 Lua 使用传递给 error 函数的参数作为错误信息。不管在什么情况下,Lua 都尽可能清楚的描述发生的错误。
例:
do
local status, err = pcall(function () a = 'a'+1 end)
print(err)
--> stdin:1:尝试对字符串值执行算术运算
local status, err = pcall(function () error("my error") end)
print(err)
--> stdin:1: my error
end
例子中错误信息给出了文件名(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.traceback,前者给出 Lua 的提示符,你可以自己动手查看错误发生时的情况;后者通过 traceback 创建更多的错误信息,后者是控制台解释器用来构建错误信息的函数。你可以在任何时候调用debug.traceback获取当前运行的traceback信息:
print(debug.traceback())