OpenResty 中的真值與假值與坑

先重溫下 Lua 裏的真值與假值:除了 nil 和 false 爲假,其餘值都是真。「其餘值」這個概念包括0、空字符串、空表,等等。
在 Lua 裏,一般使用 andor 做爲邏輯操做符。好比 true and false 返回 false,而 false or true 返回 trueredis

OK,複習到此結束,讓咱們看下這幾條規則衍生出來的各類坑。編程

第一個坑

在 Lua 代碼裏,做爲給參數設置默認值的慣用法,咱們一般能看到 xx = xx or value 的語句。若是沒有給 xx 入參指定值,它的取值爲 nil,該語句就會賦值 value 給它。函數

這裏是今天咱們遇到的第一個坑。前面說了,ui

除了 nil 和 false 爲假

除了 nil 還有 false 呢!lua

若是寫代碼的時候,把前面設置默認值的語句順手複製一份;抑或因爲業務變更,原來的入參變成布爾類型,也許一會兒就掉到這個坑裏了。因此這種慣用法,雖然便利了書寫,可是也得注意一下,多留點心。指針

第二個坑

看到前面的第一個坑,有些小夥伴可能想到一個跳過坑的辦法:改爲 xx = (xx == nil) and xx or value。實際跑下會發現,這個語句也跑不過 xx 爲 false 的 case。這就是第二個坑了。rest

Lua 沒有三元操做符!
Lua 沒有三元操做符!
Lua 沒有三元操做符!code

重要的東西說三遍!雖然你可能在 Lua 代碼中見過各類三元操做符的模擬,可是它們都是模擬。既然是模擬,也不過是贗品,只不過有些是以假亂真的高仿品。接口

a and b or c 模式是這些高仿品中的一員。這個模式其實包含兩個表達式:先 a and b 獲得 x,再執行 x or c 獲得最終結果 y。在大多數時候,它表現得像是三元操做符。但惋惜它不是。字符串

若是 b 的值爲假,那麼 a and b 的執行結果恆假;若是 x 恆假,則 x or c 的執行結果恆爲 c。因此只要 b 的值爲假,那麼最終結果恆爲 c。

上面例子裏面,xx 是一個傳進來的變量,因此你只需跑下 case,就能看出這是一個坑。但若是 b 的位置上是一個函數的返回值呢?例如 expr and func1() or func2() 的形式,若是 func1 只是偶爾返回假值,一顆定時炸彈就埋下了。
仍是像第一個坑同樣的結論,慣用法能夠用,可是要多留點心。

若是本文到此結束,它的名字應該是 Lua 中的真值與假值與坑。但實際標題是 OpenResty 中的真值與假值與坑,因此下面講講 OpenResty 專屬的坑。

第三個坑

因爲 Lua 裏面 nil 不能做爲佔位符,爲了表示數據空缺,好比 redis 鍵對應值爲空,OpenResty 引入了 ngx.null 這個常量。ngx.null 是一個值爲 NULL 的 userdata。

$ resty -e 'print(tostring(ngx.null))'
userdata: NULL

ngx.null 雖然帶了個 null 字,可是它並不等於 nil。 根據開頭的規則(其餘值都是真),ngx.null 的布爾值爲真。 一個布爾值爲真的,表示空的常量,說實話,我還沒在其餘編程環境中見過。這又是一個潛在的坑。由於在思考的時候,一不當心就會把它看成假值考慮了。舉個例子,從 redis 獲取特定鍵,若是不存在,調用函數A。若是一時半會想不起 ngx.null 的特殊性,可能會直接判斷返回值是否爲真(或者是否爲 nil),而後就掉到坑裏了。尤爲是若是底層邏輯沒有把 ngx.null 包裝好,上層調用的人也許壓根沒想到除了 nil 和 value 以外,還有一個 ngx.null 的存在!

local res, err = redis.get('key1')
if not res then
    ...
end

-- 大部分狀況都是好的,直到有一天 key1 不存在…… 500 Internal Server Error!
-- res = res + 1
-- 正確作法
if res ~= ngx.null then
    res = res + 1

因此這種時候就須要給在底層跟外部數據服務打交道的代碼攔上一道崗,確保妥善處理好 ngx.null 。至於具體怎麼處理,是把 ngx.null 轉換成 nil 呢,仍是改爲業務相關的默認值,這就看具體的業務邏輯了。

第四個坑

到目前爲止,咱們已經見識了 ngx.null 這個特立獨行的空值了。OpenResty 裏還有另一個空值,來源於 LuaJIT FFI 的 cdata:NULL。正如 ngx.null 是 userdata 範疇內的 NULL,cdata:NULL 是 cdata 範疇內的 NULL。當你經過一個 FFI 接口調用 C 函數,而這個函數返回一個 NULL 指針,在 Lua 代碼看來,它收到的是一個 cdata:NULL 值。你可能會想固然地認爲,這時候返回的值應該是 nil,由於 Lua 裏面的 nil 對應的,不正是 C 裏面的 NULL 嘛。但天不遂人意,這時候返回的倒是 cdata:NULL,一個怪胎。

爲何說它是個怪胎呢?由於它跟 nil 相等,而 ngx.null 就不等於 nil。可是,跟 nil 相等並不意味着它可替換 nil。cdata:NULL 依然服從開頭提到的規則 —— 意味着它的布爾值爲真。又一個布爾值爲真的,表示空的常量!並且此次更詭異了,這個空常量跟 nil 是相等的!

各位能夠看下示例代碼,體會一下:

#!/usr/bin/env luajit
local ffi = require "ffi"

local cdata_null = ffi.new("void*", nil)
print(tostring(cdata_null))
if cdata_null == nil then
    print('cdata:NULL is equal to nil')
end
if cdata_null then
    print('...but it is not nil!')
end

怎麼處理呢?大多數狀況下,只要判斷 FFI 調用返回的是否是 cdata:NULL 就夠了。咱們能夠利用 cdata:NULLnil 相等這一點:

#!/usr/bin/env luajit
local ffi = require "ffi"

local cdata_null = ffi.new("void*", nil)
if cdata_null == nil then
    print('cdata:NULL found')
end

跟上一個坑同樣的,應該在底層跟 C 接口打交道的時候消除掉 cdata:NULL,不要讓它擴散出去,「禍害」代碼的其餘部分。

相關文章
相關標籤/搜索