第八章:错误处理与调试

在编程过程中,错误是不可避免的。Lua 提供了几种处理错误和调试代码的机制,帮助我们编写更健壮的程序。

Lua 将错误主要分为两类:语法错误(Syntax Errors)和运行时错误(Runtime Errors)。本章主要讨论运行时错误的捕获与处理。

8.1 错误处理

error 函数

我们可以使用 error 函数显式地抛出一个错误,终止程序的执行。

print("Enter a number:")
local n = io.read("n")
if not n then
    error("Invalid input! Not a number.")
end

assert 函数

assert(v [, message]) 检查第一个参数 v 是否为真(非 false 且非 nil)。如果是真,返回 v;如果是假,抛出一个错误,错误信息为 message(默认为 “assertion failed!”)。

local n = assert(io.read("n"), "invalid input")

相当于:

local n = io.read("n")
if not n then
    error("invalid input")
end

pcall (Protected Call)

pcall 用于以保护模式调用函数。如果函数执行过程中发生错误,pcall 会捕获错误并返回 false 和错误信息,而不会导致整个程序崩溃。如果执行成功,返回 true 和函数的返回值。

语法:status, result = pcall(func, arg1, arg2, ...)

local function div(a, b)
    if b == 0 then error("division by zero") end
    return a / b
end

local status, result = pcall(div, 10, 2)
if status then
    print("Result:", result) -- Result: 5.0
else
    print("Error:", result)
end

local status, result = pcall(div, 10, 0)
if status then
    print("Result:", result)
else
    print("Error:", result) -- Error: division by zero
end

xpcall

xpcall(func, msgh, arg1, ...) 类似于 pcall,但多了一个错误处理函数 msgh(Message Handler)。当发生错误时,Lua 会在栈展开(Stack Unwinding)之前调用 msgh,这使得我们可以获取调用栈信息。

local function myErrorHandler(err)
    print("Error occurred:", err)
    print(debug.traceback()) -- 打印调用栈
    return err
end

local function faulty()
    error("Something went wrong")
end

local status = xpcall(faulty, myErrorHandler)
print("Status:", status) -- Status: false

8.2 调试 (Debug)

Lua 提供了一个名为 debug 的标准库,包含许多用于调试的功能。

常用 debug 函数

  • debug.traceback([message]): 返回当前调用栈的字符串表示。

  • debug.getinfo(func [, what]): 返回关于函数的信息表(如源文件名、行号、参数个数等)。

  • debug.getlocal(level, index): 获取局部变量的名称和值。

  • debug.setlocal(level, index, value): 设置局部变量的值。

  • debug.getupvalue(func, index): 获取闭包的上值(Upvalue)。

  • debug.setupvalue(func, index, value): 设置闭包的上值。

简易调试器示例

我们可以利用 debug.debug() 进入一个交互式调试环境,但这通常用于开发阶段手动调试。在代码中,我们可以编写一个简单的钩子(Hook)来跟踪程序执行。

debug.sethook(function(event, line)
    local info = debug.getinfo(2)
    print(event, info.short_src, line)
end, "l") -- "l" 表示每行代码执行时触发

性能分析 (Profiling)

通过 debug.sethook,我们还可以统计函数调用的次数或耗时,从而进行性能分析。

local Counters = {}
local Names = {}

local function hook()
    local f = debug.getinfo(2, "f").func
    if Counters[f] == nil then
        Counters[f] = 1
        Names[f] = debug.getinfo(2, "Sn")
    else
        Counters[f] = Counters[f] + 1
    end
end

debug.sethook(hook, "c") -- "c" 表示函数调用时触发

-- 运行一些代码
function foo() end
function bar() foo() end
bar()
bar()

-- 打印统计结果
for f, count in pairs(Counters) do
    print(Names[f].name or "anonymous", count)
end

8.3 常见错误与解决方案

  1. attempt to index a nil value: 尝试对 nil 进行索引操作(例如 t.kt[k]),通常是因为表未初始化或键不存在。

  2. attempt to call a nil value: 尝试调用一个 nil 值(例如 f()),通常是因为函数名拼写错误或变量未赋值。

  3. stack overflow: 栈溢出,通常是因为无限递归且未进行尾调用优化。

  4. C stack overflow: C 栈溢出,通常是因为过深的递归调用(即使是尾调用也可能触发 C 栈限制,但这在纯 Lua 中较少见)。

练习题

  1. 编写一个函数 safe_read_number(),使用 pcall 尝试读取用户输入的数字,如果输入非法,提示用户重新输入,直到成功为止。

  2. 使用 xpcall 捕获一个除以零的错误,并在错误处理函数中打印出发生错误的文件名和行号。

  3. 研究 debug.getinfo 函数,编写一个辅助函数 print_caller_info(),打印出调用该函数的上层函数的名字。


下一章预告:Lua 的标准库虽然小巧,但涵盖了数学、操作系统、表、字符串等常用功能。下一章我们将通过实例来学习这些标准库的使用。