18.错误处理
18.1 知识点
错误处理的基本概念
- Lua 脚本运行时如果出现错误,默认会直接中断当前执行流程。
- 常用方法:
errorassertpcallxpcalldebug.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相当于“条件检查”。- 如果第一个参数为真,就继续执行。
- 如果第一个参数为
false或nil,就抛出错误。 - 语法:
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自定义错误处理
xpcall和pcall类似,也可以保护函数调用。- 区别是:
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+ 可在错误处理函数后继续传参:
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。
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
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不放在基础篇。
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