已完成的例外

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函式不會混淆錯誤訊息,而且它會在引發錯誤之前呼叫選擇性的完成函式。在我們範例中,完成函式會簡單關閉 socket。

即使使用像這樣的簡單範例,我們也可以看出最後化的例外簡化了我們的生活。讓我們來看看我們在一般情況下可以獲得什麼,而不仅仅是這個範例

試著不要使用我們在上面使用的技巧來寫相同的函式,你會發現那個程式碼很醜陋。使用錯誤檢查的更長的運算順序會變得更醜陋。因此我們在 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 (diff)