18.错误处理

18.错误处理


18.1 知识点

错误处理的基本概念

  • Lua 脚本运行时如果出现错误,默认会直接中断当前执行流程。
  • 基础阶段先掌握几个常用方法:
    • error
    • assert
    • pcall
    • xpcall
    • debug.traceback
  • 这篇只讲最小可用写法,目标是知道怎么主动抛错、怎么检查条件、怎么避免脚本报错后直接把主流程打断。

项目里写 Lua,最怕的是一个小脚本报错把后面的逻辑全断了。
所以错误处理不是为了“吞掉错误”,而是为了让错误有地方被接住,并且能打出足够的信息方便排查。

error主动抛出错误

  • error 用来主动抛出错误。
  • 语法:
error("错误信息")
  • 示例:
function TestError()
    error("这里主动抛出一个错误")
end

TestError()

print("这行不会执行")

运行到 error 后,当前流程会被中断。
所以上面最后一行 print 不会执行。

实际项目里,error 一般用在“不应该继续跑”的地方,比如参数非法、配置缺字段、状态不对。

function CreatePlayer(playerId)
    if playerId == nil then
        error("CreatePlayer失败,playerId不能为空")
    end

    print("创建角色:" .. playerId)
end

CreatePlayer(nil)

这里不要为了省事到处 error
普通业务失败更常见的是返回 false, reason,真正遇到不该继续的异常情况,再考虑抛错。

assert断言

  • assert 可以理解成“条件检查”。
  • 如果第一个参数为真,就继续执行。
  • 如果第一个参数为 falsenil,就抛出错误。
  • 语法:
assert(条件, "错误信息")
  • 示例:
local playerId = nil

assert(playerId ~= nil, "playerId不能为空")

print("这里不会执行")

assert 常用来检查函数参数:

function LoadConfig(configName)
    assert(configName ~= nil, "configName不能为空")

    print("加载配置:" .. configName)
end

LoadConfig("PlayerConfig")
LoadConfig(nil)

assert 比手写 if + error 更短,适合做一些基础前置检查。
但它不是万能校验系统,复杂的业务校验还是正常写 if / else 更清楚。

pcall保护调用

  • pcall 可以保护一次函数调用。
  • pcall 包起来的函数如果报错,不会直接中断外层主流程。
  • pcall 会返回两个主要结果:
    • 第一个返回值:是否执行成功。
    • 后面的返回值:成功时是函数返回值,失败时是错误信息。

pcall调用成功

function Add(a, b)
    return a + b
end

local isSuccess, result = pcall(Add, 1, 2)

print(isSuccess) -- true
print(result)    -- 3

pcall调用失败

function TestPCallError()
    error("TestPCallError里出错了")
end

local isSuccess, errorMessage = pcall(TestPCallError)

print(isSuccess)    -- false
print(errorMessage) -- TestPCallError里出错了

print("pcall接住错误后,外层流程还可以继续执行")

pcall 的价值在这里:
里面的函数炸了,外面还能继续跑。比如加载某个可选配置、执行某个脚本回调、跑一段活动逻辑时,可以先用 pcall 兜住,避免一个错误直接打断整条主流程。

不过也别把 pcall 当成万能保险。
它能接住错误,不代表错误就不用修。线上代码如果只是把错误吞掉,后面排查会更麻烦。

pcall传参和接收返回值

  • pcall 后面可以继续传参数。
  • 这些参数会传给被保护调用的函数。
  • 如果函数正常返回,pcall 后面的返回值就是函数本身的返回值。
function Divide(a, b)
    if b == 0 then
        error("除数不能为0")
    end

    return a / b
end

local isSuccess, result = pcall(Divide, 10, 2)

print(isSuccess) -- true
print(result)    -- 5

如果函数内部报错:

local isSuccess, result = pcall(Divide, 10, 0)

print(isSuccess) -- false
print(result)    -- 除数不能为0

这种写法很适合包一层危险逻辑。
比如外部配置不可信、脚本回调不确定、数据可能为空时,至少可以先保证主流程别直接崩掉。

xpcall自定义错误处理

  • xpcallpcall 类似,也可以保护函数调用。
  • 区别是:xpcall 可以额外传入一个错误处理函数。
  • 被保护的函数如果正常执行,错误处理函数不会执行。
  • 被保护的函数如果报错,才会进入错误处理函数。
  • 错误处理函数的参数,就是原始错误信息。
  • 错误处理函数的返回值,会作为 xpcall 失败时的第二个返回值。
  • 基础阶段最常见的用法,就是配合 debug.traceback 打出调用栈。

语法可以先这样记:

xpcall(要执行的函数, 错误处理函数)

xpcall调用失败

function TestXPCallError()
    error("TestXPCallError里出错了")
end

function ErrorHandler(errorMessage)
    print("捕获到错误:" .. tostring(errorMessage))
    return "处理后的错误:" .. tostring(errorMessage)
end

local isSuccess, result = xpcall(TestXPCallError, ErrorHandler)

