在表格中儲存 Nil

lua-users home
wiki

Lua 表格不會區分表格值為 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」欄位

我們可以採用的一種解決方式,就是將表格長度儲存在鍵值為 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作為表格,是一個物件,而物件有獨特的識別碼。表格NILdo區塊中是詞彙作用範圍,且在程式中其他地方都不可見,只有一個例外,就是表格中。使用者可以從表格中取用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的方式。

破解:透過元函式區分UNDEFnil

一個可能的解決方案是針對特定表格定義元表,以便如果鍵存在且值為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

另請參閱


近期變更 · 偏好設定
編輯 · 歷史紀錄
上次編輯於 2011 年 1 月 13 日格林威治時間上午 6:36 (差異)