最终异常 |
|
assert
和 pcall
函数之间。为了更清晰地展示,我们坚持使用 Lua 函数返回值的便捷标准(你可能已经使用过),并定义了两个非常简单的辅助函数(可以在 C 或 Lua 中定义)。
大多数 Lua 函数在发生错误时返回 nil
,并附带描述错误的消息。如果你没有使用这种约定,你可能是有充分理由的。希望在阅读完本文后,你会意识到你的理由并不充分。
如果你像我一样,你讨厌错误检查。大多数看起来很漂亮的代码片段,在你第一次编写时看起来很漂亮,但在你添加了所有错误检查代码后,它们就失去了部分魅力。然而,错误检查与代码的其他部分一样重要。多么令人沮丧。
即使你坚持使用返回值约定,任何涉及多个函数调用的复杂任务都会使错误检查既无聊又容易出错(你看到下面的 错误 了吗?)。
function task(arg1, arg2, ...) local ret1, err = task1(arg1) if not ret1 then cleanup1() return nil, err end local ret2, err = task2(arg2) if not ret then cleanup2() return nil, err end ... end
标准的 assert
函数提供了一个有趣的替代方案。要使用它,只需将每个要进行错误检查的函数调用嵌套在一个 assert
调用中。assert
函数检查其第一个参数的值。如果它是 nil
,assert
会将第二个参数作为错误消息抛出。否则,assert
会将所有参数直接传递,就像它不存在一样。这个想法极大地简化了错误检查。
function task(arg1, arg2, ...) local ret1 = assert(task1(arg1)) local ret2 = assert(task2(arg2)) ... end
如果任何任务失败,assert
会中止执行,并将错误消息显示给用户作为问题的原因。如果没有任何错误发生,任务会像以前一样完成。没有一个 if
语句,这很棒。然而,这个想法也有一些问题。
首先,最顶层的 task
函数不遵守底层任务遵循的协议:它会抛出一个错误,而不是返回 nil
和错误消息。这就是标准的 pcall
派上用场的地方。
function xtask(arg1, arg2, ...) local ret1 = assert(task1(arg1)) local ret2 = assert(task2(arg2)) ... end function task(arg1, arg2, ...) local ok, ret_or_err = pcall(xtask, arg1, arg2, ...) if ok then return ret_or_err else return nil, ret_or_err end end
我们新的 task
函数表现良好。Pcall
会捕获 assert
调用抛出的任何错误,并在状态码之后返回它。这样,错误就不会传播到高层 task
函数的用户。
这些是我们异常方案的主要想法,但仍有一些小问题需要解决。
pcall
破坏了代码的简洁性;
Assert
在引发错误之前会修改错误消息(它添加了行号信息)。
幸运的是,所有这些问题都很容易解决,这就是我们在接下来的部分中要做的。
protect
工厂
我们使用pcall
函数来保护用户免受底层实现可能引发的错误。我们更喜欢一个执行相同工作的工厂,而不是每次都直接使用pcall
(从而重复代码)。
local function pack(ok, ...) return ok, arg end function protect(f) return function(...) local ok, ret = pack(pcall(f, unpack(arg))) if ok then return unpack(ret) else return nil, ret[1] end end end
protect
工厂接收一个可能引发异常的函数,并返回一个符合我们返回值约定的函数。现在我们可以用更简洁的方式重写顶层的task
函数。
task = protect(function(arg1, arg2, ...) local ret1 = assert(task1(arg1)) local ret2 = assert(task2(arg2)) ... end)
protect
工厂的 Lua 实现存在创建表来保存多个参数和返回值的问题。在 C 中实现相同的函数是可能的(也很容易),并且不需要创建任何表。
static int safecall(lua_State *L) { lua_pushvalue(L, lua_upvalueindex(1)); lua_insert(L, 1); if (lua_pcall(L, lua_gettop(L) - 1, LUA_MULTRET, 0) != 0) { lua_pushnil(L); lua_insert(L, 1); return 2; } else return lua_gettop(L); } static int protect(lua_State *L) { lua_pushcclosure(L, safecall, 1); return 1; }
从 Lua 5.1 开始,在纯 Lua 代码中也可以避免使用临时表。
do local function fix_return_values(ok, ...) if ok then return ... else return nil, (...) end end function protect(f) return function(...) return fix_return_values(pcall(f, ...)) end end end
newtry
工厂让我们用一次性解决剩下的两个问题,并用一个具体的例子来说明提出的解决方案。假设你想写一个函数来下载一个 HTTP 文档。你必须连接、发送请求并读取回复。这些任务中的任何一个都可能失败,但如果在连接后出现问题,你必须在返回错误消息之前关闭连接。
get = protect(function(host, path) local c -- create a try function with a finalizer to close the socket local try = newtry(function() if c then c:close() end end) -- connect and send request c = try(connect(host, 80)) try(c:send("GET " .. path .. " HTTP/1.0\r\n\r\n")) -- get headers local h = {} while 1 do l = try(c:receive()) if l == "" then break end table.insert(h, l) end -- get body local b = try(c:receive("*a")) c:close() return b, h end)
newtry
工厂返回一个与assert
功能相同的函数。区别在于try
函数不会修改错误消息,并且在引发错误之前会调用一个可选的终结器。在我们的例子中,终结器只是关闭套接字。
即使像这样简单的例子,我们也看到了终结异常简化了我们的生活。让我们看看我们一般获得了什么,而不仅仅是在这个例子中。
尝试编写相同的函数,但不使用我们上面使用的技巧,你会发现代码变得很丑。更长的操作序列加上错误检查会变得更丑。所以让我们在 Lua 中实现newtry
函数。
function newtry(f) return function(...) if not arg[1] then if f then f() end error(arg[2], 0) else return unpack(arg) end end end
再次,实现会因为每次函数调用都创建表格而受到影响,所以我们更喜欢 C 版本。
static int finalize(lua_State *L) { if (!lua_toboolean(L, 1)) { lua_pushvalue(L, lua_upvalueindex(1)); lua_pcall(L, 0, 0, 0); lua_settop(L, 2); lua_error(L); return 0; } else return lua_gettop(L); } static int do_nothing(lua_State *L) { (void) L; return 0; } static int newtry(lua_State *L) { lua_settop(L, 1); if (lua_isnil(L, 1)) lua_pushcfunction(L, do_nothing); lua_pushcclosure(L, finalize, 1); return 1; }
再次,从 Lua 5.1 开始,在纯 Lua 代码中可以实现高效的无临时表格的实现。
function newtry(f) return function(ok, ...) if ok then return ok, ... else if f then f() end error((...), 0) end end end
protect
和newtry
函数在LuaSocket
[1]的实现中节省了大量工作。一些模块的大小通过这些想法减半。确实,这个方案不像 C++ 或 Java 等编程语言的异常机制那样通用,但其功能/简单性比率是有利的,我希望它能像它为LuaSocket
服务一样为你服务。