在Lua中,如何处理来自C的从零开始的数组索引?

16

在C代码中,我有一个数组和一个从零开始的索引用于在其中查找,例如:

char * names[] = {"Apple", "Banana", "Carrot"};
char * name = names[index];

我从一个嵌入的Lua脚本中,通过getIndex()函数访问了index,想要复制数组查找。鉴于Lua是以1为起始索引的数组,是否有一种被认为是“最佳”的方法来实现这一点?

例如,我可以创建一个与我的C数组具有相同内容的Lua数组,但这需要在索引时添加1:

names = {"Apple", "Banana", "Carrot"}
name = names[getIndex() + 1]

或者,我可以通过使用更复杂的表格来避免添加1的需要,但这会破坏像#names这样的东西:

names = {[0] = "Apple", "Banana", "Carrot"}
name = names[getIndex()]

推荐哪种方法?

编辑: 感谢迄今为止给出的答案。不幸的是,在getIndex函数内部将索引加1的解决方案并不总是适用的。这是因为在某些情况下,索引是“众所周知”的 - 也就是说,可能有记录表明索引0表示“苹果”等等。在这种情况下,应该更喜欢上述两种解决方案中的哪一种,还是有更好的选择吗?

编辑2: 再次感谢您的答案和评论,它们真正帮助我思考这个问题。我意识到可能存在两种不同的情况,而理想的解决方案可能针对每种情况都不同。

第一种情况是考虑一个数组,它可能随时间而变化,并且索引仅相对于当前数组。索引在代码外部没有任何意义。Doug Currie和RBerteig绝对正确:该数组应该是基于1的,getIndex应该包含+1。如前所述,这使得C和Lua端的代码都可以成为习惯用语。

第二种情况涉及具有意义的索引,可能还涉及始终相同的数组。一个极端的例子是names包含"Zero", "One", "Two"。在这种情况下,每个索引的预期值是众所周知的,我认为使Lua端的索引基于1是不直观的。我认为应该更喜欢其他方法之一。


你第一个示例中展示的方法有什么问题吗? - Tony The Lion
4
问题在于需要记得每次使用索引时都要加1。到处都有“+1”的语句显得很不美观,而第二个例子只有一个地方的代码看起来比较丑陋。 - user200783
回复:索引是“众所周知”的--这种使用“魔法数字”的做法是一种不好的实践,应该避免。更好的选择是使用getIndex函数或字符串。请注意,在Lua中,字符串很便宜,因为它们是内部化的,并且字符串比较是指针比较。 - Doug Currie
关于您在“具有意义的索引”上的编辑……当然,如果您需要将数字转换为对象,因为该数字很重要,请务必使用数字作为Lua表的索引。特别是如果数字可以是计算结果的话。这类似于记忆化。在这些情况下,表也可能是稀疏的,例如从街道号码到居民姓名的表。 - Doug Currie
7个回答

16

使用基于1的Lua表,将+ 1放在getIndex函数内部。


3
这段话的含义是:在C和Lua之间的连接中隐藏它们之间的特殊差异,这样你的Lua用户就可以以Lua惯用语言的方式看待世界,同时保留C的惯用语言。在getIndex函数和你编写的将C数组作为表发布到Lua中的函数中隐藏摩擦。 - RBerteig

6

I prefer

names = {[0] = "Apple", "Banana", "Carrot"}
name = names[getIndex()]

表格操作的一些功能 - #, insert, remove, sort - 已经失效。
其他功能 - concat(t, sep, 0), unpack(t, 0) - 需要明确指定起始索引才能正常运行:

print(table.concat(names, ',', 0))  --> Apple,Banana,Carrot
print(unpack(names, 0))             --> Apple   Banana  Carrot

我不喜欢不断地记住这个+1以迎合Lua的默认基于1的索引风格。
你的代码应该反映你特定领域的索引以提高可读性。
如果基于0的索引在你的任务中表现良好,则应在Lua中使用基于0的索引。

我喜欢Pascal中如何实现数组索引:您完全可以自由选择任何范围,例如,array[-10..-5]of byte对于一个包含6个元素的数组来说是绝对没问题的。


6

这就是Lua元方法和元表的用处。使用一个table代理和一些元方法,你可以以适合自己需要的方式修改对table的访问。

local names = {"Apple", "Banana", "Carrot"} -- Original Table
local _names = names -- Keep private access to the table
local names = {}    -- Proxy table, used to capture all accesses to the original table

local mt = {
  __index = function (t,k)
    return _names[k+1]   -- Access the original table
  end,

  __newindex = function (t,k,v)
    _names[k+1] = v   -- Update original table
  end
}

setmetatable(names, mt)  

这里正在发生的情况是,原始表格有一个代理,然后代理捕获表格的每个访问尝试。当访问该表时,它会增加被访问的值,模拟0-based数组。以下是打印结果:

print(names[0]) --> Apple
print(names[1]) --> Banana
print(names[2]) --> Carrot
print(names[3]) --> nil