print(isSuccess) -- false
print(result)    -- 处理后的错误:TestXPCallError里出错了

这个例子的执行顺序是:

1. xpcall 开始执行 TestXPCallError
2. TestXPCallError 内部调用 error 抛出错误
3. xpcall 捕获到错误,不让错误继续往外抛
4. xpcall 调用 ErrorHandler,并把原始错误信息传进去
5. ErrorHandler 里面的 print 会执行,所以会打印“捕获到错误:xxx”
6. ErrorHandler return 的内容,会变成 xpcall 的第二个返回值 result
7. xpcall 返回 false, result
8. 外层流程继续执行

所以这句:

print("捕获到错误:" .. tostring(errorMessage))

会不会打印,取决于 TestXPCallError 里面有没有报错。
如果被保护函数没有报错,ErrorHandler 根本不会执行。

xpcall调用成功

function TestXPCallSuccess()
    return "执行成功"
end

function ErrorHandler(errorMessage)
    print("捕获到错误:" .. tostring(errorMessage))
    return errorMessage
end

local isSuccess, result = xpcall(TestXPCallSuccess, ErrorHandler)

print(isSuccess) -- true
print(result)    -- 执行成功

这个例子里,TestXPCallSuccess 没有报错,所以 ErrorHandler 不会执行。
因此不会打印“捕获到错误:xxx”。

xpcall传参

Lua 5.2+ 里,xpcall 通常可以在错误处理函数后面继续传参:

function Divide(a, b)
    if b == 0 then
        error("除数不能为0")
    end

    return a / b
end

function ErrorHandler(errorMessage)
    print("捕获到错误:" .. tostring(errorMessage))
    return errorMessage
end

local isSuccess, result = xpcall(Divide, ErrorHandler, 10, 2)

print(isSuccess) -- true
print(result)    -- 5

如果参数会导致报错:

local isSuccess, result = xpcall(Divide, ErrorHandler, 10, 0)

print(isSuccess) -- false
print(result)    -- 除数不能为0

执行顺序还是一样:
先执行 Divide(10, 0),里面报错后,才会调用 ErrorHandler(errorMessage)

不过要注意版本差异。Unity Lua 热更项目里常见 Lua 5.1 / LuaJIT 环境,这类环境下 xpcall 不一定支持在后面直接传参数。更稳的写法是用匿名函数包一层:

local isSuccess, result = xpcall(function()
    return Divide(10, 0)
end, ErrorHandler)

print(isSuccess) -- false
print(result)    -- 除数不能为0

这种写法虽然多包了一层,但兼容性更好,也更直观:
xpcall 只负责执行这个匿名函数,真正的参数在匿名函数里面自己传。

这里 ErrorHandler 不是拿来修复错误的。
它更常见的作用是整理错误信息、打印日志、补充调用栈。

debug.traceback简单用法

  • debug.traceback 可以生成调用栈信息。
  • 基础阶段只用它配合 xpcall 看报错位置即可。
  • 先不展开 debug.getinfo / debug.getupvalue / debug.sethook,这些放到 Lua 进阶知识里再整理。
function A()
    B()
end

function B()
    C()
end

function C()
    error("C函数里出错了")
end

function ErrorHandler(errorMessage)
    return debug.traceback(errorMessage)
end

local isSuccess, errorMessage = xpcall(A, ErrorHandler)

print(isSuccess)
print(errorMessage)

大概会打印类似这样的内容:

false
...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:132: C函数里出错了
stack traceback:
        ...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:138: in function <...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:135>
        [C]: in function 'error'
        ...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:132: in function 'C'
        ...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:128: in function 'B'
        ...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:124: in function <...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:123>
        [C]: in function 'xpcall'
        ...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:141: in main chunk
        [C]: ?

具体输出格式不同 Lua 版本、不同宿主环境里可能不完全一样。
实际项目里更重要的是:能看到错误信息和大概调用路径,方便定位是哪一层调用炸了。

debug.traceback 一般放在 xpcall 的错误处理函数里用。
这个时机比较合适,因为错误刚被捕获,调用栈信息还比较完整。

function ErrorHandler(errorMessage)
    local traceback = debug.traceback(errorMessage)
    print(traceback)
    return traceback
end

如果在错误处理函数里已经 print(traceback),外层又 print(result),就会打印两次。
实际项目里一般二选一,不要重复打太多日志。

pcall和xpcall怎么选

  • 只是想保护一段函数调用,不让错误直接中断外层流程,用 pcall 就够了。
  • 想在报错时统一加工错误信息,或者顺手带上调用栈,用 xpcall 更合适。
  • 简单脚本里不用到处包,关键入口、外部脚本回调、配置加载这类不稳定位置再包。
-- 简单保护
local ok, result = pcall(SomeFunction)

-- 想带调用栈
local ok, err = xpcall(SomeFunction, function(errorMessage)
    return debug.traceback(errorMessage)
end)

