在表格中儲存 Nil |
|
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
的表格中。 事實上,這正是 5.1 版以前的 Lua 實作陣列長度的作法
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 Saving」)。
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
並非唯一無法儲存在表格中的值。「非數字」值(NAN
),由0/0
定義,無法儲存為鍵(但可以儲存為值)。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
v.s. 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"
上述的攻擊問題可以透過避免在模組外暴露任何新值來解決。所以,當 k
不在 table 中或是對應的值是 nil
時,t[k]
都會回傳 nil
。我們可以用新的函式 exists(t,k)
來區別這兩種條件,它會回傳一個布林值來表示鍵值 k
是否存在於 table t
中。(此行為與 Perl 語言類似。)
透過此方式,我們仍然可以在恰當的時候使用 if t[k] then ... end
這一習用語,或是使用新的 if exists(t, k) then ... end
。而且,不會將新值引入語言中,因此避免了上述「儲存在 table 中的 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
...
或函式多重回傳值轉換成 table 時,nil
會遺失的問題。