在表中存储空值 |
|
nil
还是对应的键不存在于表格中。 t = {[k] = nil}
等同于 t = {}
,并且当 k
不是表格键时,t[k]
评估为 nil
。事实上,您可以将 {}
视为一个所有可能的键都设置为 nil
的表格,并且这仍然只占用一小部分有限的内存,因为所有这些键具有 nil
值并没有被显式存储。此外,尝试将表格键设置为 nil
,例如 {[nil] = true}
,会引发运行时错误。这与其他各种常见语言不同。[1]
有时我们会发现自己处于一种我们确实想要区分表值为 nil
与未定义的表值的情况。考虑以下函数,它通过在表格中存储和操作参数来反转其参数
function reverse(...) local t = {...} for i = 1,#t/2 do local j = #t - i + 1 t[i],t[j] = t[j],t[i] -- swap end return unpack(t) end
这通常有效,但不一定在其中一个参数为 nil
时有效
print(reverse(10,20,30)) --> 30 20 10 print(reverse(10,nil,20,nil)) --> 10
我们可以在这里使用的解决方案是将表格长度存储为表格中的键 n
。事实上,这是 Lua 5.1 之前的版本实现数组长度的方式
function reverse(...) local t = {n=select('#', ...), ...} for i = 1,t.n/2 do local j = t.n - i + 1 t[i],t[j] = t[j],t[i] -- swap end return unpack(t, 1, t.n) end
(RiciLake 指出,将 ...
额外复制到函数参数列表中以确定其长度会涉及不必要的开销。事实上,最好避免构建表格以执行此操作的开销,但 ...
中的数据很难操作,除非复制到表格中,尽管有一些稍微笨拙的模式可以避免表格——请参阅 VarargTheSecondClassCitizen 中的“Vararg 保存”。)
nil
占位符
上述方法在表格用作包含 nil
的数组的特殊情况下有效。在一般情况下,表格可能包含任意(不一定是数字)值。以下示例通过将元素存储为 Lua 表格中的键来表达数学集合。但是,由于表格键不能为 nil
,因此这给在集合中存储 nil
带来了挑战。解决方案是创建一个占位符对象 NIL
,它存储在表格中 nil
的位置
do local NIL = {} function set_create(...) local t = {} for n=1,select('#', ...) do local v = select(n, ...) t[v == nil and NIL or v] = true end return t end function set_exists(t, v) return t[v == nil and NIL or v] end end
local t = set_create(10, nil, false) assert(set_exists(t, 10)) assert(set_exists(t, nil)) assert(set_exists(t, false)) assert(not set_exists(t, "hello"))
使用NIL
作为占位符,冲突的可能性很小。NIL
作为一张表,是一个对象,而对象具有唯一的标识。表NIL
在do
块中是词法作用域的,在程序中的其他地方都不可见——除了,嗯,在表中。用户可以从表中获取NIL
并尝试将其添加到另一个集合中,在这种情况下,NIL
将被视为nil
而不是NIL
,这可能是用户想要的。
除了使用NIL
之外,还有其他一些替代方法,例如使用某个特定表域中唯一的其他值(可能是false
或表本身),但这在一般情况下行不通。您也可以使用第二个表来枚举定义的键。
local t = {red = 1, green = 2} local is_key = {"red", "green", "blue"} for _,k in ipairs(is_key) do print(k, t[k]) end
需要注意的是,nil
不是唯一不能存储在表中的值。由0/0
定义的“非数字”值(NAN
)不能作为键存储(但可以作为值存储)。NAN
作为表键存在一些潜在问题:LuaList:2005-11/msg00214.html。可以通过类似的方式解决此限制。但是,请注意,NAN
是唯一不等于自身的(NAN ~= NAN
)的值,这是测试NAN
的方法。
UNDEF
和nil
一种可能的解决方案是在给定表上定义一个元表,以便如果键存在且值为nil
,则表返回nil
,但如果键不存在,则返回一个新的唯一值UNDEF = {
}。但是,这有一些相当严重的问题。首先,UNDEF
在逻辑上等于true
,因此我们不能使用if t[k] then ... end
这种习惯用法,因为它将在k
在表中未定义的情况下执行分支。更重要的是,程序员可能会尝试将这些UNDEF
值存储在表中,从而导致类似的问题,即UNDEF
不能存储在表中。
该方法在下面通过使表成为另一个私有表的代理来实现,该私有表维护nil
与UNDEF
信息,并且__newindex
元函数记录了nil
的设置。它部分基于Programming In Lua, 2nd ed., 13.4 中的“带有默认值的表”示例。
-- NiledTable.lua local M = {} -- weak table for representing nils. local nils = setmetatable({}, {__mode = 'k'}) -- undefined value local UNDEF = setmetatable({}, {__tostring = function() return "undef" end}) M.UNDEF = UNDEF -- metatable for NiledTable's. local mt = {} function mt.__index(t,k) local n = nils[t] return not (n and n[k]) and UNDEF or nil end function mt.__newindex(t,k,v) if v == nil then local u = nils[t] if not u then u = {} nils[t] = u end u[k] = true else rawset(t,k,v) end end -- constructor setmetatable(M, {__call = function(class, t) return setmetatable(t, mt) end}) local function exipairs_iter(t, i) i = i + 1 local v = t[i] if v ~= UNDEF then return i, v end end -- ipairs replacement that handles nil values in tables. function M.exipairs(t, i) return exipairs_iter, t, 0 end -- next replacement that handles nil values in tables function M.exnext(t, k) if k == nil or rawget(t,k) ~= nil then k = next(t,k) if k == nil then t = nils[t] if t then k = next(t) end end else t = nils[t] k = next(t, k) end return k end local exnext = M.exnext -- pairs replacement that handles nil values in tables. function M.expairs(t, i) return exnext, t, nil end -- Undefine key in table. This is used since t[k] = UNDEF doesn't work -- as is. function M.undefine(t, k) rawset(t, k, nil) end return M
示例/测试
-- test_nil.lua - test of NiledTable.lua local NiledTable = require "NiledTable" local UNDEF = NiledTable.UNDEF local exipairs = NiledTable.exipairs local expairs = NiledTable.expairs local exnext = NiledTable.exnext local t = NiledTable { } t[1] = 3 t[2] = nil t.x = 4 t.y = nil assert(t[1] == 3) assert(t[2] == nil) assert(t[3] == UNDEF) assert(t.x == 4) assert(t.y == nil) assert(t.z == UNDEF) -- UNDEF is true. This is possible undesirable since -- "if t[3] then ... end" syntax cannot be used as before. assert(UNDEF) -- nils don't count. __len cannot be overriden in 5.1 without special -- userdata tricks. assert(#t == 1) -- constructor syntax doesn't work. The construction is done -- before the metatable is set, so the nils are discarded before -- NiledTable can see them. local t2 = NiledTable {nil, nil} assert(t2[1] == UNDEF) -- nils don't work with standard iterators local s = ""; local n=0 for k,v in pairs(t) do print("pairs:", k, v); n=n+1 end assert(n == 2) for i,v in ipairs(t) do print("ipairs:", i, v); n=n+1 end assert(n == 3) -- replacement iterators that do work for i,v in exipairs(t) do print("exipairs:", i, v); n=n+1 end assert(n == 5) for k,v in expairs(t) do print("expairs:", k, v); n=n+1 end assert(n == 9) for k,v in exnext, t do print("next:", k, v); n=n+1 end assert(n == 13) -- This does not do what you might expect. The __newindex -- metamethod is not called. We might resolve that by making -- the table be a proxy table to allow __newindex to handle this. t[1] = UNDEF assert(t[1] == UNDEF) for k,v in expairs(t) do print("expairs2:", k, v); n=n+1 end assert(n == 17) --opps -- Alternative undefine(t, 1) for k,v in expairs(t) do print("expairs3:", k, v); n=n+1 end assert(n == 20) --ok -- Now that we can store nil's in tables, we might now ask -- whether it's possible to store UNDEF in tables. That's -- probably not a good idea, and I don't know why you would -- even want to do that. It leads to a similar problem. -- -- Here is why: any value in Lua can potentially be used as input or -- output to a function, and any function input or output can potentially -- be captured as a Lua list, and Lua lists are implemented with tables... print "done"
上述黑客中的问题可以通过避免在模块外部暴露任何新值来解决。相反,t[k]
将在 k
不在表中或对应值是 nil
时都返回 nil
。我们将使用一个新的函数 exists(t,k)
来区分这两种情况,该函数返回一个布尔值,指示键 k
是否存在于表 t
中。(这种行为类似于 Perl 语言。)
通过这种方式,我们仍然可以在适当的时候使用 if t[k] then ... end
这种习惯用法,或者使用新的习惯用法 if exists(t, k) then ... end
。此外,没有向语言中引入新的值,从而避免了上述“在表中存储 UNDEF
”的问题。
-- NiledTable.lua local M = {} -- weak table for representing proxied storage tables. local data = setmetatable({}, {__mode = 'k'}) -- nil placeholder. -- Note: this value is not exposed outside this module, so -- there's typically no possibility that a user could attempt -- to store a "nil placeholder" in a table, leading to the -- same problem as storing nils in tables. local NIL = {__tostring = function() return "NIL" end} setmetatable(NIL, NIL) -- metatable for NiledTable's. local mt = {} function mt.__index(t,k) local d = data[t] local v = d and d[k] if v == NIL then v = nil end return v end function mt.__newindex(t,k,v) if v == nil then v = NIL end local d = data[t] if not d then d = {} data[t] = d end d[k] = v end function mt.__len(t) -- note: ignored by Lua but used by exlen below local d = data[t] return d and #d or 0 end -- constructor setmetatable(M, {__call = function(class, t) return setmetatable(t, mt) end}) function M.exists(t, k) local d = data[t] return (d and d[k]) ~= nil end local exists = M.exists function M.exlen(t) local mt = getmetatable(t) local len = mt.__len return len and len(t) or #t end local function exipairs_iter(t, i) i = i + 1 if exists(t, i) then local v = t[i] return i, v end end -- ipairs replacement that handles nil values in tables. function M.exipairs(t, i) return exipairs_iter, t, 0 end -- next replacement that handles nil values in tables function M.exnext(t, k) local d = data[t] if not d then return end k = next(d, k) return k end local exnext = M.exnext -- pairs replacement that handles nil values in tables. function M.expairs(t, i) return exnext, t, nil end -- Remove key in table. This is used since there is no -- value v such that t[k] = v will remove k from the table. function M.delete(t, k) local d = data[t] if d then d[k] = nil end end -- array constructor replacement. used since {...} discards nils. function M.niledarray(...) local n = select('#', ...) local d = {...} local t = setmetatable({}, mt) for i=1,n do if d[i] == nil then d[i] = NIL end end data[t] = d return t end -- table constructor replacement. used since {...} discards nils. function M.niledtablekv(...) -- possibly more optimally implemented in C. local n = select('#', ...) local tmp = {...} -- it would be nice to avoid this local t = setmetatable({}, mt) for i=1,n,2 do t[tmp[i]] = tmp[i+1] end return t end return M
示例/测试
-- test_nil.lua - test of NiledTable.lua local NiledTable = require "NiledTable" local exlen = NiledTable.exlen local exipairs = NiledTable.exipairs local expairs = NiledTable.expairs local exnext = NiledTable.exnext local exists = NiledTable.exists local delete = NiledTable.delete local niledarray = NiledTable.niledarray local niledtablekv = NiledTable.niledtablekv local t = NiledTable { } t[1] = 3 t[2] = nil t.x = 4 t.y = nil assert(t[1] == 3 and exists(t, 1)) assert(t[2] == nil and exists(t, 2)) assert(t[3] == nil and not exists(t, 3)) assert(t.x == 4 and exists(t, 'x')) assert(t.y == nil and exists(t, 'y')) assert(t.z == nil and not exists(t, 'z')) -- non-existant and nil values are both returned as nil and -- therefore both are logically false. -- allows "if t[3] then ... end" usage. assert(not t[2] and not t[3]) -- nils don't count in #t since __len cannot be overriden in -- 5.1 without special userdata tricks. assert(#t == 0) assert(exlen(t) == 2) -- workaround function -- constructor syntax doesn't work. The construction is done -- before the metatable is set, so the nils are discarded before -- NiledTable can see them. local t2 = NiledTable {nil, nil} assert(t2[1] == nil) -- alternate array constructor syntax (value list) that does work local t2 = niledarray(nil,nil) assert(t2[1] == nil and exists(t2, 1)) assert(t2[2] == nil and exists(t2, 2)) assert(t2[3] == nil and not exists(t2, 3)) --- more tests of niledarray local t2 = niledarray(1,nil,nil) assert(t2[1] == 1 and exists(t2, 1)) assert(t2[2] == nil and exists(t2, 2)) assert(t2[3] == nil and exists(t2, 3)) assert(t2[4] == nil and not exists(t2, 4)) t2[4]=4 assert(t2[4] == 4 and exists(t2, 4)) -- alternate table constructor syntax (key-value pair list) that does work local t2 = niledtablekv(1,nil, 2,nil) -- {[1]=nil, [2]=nill} assert(t2[1] == nil and exists(t2, 1)) assert(t2[2] == nil and exists(t2, 2)) assert(t2[3] == nil and not exists(t2, 3)) -- nils don't work with standard iterators local s = ""; local n=0 for k,v in pairs(t) do print("pairs:", k, v); n=n+1 end assert(n == 0) for i,v in ipairs(t) do print("ipairs:", i, v); n=n+1 end assert(n == 0) -- replacement iterators that do work for i,v in exipairs(t) do print("exipairs:", i, v); n=n+1 end n = n - 2; assert(n == 0) for k,v in expairs(t) do print("expairs:", k, v); n=n+1 end n = n - 4; assert(n == 0) for k,v in exnext, t do print("next:", k, v); n=n+1 end n = n - 4; assert(n == 0) -- Setting an existing element to nil, makes it nil and existant t[1] = nil assert(t[1] == nil and exists(t, 1)) for k,v in expairs(t) do print("expairs2:", k, v); n=n+1 end n = n - 4; assert(n == 0) -- Calling delete makes an element non-existant delete(t, 1) for k,v in expairs(t) do print("expairs3:", k, v); n=n+1 end n = n - 3; assert(n == 0) -- nil's still can't be used as keys though (and neither can NaN) assert(not pcall(function() t[nil] = 10 end)) assert(not pcall(function() t[0/0] = 10 end)) print "done"
[1] 包括 C、Java、Python 和 Perl。在 Perl 中,[exists 和 defined] 区分了这两种情况:exists $v{x}
与 defined $v{x}
。
-- DavidManura
...
或函数多个返回值转换为表时 nil
丢失的问题。