OpenResty 中的真值与假值与坑

先重温下 Lua 里的真值与假值:除了 nil 和 false 为假,其他值都是真。“其他值”这个概念包括0、空字符串、空表,等等。
在 Lua 里,通常使用 andor 作为逻辑操作符。比如 true and false 返回 false,而 false or true 返回 true

OK,复习到此结束,让我们看下这几条规则衍生出来的各种坑。

第一个坑

在 Lua 代码里,作为给参数设置默认值的惯用法,我们通常能看到 xx = xx or value 的语句。如果没有给 xx 入参指定值,它的取值为 nil,该语句就会赋值 value 给它。

这里是今天我们遇到的第一个坑。前面说了,

除了 nil 和 false 为假

除了 nil 还有 false 呢!

如果写代码的时候,把前面设置默认值的语句顺手复制一份;抑或由于业务变动,原来的入参变成布尔类型,也许一下子就掉到这个坑里了。所以这种惯用法,虽然便利了书写,但是也得注意一下,多留点心。

第二个坑

看到前面的第一个坑,有些小伙伴可能想到一个跳过坑的办法:改成 xx = (xx == nil) and xx or value。实际跑下会发现,这个语句也跑不过 xx 为 false 的 case。这就是第二个坑了。

Lua 没有三元操作符!
Lua 没有三元操作符!
Lua 没有三元操作符!

重要的东西说三遍!虽然你可能在 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,不要让它扩散出去,“祸害”代码的其他部分。

相关推荐