Lua 避免在用作数组的表中出现间隙

示例

定义我们的条款

通过阵列这里我们指的是作为一个序列的Lua表。例如:

-- Create a table to store the types of pets we like.
local pets = {"dogs", "cats", "birds"}

我们将此表用作序列:一组由整数键组成的项目。许多语言将其称为数组,我们也将如此。但是严格来说,Lua中没有数组这样的东西。只有表格,其中一些像数组,有些像哈希(或字典,如果您愿意),有些是混合的。

关于pets数组的重要一点是没有差距。第一项pets[1]是字符串“ dogs”,第二项pets[2]是字符串“ cats”,最后一项pets[3]是“ birds”。Lua的标准库和为Lua编写的大多数模块都将1作为序列的第一个索引。因此,无间隙数组具有项,1..n而不会丢失序列中的任何数字。(在极限情况下,,n = 1并且数组中只有一项。)

Lua提供了内置功能ipairs来迭代此类表。

-- Iterate over our pet types.
for idx, pet in ipairs(pets) do
  print("位置项目 " .. idx .. " is " .. pet .. ".")
end

这将打印“位置1的项是狗。”,“位置2的项是猫”,“位置3的项是鸟”。

但是,如果我们执行以下操作会怎样?

local pets = {"dogs", "cats", "birds"}
pets[12] = "goldfish"
for idx, pet in ipairs(pets) do
  print("位置项目 " .. idx .. " is " .. pet .. ".")
end

诸如第二个示例这样的数组是稀疏数组。序列中有缺口。该数组如下所示:

{"dogs", "cats", "birds", nil, nil, nil, nil, nil, nil, nil, nil, "goldfish"}
-- 1        2       3      4    5    6    7    8    9    10   11       12

nil值不占用任何其他内存;内部卢阿只保存值[1] = "dogs",[2] = "cats",[3] = "birtds"和[12] = "goldfish"

为了回答眼前的问题,ipairs将在鸟鸣之后停止;pets[12]除非我们调整代码,否则永远不会达到“金鱼” 。这是因为,ipairs从迭代1..n-1这里n是第一的位置nil发现的。Lua定义table[length-of-table + 1]为nil。因此,按照适当的顺序,当Lua尝试获取三项数组中的第四项时,迭代停止。

什么时候?

稀疏数组出现问题的两个最常见的地方是(i)尝试确定数组的长度时和(ii)尝试遍历数组时。特别是:

  • 使用#长度运算符时,由于长度运算符会从nil发现的第一个停止计数。

  • ipairs()由于如上所述,在使用该函数时,它将在nil找到的第一个位置停止迭代。

  • 使用该函数时,因为此方法在首次发现时便停止解包。table.unpack()nil

  • 当使用(直接或间接)访问上述任何功能的其他功能时。

为避免此问题,编写代码很重要,这样,如果您希望表是数组,就不会引入间隙。差距可以通过几种方式引入:

  • 如果将某些内容添加到错误位置的数组中。

  • 如果将nil值插入数组。

  • 如果从数组中删除值。

您可能会想,“但是我永远不会做任何事情。” 好吧,不是故意的,但这是一个具体的例子,说明事情可能会出错。想象一下,您想为Lua编写一个Rubyselect和Perl的过滤方法grep。该方法将接受一个测试函数和一个数组。遍历数组,依次对每个项目调用test方法。如果该项通过,则将该项添加到结果数组,该数组在方法末尾返回。以下是错误的实现:

local filter = function (fun, t)
  local res = {}
  for idx, item in ipairs(t) do
    if fun(item) then
      res[idx] = item
    end
  end

  return res
end

问题在于,当函数返回时false,我们将跳过序列中的数字。想象一下调用filter(isodd, {1,2,3,4,5,6,7,8,9,10}):每次传递给的数组中有偶数时,返回的表中都会有间隙filter。

这是一个固定的实现:

local filter = function (fun, t)
  local res = {}
  for _, item in ipairs(t) do
    if fun(item) then
      res[#res + 1] = item
    end
  end

  return res
end

提示

  1. 使用标准函数:table.insert(<table>, <value>)始终追加到数组的末尾。  table[#table + 1] = value是这个的捷径。table.remove(<table>, <index>)会将所有后续值移回以填补空白(这也会使其变慢)。

  2. 插入之前检查nil值,避免使用诸如之类的东西,它们可能会将值潜入我们的表中。table.pack(function_call())nil

  3. 插入后检查nil值,必要时通过移动所有连续值来填补空白。

  4. 如果可能,请使用占位符值。例如,更改nil为0或其他一些占位符值。

  5. 如果不可避免要留有空隙,则应妥善记录(注释)。

  6. 编写一个__len()元方法并使用#运算符。

示例6 .:

tab = {"john", "sansa", "daenerys", [10] = "the imp"}
print(#tab) --> prints 3
setmetatable(tab, {__len = function() return 10 end})
-- __len needs to be a function, otherwise it could just be 10
print(#tab) --> prints 10
for i=1, #tab do print(i, tab[i]) end
--> prints:
-- 1 john
-- 2 sansa
-- 3 daenerys
-- 4 nil
-- ...
-- 10 the imp

for key, value in ipairs(tab) do print(key, value) end
--> this only prints '1 john \n 2 sansa \n 3 daenerys'

另一种选择是使用该pairs()函数并滤除非整数索引:

for key in pairs(tab) do
    if type(key) == "number" then
        print(key, tab[key]
    end
end
-- note: this does not remove float indices
-- does not iterate in order