names[3] = "Orange" --Add a new field to the table
print(names[3]) --> Orange

所有表操作都像平常一样。使用这种方法,您不必担心与表的任何异常访问有关的问题。

编辑:我想指出新的“names”表仅是一个代理,用于访问原始的“names”表。因此,如果查询#names,结果将为nil,因为该表本身没有值。您需要查询#_names来访问原始表的大小。

编辑2:正如Charles Stewart在下面的评论中指出的那样,您可以向mt表添加__len元方法,以确保#names调用会给出正确的结果。


这种方法是否比 local names = {[0] = "Apple", "Banana", "Carrot"} 有任何优势?虽然这两种方法对于 #namesipairs(names) 都有不同的行为,但对于任何一种方法都不是理想的行为。 - user200783
3
据我所了解,优势在于原始数组仍然基于1开始编号,因此只要在原始表上调用标准的Lua函数(如insert,ipairs,remove,#),它们仍旧有效。只有通过代理才能以0为基础访问该数组。如果你想了解更多关于Lua元表和代理的知识,我建议参考《Programming in Lua》 链接。第一版可以免费在线阅读,但略显过时。 - dburdan
4
在Lua 5.2中,通过在上述mt中设置__len方法,您可以确保#names返回正确的结果。 - Charles Stewart

6
首先,这种情况并不仅限于混合使用Lua和C的应用程序;即使在仅使用Lua的应用程序中,您也可能会面临同样的问题。举个例子,我正在使用一个编辑器组件,它从0开始索引行(是基于C的,但我只使用其Lua接口),但我在编辑器中编辑的脚本中的行是从1开始的。因此,如果用户在第3行(从编辑器中的0开始)上设置断点,则需要向调试器发送命令,在脚本中将其设置为第4行(并在触发断点时进行转换)。
现在是一些建议。
(1)我个人不喜欢使用[ 0 ]数组hack,因为它会破坏太多东西。你和Egor已经列出了许多问题;对我来说最重要的是它破坏了#和ipairs。
(2)当使用基于1的数组时,我尽量避免对它们进行索引,并尽可能使用迭代器:for i, v in ipairs(...) do而不是for i = 1, #array do)。
(3)我还尝试隔离处理这些转换的代码;例如,如果您正在转换编辑器中的行以管理标记和脚本中的行,则使用marker2script和script2marker函数进行转换(即使它是简单的+1和-1操作)。即使没有+1/-1调整,您也会有类似的代码。这将是隐式的。
(4)如果不能隐藏转换(我同意,+1可能看起来很丑),那么使它更加显眼:使用c2l和l2c调用进行转换。在我看来,这不像+1/-1那么丑陋,但具有传达意图的优点,并为您提供搜索发生转换的所有位置的简单方法。当您正在查找关闭一个错误或API更改导致更新此逻辑时,它非常有用。
总体而言,我不会过于担心这些方面。我正在开发一个包装多个基于0的C组件的相当复杂的Lua应用程序,不记得由于不同的索引导致的任何问题...

4
为什么不将C数组也转换为基于1的数组呢?
char * names[] = {NULL, "Apple", "Banana", "Carrot"};
char * name = names[index];

坦白说,这将导致C端的一些不直观的代码,但如果你坚持认为必须有在两侧都有效的“众所周知”的索引,那么这似乎是最好的选择。
更干净的解决方案当然是不将那些“众所周知”的索引作为接口的一部分。例如,您可以使用命名标识符而不是纯数字。在C端,枚举类型很适合这种情况,而在Lua中,甚至可以使用字符串作为表键。
另一个可能性是将表封装在接口后面,以便用户永远不会直接访问数组,而只能通过C函数调用来访问,该调用可以执行任意复杂的索引转换。然后,您只需要在Lua中公开该C函数,就可以得到一个干净且易于维护的解决方案。

谢谢您的建议。不幸的是,我无法更改界面和/或底层C代码 - 我正在寻找在现有接口周围构建我的Lua代码的最佳方法。 - user200783

3
为什么不将C数组呈现为Lua的userdata? 该技术在PiL,'Userdata'部分中有代码描述;您可以设置__index、__newindex和__len元表方法,并且可以从类继承以提供其他序列操作函数作为常规方法(例如使用array.remove、array.sort、array.pairs函数定义一个数组,通过对__index进行进一步调整,这些函数可以定义为对象方法)。这种方式做事意味着Lua和C之间没有“同步”问题,并避免了“数组”表被视为普通表而导致的差一错误。

0

您可以通过使用一个能够识别不同索引基础的迭代器来修复这个 Lua 缺陷:

function iarray(a)
  local n = 0
  local s = #a
  if a[0] ~= nil then
    n = -1
  end
  return function()
    n = n + 1
    if n <= s then return n,a[n] end
  end
end

然而,您仍需手动添加第零个元素:

使用示例:

myArray = {1,2,3,4,5}
myArray[0] = 0
for _,e in iarray(myArray) do
  -- do something with element e
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接