Lua 字符串迭代方式

最近一直有同学在私下问我关于Lua字符串迭代的方式, 我觉得有必要和大家聊一下关于为字符串迭代聊一个使用场景.

迭代器

如果我们使用C/C++, 那么非常简单可以使用那些数字索引的loop来完成. 且这样的迭代、比较效率也非常高. 然而ANSIC字符并不能完全符合业务多变的复杂环境, 所以我们也需要考虑高级、方便的使用方法.

参考其它编程语言我们可以发现, 其中有绝大部分使用的是迭代器思想. 即: 给出迭代函数, 通过循环来不断传入对象后调用迭代函数来完成. 这种方式是值得借鉴的, 即使在Lua内我们需要一些trick才能完成.

Lua内使用迭代方式目前存在以下几种使用方式:

  1. for i = start, stop, step do ... end

  2. for index, value in ipairs(..) do ... end

  3. for ... in iterator function do ... end

如果没有规定业务级别的特殊情况, 建议大家使用第1种来完成. 理由当然是效率最高, 编码也非常简单易懂. 但是当你不考虑性能问题且觉得迭代需要统一使用ipairs完成的时候, 只需要在string的元方法内实现利于ipairs迭代字符串的判断即可.

3种方式其实我们可以通过string.gmatch来实现! 但是我个人并不希望使用正则或匹配模式实现, 且我希望它能提供丰富的特性来适用于更多的场景.

1. 迭代器

既然自定义一个iterator才能完成我们的需求那就直接动手就好了. 首先, 我们规划了以下需求:

  • 字符串必须支持ANSIC字符迭代与UTF-8字符两种.

  • 字符串迭代出错必须不影响流程.

  • 字符串迭代对性能影响不能太大.

现在需求知道了! 那么先定义函数来规定我们如何使用:

    ---comment 可以通过迭代函数逐个字符迭代
    ---@param  text    string    @待迭代的内容
    ---@param  u8      boolean?  @检查UTF8字符(可选, 默认为`false`)
    ---@return function
    function string.iterator(text, u8)
    -- TODO
    end

普通ANSIC字符迭代我们可以直接string.sub分割即可, UTF-8字符则可以使用utf8可以来根据位置与数字编码来确定. 最后我们可以根据开始位置加上每次迭代字符的位置计算是否到达字符串结尾. 这样我们的迭代器的功能就算完成了, 大致的伪代码如下:

if u8 then
  -- call utf8.code got code.
  -- call utf8.char got char.
  -- ret
end
-- sub (text, index, index)
-- ret

字符串内出现中文应该并不少见, 但是我们在迭代器中如果抛出捕获和调试代码会变得非常难看. 如果有必要, 我们在迭代器内迭代UTF-8字符串的时候可以这样操作:

if not valid_u8_char then
  return index, false, char
end
return index, true, char

1个返回值是utf8字符串的索引位置, 第2个值在按utf8字符串规则迭代时发现了非法字符则为false反之为true. 第3个返回值就是实际字符. 这种方式可以让开发者在循环期间就能判断, 并且拿到合适的值后再进行选择性跳出(或忽略). 最后我们来看看示例代码与结果:

local s = "我是谁?\x80\x80"

for index, valid, str in string.iterator(s, true) do
  print(index, valid, str)
end
1       true    我
4       true    是
7       true    谁
10      true    ?
13      false   �
14      false   �

可以看到结果完全满足我们的构想.

2. 索引

众所周知, ipairs 在5.3开始就废弃了__ipairs原方法而使用__index来索引迭代. 而string并未完成实际意义上的数字索引下标查找字符的特性. 那么, 我们就只能通过重写__index方法来完成了.

首先我们必须通过getmetatable拿到string的元表, 这样就能hook所有字符串向上索引的操作. 然后预留好我们所需的代码位置, 在条件不满足时交给string来完成后续操作.

getmetatable("").__index = function (text, key)
  -- hook start
  -- ...
  -- hook stop
  return string[key]
end

最后我们来补齐中间的代码:

if key == 'integer' then
  if key > text.len then
    -- return nil that let's stoped
  end
  -- sub(text, key, key)
  -- ret 
end

实现代码完成! 现在我们可以尝试编写一些示例来测试:

local s = "abcdefg"

for index, value in ipairs(s) do
  print(index, value)
end
1       a
2       b
3       c
4       d
5       e
6       f
7       g

最后

之前所在项目内并无对迭代大量字符串的需求, 所以C API来完成上述功能并不是那么必要.