Skip to content

Lua错误处理

概述

错误处理是编程中的重要环节,Lua提供了多种机制来处理运行时错误。良好的错误处理可以让程序更加健壮,提供更好的用户体验,并且便于调试和维护。

1. 错误类型

1.1 语法错误

语法错误在编译时被检测到,程序无法运行。

lua
-- 语法错误示例(这些代码无法运行)
--[[
-- 缺少end
if true then
    print("Hello")

-- 括号不匹配
print("Hello"

-- 非法字符
print("Hello" + )
--]]

-- 正确的语法
if true then
    print("Hello")
end

print("Hello")

1.2 运行时错误

运行时错误在程序执行过程中发生。

lua
-- 除零错误
local function divide_by_zero()
    local a = 10
    local b = 0
    return a / b  -- 在某些情况下可能产生inf或nan
end

print(divide_by_zero())  -- inf

-- 访问nil值的字段
local function access_nil_field()
    local obj = nil
    return obj.field  -- 运行时错误
end

-- 这会产生错误
-- access_nil_field()  -- attempt to index a nil value

-- 调用非函数值
local function call_non_function()
    local not_a_function = 42
    return not_a_function()  -- 运行时错误
end

-- 这会产生错误
-- call_non_function()  -- attempt to call a number value

1.3 逻辑错误

逻辑错误不会导致程序崩溃,但会产生错误的结果。

lua
-- 逻辑错误示例
local function calculate_average(numbers)
    local sum = 0
    for i = 1, #numbers do
        sum = sum + numbers[i]
    end
    -- 逻辑错误:没有检查除零情况
    return sum / #numbers
end

local empty_array = {}
print(calculate_average(empty_array))  -- nan 或 inf

-- 修正版本
local function calculate_average_safe(numbers)
    if #numbers == 0 then
        return nil, "数组为空,无法计算平均值"
    end
    
    local sum = 0
    for i = 1, #numbers do
        sum = sum + numbers[i]
    end
    return sum / #numbers
end

local result, err = calculate_average_safe({})
if result then
    print("平均值:", result)
else
    print("错误:", err)
end

2. 错误处理机制

2.1 error函数

error函数用于主动抛出错误。

lua
-- 基本用法
local function validate_age(age)
    if type(age) ~= "number" then
        error("年龄必须是数字")
    end
    
    if age < 0 then
        error("年龄不能为负数")
    end
    
    if age > 150 then
        error("年龄不能超过150岁")
    end
    
    return true
end

-- 测试错误抛出
local function test_validate_age()
    local test_cases = {
        {25, true},
        {"abc", false},
        {-5, false},
        {200, false}
    }
    
    for _, case in ipairs(test_cases) do
        local age, should_succeed = case[1], case[2]
        local success, result = pcall(validate_age, age)
        
        if should_succeed then
            assert(success, "应该成功但失败了: " .. tostring(result))
            print("✓ 年龄", age, "验证通过")
        else
            assert(not success, "应该失败但成功了")
            print("✓ 年龄", age, "验证失败:", result)
        end
    end
end

test_validate_age()

-- 带错误级别的error
local function error_with_level()
    local function inner_function()
        error("内部错误", 2)  -- 错误级别2,指向调用者
    end
    
    inner_function()  -- 错误会指向这一行
end

-- 取消注释以测试
-- error_with_level()

2.2 assert函数

assert函数用于断言检查,条件为假时抛出错误。

lua
-- 基本assert用法
local function safe_divide(a, b)
    assert(type(a) == "number", "第一个参数必须是数字")
    assert(type(b) == "number", "第二个参数必须是数字")
    assert(b ~= 0, "除数不能为零")
    
    return a / b
end

print(safe_divide(10, 2))  -- 5

-- 自定义assert函数
local function custom_assert(condition, message, level)
    if not condition then
        error(message or "断言失败", (level or 1) + 1)
    end
    return condition
end

-- 使用自定义assert
local function process_user_data(user)
    custom_assert(user, "用户数据不能为空")
    custom_assert(user.name, "用户名不能为空")
    custom_assert(type(user.age) == "number", "年龄必须是数字")
    
    return "用户 " .. user.name .. " 年龄 " .. user.age
end

local user = {name = "Alice", age = 30}
print(process_user_data(user))

2.3 pcall和xpcall

pcall(protected call)和xpcall用于捕获错误。

lua
-- pcall基本用法
local function risky_operation(x)
    if x < 0 then
        error("x不能为负数")
    end
    return math.sqrt(x)
end

-- 使用pcall捕获错误
local function safe_sqrt(x)
    local success, result = pcall(risky_operation, x)
    
    if success then
        return result
    else
        print("错误:", result)
        return nil
    end
end

print(safe_sqrt(16))   -- 4
print(safe_sqrt(-4))   -- nil,并打印错误信息

-- xpcall用法(可以指定错误处理函数)
local function error_handler(err)
    print("捕获到错误:", err)
    print("调用栈:", debug.traceback())
    return "处理后的错误: " .. err
end

local function test_xpcall()
    local function problematic_function()
        error("这是一个测试错误")
    end
    
    local success, result = xpcall(problematic_function, error_handler)
    
    if not success then
        print("xpcall返回的错误:", result)
    end
end

test_xpcall()

-- 嵌套错误处理
local function nested_error_handling()
    local function level3()
        error("第三层错误")
    end
    
    local function level2()
        local success, err = pcall(level3)
        if not success then
            error("第二层错误: " .. err)
        end
    end
    
    local function level1()
        local success, err = pcall(level2)
        if not success then
            error("第一层错误: " .. err)
        end
    end
    
    local success, err = pcall(level1)
    print("最终捕获:", err)
end

nested_error_handling()

3. 错误处理模式

3.1 返回值模式

通过返回值来表示成功或失败。

lua
-- 返回值模式
local function parse_number(str)
    local num = tonumber(str)
    if num then
        return num, nil  -- 成功:返回结果和nil错误
    else
        return nil, "无法解析数字: " .. str  -- 失败:返回nil和错误信息
    end
end

-- 使用返回值模式
local function process_numbers(strings)
    local results = {}
    local errors = {}
    
    for i, str in ipairs(strings) do
        local num, err = parse_number(str)
        if num then
            results[i] = num
        else
            errors[i] = err
        end
    end
    
    return results, errors
end

local test_strings = {"123", "45.6", "abc", "78", "xyz"}
local results, errors = process_numbers(test_strings)

print("结果:")
for i, result in pairs(results) do
    print(i, result)
end

print("错误:")
for i, error in pairs(errors) do
    print(i, error)
end

3.2 异常模式

使用error和pcall实现类似异常的机制。

lua
-- 异常模式
local function try(func, catch_func, finally_func)
    local success, result = pcall(func)
    
    if not success and catch_func then
        result = catch_func(result)
    end
    
    if finally_func then
        finally_func()
    end
    
    if success then
        return result
    else
        error(result)
    end
end

-- 使用异常模式
local function file_operation_example()
    local file_handle = nil
    
    try(
        function()  -- try块
            file_handle = io.open("nonexistent.txt", "r")
            if not file_handle then
                error("无法打开文件")
            end
            
            local content = file_handle:read("*a")
            return content
        end,
        function(err)  -- catch块
            print("捕获异常:", err)
            return nil
        end,
        function()  -- finally块
            if file_handle then
                file_handle:close()
                print("文件已关闭")
            end
        end
    )
end

file_operation_example()

3.3 回调模式

通过回调函数处理错误。

lua
-- 回调模式
local function async_operation(data, success_callback, error_callback)
    -- 模拟异步操作
    if type(data) ~= "string" then
        if error_callback then
            error_callback("数据必须是字符串")
        end
        return
    end
    
    if #data == 0 then
        if error_callback then
            error_callback("数据不能为空")
        end
        return
    end
    
    -- 模拟处理延迟
    local result = string.upper(data)
    
    if success_callback then
        success_callback(result)
    end
end

-- 使用回调模式
async_operation("hello", 
    function(result)  -- 成功回调
        print("操作成功:", result)
    end,
    function(err)     -- 错误回调
        print("操作失败:", err)
    end
)

async_operation(123,
    function(result)
        print("操作成功:", result)
    end,
    function(err)
        print("操作失败:", err)
    end
)

4. 调试和诊断

4.1 debug库的使用

lua
-- debug库基本用法
local function debug_example()
    local function inner_function(x, y)
        print("调用栈信息:")
        print(debug.traceback())
        
        print("\n当前函数信息:")
        local info = debug.getinfo(1, "nSl")
        print("函数名:", info.name or "匿名")
        print("文件:", info.source)
        print("行号:", info.currentline)
        
        print("\n局部变量:")
        local i = 1
        while true do
            local name, value = debug.getlocal(1, i)
            if not name then break end
            print(string.format("  %s = %s", name, tostring(value)))
            i = i + 1
        end
        
        return x + y
    end
    
    return inner_function(10, 20)
end

debug_example()

-- 自定义调试函数
local function debug_print(...)
    local info = debug.getinfo(2, "Sl")
    local prefix = string.format("[DEBUG %s:%d]", info.source, info.currentline)
    print(prefix, ...)
end

local function test_debug_print()
    local x = 42
    debug_print("x的值是", x)
    
    local y = "hello"
    debug_print("y的值是", y)
end

test_debug_print()

4.2 错误日志系统

lua
-- 简单的错误日志系统
local Logger = {}
Logger.__index = Logger

function Logger.new(filename)
    local logger = {
        filename = filename,
        levels = {
            DEBUG = 1,
            INFO = 2,
            WARN = 3,
            ERROR = 4,
            FATAL = 5
        },
        current_level = 2  -- INFO
    }
    return setmetatable(logger, Logger)
end

function Logger:set_level(level)
    if type(level) == "string" then
        level = self.levels[level:upper()]
    end
    self.current_level = level or self.current_level
end

function Logger:log(level, message, ...)
    if type(level) == "string" then
        level = self.levels[level:upper()] or self.levels.INFO
    end
    
    if level < self.current_level then
        return
    end
    
    local level_names = {"DEBUG", "INFO", "WARN", "ERROR", "FATAL"}
    local level_name = level_names[level] or "UNKNOWN"
    
    local info = debug.getinfo(2, "Sl")
    local timestamp = os.date("%Y-%m-%d %H:%M:%S")
    local location = string.format("%s:%d", info.source or "unknown", info.currentline or 0)
    
    local formatted_message = string.format(message, ...)
    local log_entry = string.format("[%s] %s %s - %s", 
        timestamp, level_name, location, formatted_message)
    
    print(log_entry)
    
    -- 写入文件(如果指定了文件名)
    if self.filename then
        local file = io.open(self.filename, "a")
        if file then
            file:write(log_entry .. "\n")
            file:close()
        end
    end
end

function Logger:debug(message, ...)
    self:log("DEBUG", message, ...)
end

function Logger:info(message, ...)
    self:log("INFO", message, ...)
end

function Logger:warn(message, ...)
    self:log("WARN", message, ...)
end

function Logger:error(message, ...)
    self:log("ERROR", message, ...)
end

function Logger:fatal(message, ...)
    self:log("FATAL", message, ...)
end

-- 使用日志系统
local logger = Logger.new("app.log")
logger:set_level("DEBUG")

logger:debug("这是调试信息")
logger:info("应用程序启动")
logger:warn("这是警告信息")
logger:error("发生错误: %s", "文件未找到")
logger:fatal("严重错误,程序即将退出")

4.3 性能监控

lua
-- 性能监控工具
local Profiler = {}
Profiler.__index = Profiler

function Profiler.new()
    return setmetatable({
        timers = {},
        counters = {},
        memory_snapshots = {}
    }, Profiler)
end

function Profiler:start_timer(name)
    self.timers[name] = {
        start_time = os.clock(),
        total_time = self.timers[name] and self.timers[name].total_time or 0,
        call_count = self.timers[name] and self.timers[name].call_count or 0
    }
end

function Profiler:stop_timer(name)
    local timer = self.timers[name]
    if timer and timer.start_time then
        local elapsed = os.clock() - timer.start_time
        timer.total_time = timer.total_time + elapsed
        timer.call_count = timer.call_count + 1
        timer.start_time = nil
        timer.average_time = timer.total_time / timer.call_count
    end
end

function Profiler:increment_counter(name, value)
    value = value or 1
    self.counters[name] = (self.counters[name] or 0) + value
end

function Profiler:take_memory_snapshot(name)
    self.memory_snapshots[name] = collectgarbage("count")
end

function Profiler:get_memory_usage(start_snapshot, end_snapshot)
    local start_mem = self.memory_snapshots[start_snapshot] or 0
    local end_mem = self.memory_snapshots[end_snapshot] or collectgarbage("count")
    return end_mem - start_mem
end

function Profiler:report()
    print("=== 性能报告 ===")
    
    print("\n计时器:")
    for name, timer in pairs(self.timers) do
        print(string.format("  %s: 总时间=%.4fs, 调用次数=%d, 平均时间=%.4fs", 
            name, timer.total_time, timer.call_count, timer.average_time or 0))
    end
    
    print("\n计数器:")
    for name, count in pairs(self.counters) do
        print(string.format("  %s: %d", name, count))
    end
    
    print("\n内存快照:")
    for name, memory in pairs(self.memory_snapshots) do
        print(string.format("  %s: %.2f KB", name, memory))
    end
end

-- 使用性能监控
local profiler = Profiler.new()

-- 监控函数执行时间
local function monitored_function(n)
    profiler:start_timer("fibonacci")
    profiler:take_memory_snapshot("fib_start")
    
    local function fib(n)
        profiler:increment_counter("fib_calls")
        if n <= 1 then return n end
        return fib(n-1) + fib(n-2)
    end
    
    local result = fib(n)
    
    profiler:take_memory_snapshot("fib_end")
    profiler:stop_timer("fibonacci")
    
    return result
end

-- 测试
for i = 1, 5 do
    local result = monitored_function(10)
    print("fib(10) =", result)
end

profiler:report()
print("内存使用变化:", profiler:get_memory_usage("fib_start", "fib_end"), "KB")

5. 实际应用示例

5.1 文件操作错误处理

lua
-- 安全的文件操作
local FileUtils = {}

function FileUtils.safe_read_file(filename)
    local file, err = io.open(filename, "r")
    if not file then
        return nil, "无法打开文件: " .. err
    end
    
    local success, content = pcall(function()
        return file:read("*a")
    end)
    
    file:close()
    
    if success then
        return content, nil
    else
        return nil, "读取文件失败: " .. content
    end
end

function FileUtils.safe_write_file(filename, content)
    local file, err = io.open(filename, "w")
    if not file then
        return false, "无法创建文件: " .. err
    end
    
    local success, write_err = pcall(function()
        file:write(content)
    end)
    
    file:close()
    
    if success then
        return true, nil
    else
        return false, "写入文件失败: " .. write_err
    end
end

function FileUtils.copy_file(source, destination)
    local content, read_err = FileUtils.safe_read_file(source)
    if not content then
        return false, "复制失败 - " .. read_err
    end
    
    local success, write_err = FileUtils.safe_write_file(destination, content)
    if not success then
        return false, "复制失败 - " .. write_err
    end
    
    return true, nil
end

-- 使用文件工具
local content, err = FileUtils.safe_read_file("test.txt")
if content then
    print("文件内容长度:", #content)
else
    print("读取失败:", err)
end

local success, err = FileUtils.safe_write_file("output.txt", "Hello, World!")
if success then
    print("写入成功")
else
    print("写入失败:", err)
end

5.2 网络请求错误处理

lua
-- 模拟网络请求错误处理
local HttpClient = {}
HttpClient.__index = HttpClient

function HttpClient.new(timeout)
    return setmetatable({
        timeout = timeout or 30,
        retry_count = 3,
        retry_delay = 1
    }, HttpClient)
end

function HttpClient:request(url, options)
    options = options or {}
    local method = options.method or "GET"
    local headers = options.headers or {}
    local body = options.body
    
    for attempt = 1, self.retry_count do
        local success, response, err = self:_do_request(url, method, headers, body)
        
        if success then
            return response, nil
        end
        
        -- 记录重试
        print(string.format("请求失败 (尝试 %d/%d): %s", attempt, self.retry_count, err))
        
        if attempt < self.retry_count then
            -- 等待重试
            local start = os.clock()
            while os.clock() - start < self.retry_delay do end
        end
    end
    
    return nil, "请求失败,已重试 " .. self.retry_count .. " 次"
end

function HttpClient:_do_request(url, method, headers, body)
    -- 模拟网络请求
    local random_fail = math.random() < 0.7  -- 70%失败率用于测试
    
    if random_fail then
        local errors = {
            "连接超时",
            "DNS解析失败",
            "服务器返回500错误",
            "网络不可达"
        }
        return false, nil, errors[math.random(#errors)]
    end
    
    -- 模拟成功响应
    local response = {
        status = 200,
        headers = {["content-type"] = "application/json"},
        body = '{"message": "success"}'
    }
    
    return true, response, nil
end

-- 使用HTTP客户端
local client = HttpClient.new(10)
local response, err = client:request("https://api.example.com/users")

if response then
    print("请求成功:", response.status)
    print("响应体:", response.body)
else
    print("请求失败:", err)
end

5.3 数据验证框架

lua
-- 数据验证框架
local Validator = {}
Validator.__index = Validator

function Validator.new()
    return setmetatable({
        rules = {},
        errors = {}
    }, Validator)
end

function Validator:add_rule(field, rule_func, message)
    if not self.rules[field] then
        self.rules[field] = {}
    end
    
    table.insert(self.rules[field], {
        validate = rule_func,
        message = message
    })
    
    return self  -- 支持链式调用
end

function Validator:required(field, message)
    return self:add_rule(field, function(value)
        return value ~= nil and value ~= ""
    end, message or field .. " 是必填项")
end

function Validator:min_length(field, min_len, message)
    return self:add_rule(field, function(value)
        return type(value) == "string" and #value >= min_len
    end, message or field .. " 长度不能少于 " .. min_len .. " 个字符")
end

function Validator:max_length(field, max_len, message)
    return self:add_rule(field, function(value)
        return type(value) == "string" and #value <= max_len
    end, message or field .. " 长度不能超过 " .. max_len .. " 个字符")
end

function Validator:email(field, message)
    return self:add_rule(field, function(value)
        return type(value) == "string" and string.match(value, "^[%w%.%-_]+@[%w%.%-_]+%.%w+$") ~= nil
    end, message or field .. " 必须是有效的邮箱地址")
end

function Validator:numeric(field, message)
    return self:add_rule(field, function(value)
        return type(value) == "number" or (type(value) == "string" and tonumber(value) ~= nil)
    end, message or field .. " 必须是数字")
end

function Validator:range(field, min_val, max_val, message)
    return self:add_rule(field, function(value)
        local num = tonumber(value)
        return num and num >= min_val and num <= max_val
    end, message or string.format("%s 必须在 %s 到 %s 之间", field, min_val, max_val))
end

function Validator:validate(data)
    self.errors = {}
    
    for field, rules in pairs(self.rules) do
        local value = data[field]
        
        for _, rule in ipairs(rules) do
            local success, result = pcall(rule.validate, value)
            
            if not success then
                -- 验证函数本身出错
                if not self.errors[field] then
                    self.errors[field] = {}
                end
                table.insert(self.errors[field], "验证规则错误: " .. result)
            elseif not result then
                -- 验证失败
                if not self.errors[field] then
                    self.errors[field] = {}
                end
                table.insert(self.errors[field], rule.message)
            end
        end
    end
    
    return next(self.errors) == nil, self.errors
end

function Validator:get_error_messages()
    local messages = {}
    for field, field_errors in pairs(self.errors) do
        for _, error in ipairs(field_errors) do
            table.insert(messages, error)
        end
    end
    return messages
end

-- 使用验证框架
local function validate_user_registration(user_data)
    local validator = Validator.new()
    
    validator:required("username")
             :min_length("username", 3)
             :max_length("username", 20)
             :required("email")
             :email("email")
             :required("age")
             :numeric("age")
             :range("age", 13, 120)
             :required("password")
             :min_length("password", 8)
    
    local is_valid, errors = validator:validate(user_data)
    
    if is_valid then
        return true, "验证通过"
    else
        return false, validator:get_error_messages()
    end
end

-- 测试验证
local test_data = {
    username = "jo",  -- 太短
    email = "invalid-email",  -- 无效邮箱
    age = "25",  -- 字符串数字,应该可以通过
    password = "123"  -- 太短
}

local success, result = validate_user_registration(test_data)
if success then
    print("验证成功:", result)
else
    print("验证失败:")
    for _, error in ipairs(result) do
        print("  -", error)
    end
end

6. 最佳实践

6.1 错误处理原则

lua
-- 错误处理最佳实践

-- 1. 早期验证
local function process_user_input(input)
    -- 立即验证输入
    if not input then
        return nil, "输入不能为空"
    end
    
    if type(input) ~= "table" then
        return nil, "输入必须是表格"
    end
    
    -- 继续处理...
    return "处理成功"
end

-- 2. 明确的错误信息
local function connect_to_database(config)
    if not config then
        return nil, "数据库配置不能为空"
    end
    
    if not config.host then
        return nil, "数据库主机地址未配置"
    end
    
    if not config.port or config.port <= 0 or config.port > 65535 then
        return nil, "数据库端口配置无效,必须在1-65535之间"
    end
    
    -- 尝试连接...
    return "连接成功"
end

-- 3. 资源清理
local function safe_file_processing(filename)
    local file = nil
    local temp_file = nil
    
    local function cleanup()
        if file then
            file:close()
        end
        if temp_file then
            temp_file:close()
            os.remove("temp_" .. filename)
        end
    end
    
    local success, result = pcall(function()
        file = assert(io.open(filename, "r"))
        temp_file = assert(io.open("temp_" .. filename, "w"))
        
        -- 处理文件...
        local content = file:read("*a")
        temp_file:write(string.upper(content))
        
        return "处理完成"
    end)
    
    cleanup()
    
    if success then
        return result
    else
        return nil, "文件处理失败: " .. result
    end
end

-- 4. 错误传播
local function layered_error_handling()
    local function database_layer()
        return nil, "数据库连接失败"
    end
    
    local function service_layer()
        local result, err = database_layer()
        if not result then
            return nil, "服务层错误: " .. err
        end
        return result
    end
    
    local function controller_layer()
        local result, err = service_layer()
        if not result then
            return nil, "控制器错误: " .. err
        end
        return result
    end
    
    local result, err = controller_layer()
    print("最终结果:", result or err)
end

layered_error_handling()

6.2 错误恢复策略

lua
-- 错误恢复策略
local function retry_with_backoff(func, max_attempts, initial_delay)
    max_attempts = max_attempts or 3
    initial_delay = initial_delay or 1
    
    for attempt = 1, max_attempts do
        local success, result = pcall(func)
        
        if success then
            return result
        end
        
        print(string.format("尝试 %d/%d 失败: %s", attempt, max_attempts, result))
        
        if attempt < max_attempts then
            local delay = initial_delay * (2 ^ (attempt - 1))  -- 指数退避
            print(string.format("等待 %.1f 秒后重试...", delay))
            
            local start = os.clock()
            while os.clock() - start < delay do end
        end
    end
    
    error("所有重试都失败了")
end

-- 使用重试机制
local function unreliable_operation()
    if math.random() < 0.7 then  -- 70%失败率
        error("操作失败")
    end
    return "操作成功"
end

local success, result = pcall(retry_with_backoff, unreliable_operation, 5, 0.5)
print("最终结果:", success and result or "彻底失败")

总结

Lua的错误处理机制提供了多种处理错误的方式:

  1. error和assert - 主动抛出错误
  2. pcall和xpcall - 捕获和处理错误
  3. 返回值模式 - 通过返回值表示成功或失败
  4. debug库 - 提供调试和诊断信息
  5. 日志系统 - 记录和追踪错误

良好的错误处理应该:

  • 预防性 - 早期验证,防止错误发生
  • 描述性 - 提供清晰的错误信息
  • 恢复性 - 尽可能从错误中恢复
  • 可维护性 - 便于调试和维护

掌握这些错误处理技术可以让你的Lua程序更加健壮和可靠。

基于 MIT 许可发布