这里要注意:错误处理不是为了把错误藏起来。
更好的习惯是把错误接住、打清楚,然后决定当前流程是否还能继续。

总结

  • error:主动抛出错误。
  • assert:条件不成立时抛出错误,适合做简单参数检查。
  • pcall:保护函数调用,防止错误直接中断外层流程。
  • xpcall:比 pcall 多一个错误处理函数。
  • ErrorHandler:只在被保护函数报错时执行。
  • ErrorHandler 的返回值:会变成 xpcall 失败时的第二个返回值。
  • xpcall 传参:新版本 Lua 可以直接传,Lua 5.1 / LuaJIT 项目更稳的是匿名函数包一层。
  • debug.traceback:生成调用栈信息,基础阶段常和 xpcall 搭配使用。
  • debug.getinfo / debug.getupvalue / debug.sethook / profiling 不放在基础篇,后面放到 Lua 进阶知识的错误与调试里整理。

18.2 知识点代码

Lesson18_错误处理.lua

print("**********错误处理************")

print("**********知识点一 error主动抛出错误************")

-- error 用来主动抛出错误
-- 运行到 error 后,当前执行流程会被中断

-- function TestError()
-- 	error("这里主动抛出一个错误")
-- end

-- TestError()

-- print("这行不会执行")


print("**********知识点二 assert断言************")

-- assert 可以理解成条件检查
-- 如果第一个参数为真,就继续执行
-- 如果第一个参数为 false 或 nil,就抛出错误

function LoadConfig(configName)
    assert(configName ~= nil, "configName不能为空")

    print("加载配置:" .. configName)
end

LoadConfig("PlayerConfig")

-- LoadConfig(nil) -- 会报错:configName不能为空


print("**********知识点三 pcall保护调用************")

function Add(a, b)
    return a + b
end

local isSuccess, result = pcall(Add, 1, 2)

print(isSuccess) -- true
print(result)    -- 3


function TestPCallError()
    error("TestPCallError里出错了")
end

local isSuccess, errorMessage = pcall(TestPCallError)

print(isSuccess)    -- false
print(errorMessage) -- TestPCallError里出错了

print("pcall接住错误后,外层流程还可以继续执行")


print("**********知识点四 pcall传参和接收返回值************")

function Divide(a, b)
    if b == 0 then
        error("除数不能为0")
    end

    return a / b
end

local isSuccess, result = pcall(Divide, 10, 2)

print(isSuccess) -- true
print(result)    -- 5

local isSuccess, result = pcall(Divide, 10, 0)

print(isSuccess) -- false
print(result)    -- 除数不能为0


print("**********知识点五 xpcall自定义错误处理************")

function TestXPCallError()
    error("TestXPCallError里出错了")
end

function ErrorHandler(errorMessage)
    print("捕获到错误:" .. tostring(errorMessage))
    return "处理后的错误:" .. tostring(errorMessage)
end

local isSuccess, result = xpcall(TestXPCallError, ErrorHandler)

print(isSuccess) -- false
print(result)    -- 处理后的错误:TestXPCallError里出错了


function TestXPCallSuccess()
    return "执行成功"
end

local isSuccess, result = xpcall(TestXPCallSuccess, ErrorHandler)

print(isSuccess) -- true
print(result)    -- 执行成功
-- 这次不会打印“捕获到错误”,因为 TestXPCallSuccess 没有报错


print("**********知识点六 xpcall传参************")

-- Lua 5.2+ / Lua 5.4 可以在 xpcall 后面直接传参数
-- 但 Unity Lua 热更项目里常见 Lua 5.1 / LuaJIT
-- 为了兼容这类环境,更稳的是用匿名函数包一层

local isSuccess, result = xpcall(function()
    return Divide(10, 0)
end, ErrorHandler)

print(isSuccess) -- false
print(result)    -- 处理后的错误:除数不能为0


print("**********知识点七 debug.traceback简单用法************")

function A()
    B()
end

function B()
    C()
end

function C()
    error("C函数里出错了")
end

function TracebackHandler(errorMessage)
    -- debug.traceback 可以生成调用栈信息
    -- 这里先只用来辅助查看错误位置
    return debug.traceback(errorMessage)
end

local isSuccess, errorMessage = xpcall(A, TracebackHandler)

print(isSuccess)    -- false
print(errorMessage) -- 会带上错误信息和调用栈


print("**********知识点八 pcall和xpcall怎么选************")

-- 只是想保护一段函数调用,不让错误直接中断外层流程,用 pcall 就够了
-- 想在报错时统一加工错误信息,或者顺手带上调用栈,用 xpcall 更合适
-- 错误处理不是为了把错误藏起来
-- 更好的习惯是把错误接住、打清楚,然后决定当前流程是否还能继续

local ok1, ret1 = pcall(Add, 10, 20)
print(ok1, ret1) -- true	30

local ok2, ret2 = xpcall(function()
    return Divide(10, 0)
end, function(errorMessage)
    return debug.traceback(errorMessage)
end)

print(ok2)
print(ret2)


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