最终异常

lua-users home
wiki

使用最终异常

或如何摆脱所有这些 if 语句

作者:DiegoNehab


摘要

这份简短的 LTN 描述了一种简单的异常方案,它极大地简化了 Lua 程序中的错误检查。所有需要的功能都包含在 Lua 的标准库中,但隐藏在 assertpcall 函数之间。为了更清晰地展示,我们坚持使用 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 函数检查其第一个参数的值。如果它是 nilassert 会将第二个参数作为错误消息抛出。否则,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 函数的用户。

这些是我们异常方案的主要想法,但仍有一些小问题需要解决。

幸运的是,所有这些问题都很容易解决,这就是我们在接下来的部分中要做的。

介绍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

最终考虑因素

protectnewtry函数在LuaSocket[1]的实现中节省了大量工作。一些模块的大小通过这些想法减半。确实,这个方案不像 C++ 或 Java 等编程语言的异常机制那样通用,但其功能/简单性比率是有利的,我希望它能像它为LuaSocket服务一样为你服务。


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2013 年 10 月 7 日凌晨 2:42 GMT (差异)