在表中存储空值

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。事实上,这是 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作为一张表,是一个对象,而对象具有唯一的标识。表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不是唯一不能存储在表中的值。由0/0定义的“非数字”值(NAN)不能作为键存储(但可以作为值存储)。NAN作为表键存在一些潜在问题:LuaList:2005-11/msg00214.html。可以通过类似的方式解决此限制。但是,请注意,NAN是唯一不等于自身的(NAN ~= NAN)的值,这是测试NAN的方法。

技巧:通过元函数区分UNDEFnil

一种可能的解决方案是在给定表上定义一个元表,以便如果键存在且值为nil,则表返回nil,但如果键不存在,则返回一个新的唯一值UNDEF = {}。但是,这有一些相当严重的问题。首先,UNDEF在逻辑上等于true,因此我们不能使用if t[k] then ... end这种习惯用法,因为它将在k在表中未定义的情况下执行分支。更重要的是,程序员可能会尝试将这些UNDEF值存储在表中,从而导致类似的问题,即UNDEF不能存储在表中。

该方法在下面通过使表成为另一个私有表的代理来实现,该私有表维护nilUNDEF信息,并且__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

另请参阅


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2011 年 1 月 13 日上午 6:36 GMT (差异)