Lua string 库经验分享:



在部门里用Lua实现了一个RichText富文本,主要用到了一些Lua的模式匹配,想到之前也有好几次使用Lua string库的经验,虽然不多,也没怎么读过Lua的源码,但还是想拿出来与人分享。



Lua的模式匹配极为强大,掌握之后使用起来非常有乐趣,但期间很可能会遇到很多问题。比如在下就在string.find上跌倒过无数次。




string.find 注意事项:你的string.find 能 find 到吗?



测试以下代码:



local TAG = "string.find: "
print(TAG, string.find("1+1=2", "1+1"))





基础好的人一眼就看出来,这样是找不出结果的。原因很简单,没有处理Lua里的 Magic 字符,这些 Magic 字符包含:



 ( ) . % + - * ? [ ^ $    



【注,Programming in Lua 一书中似乎漏掉了 "]"】



现在,我们需要一个方法,能把字符串里的魔法字符使用Lua的转义字符转义。这时候应该使用 string.gsub 了。




各位先思考一下,这个函数应该如何来写?



1秒过去了......



2秒过去了......



3秒过去了......



......



好了,为了方便大家理解 string.gsub() 的用法,我用两种方式来写吧。



第一种:



function ConvertSpecialChar(str)
    return string.gsub(str, "[%(%)%.%%%+%-%*%?%[%]%^%$]",
        function(ret)
            ret = "%"..ret 
            return ret 
        end) 
end




参数2 是一个pattern,



参数3 是一个函数,string.gsub会把匹配得到的结果作为函数参数传入,函数内切莫忘了return。






第二种:



function ConvertSpecialChar(str)
    return string.gsub(str, "([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1") 
end




参数2 里多了一对括号,表示捕获截取到的字符串。



参数3 如果参数2里有多个括号,则 %1 对应第一个括号里捕获的内容,%2 对应第二个括号捕获的内容,一次类推。gsub会把捕获出的内容,按参数3的格式去替换原来的字符串。



至于第二种方案里,为什么要用到3个 % ,这个请大家自己去测试。






测试代码:



local TAG = "string.gsub: "
print(TAG, ConvertSpecialChar("1+1+1++1+++1=2"))




看看是不是每个+号前都多了一个%?







有了上面的基础,再回过头来看看开始的例子:



print(TAG, string.find("1+1=2", "1+1"))



理论上,是不是只要调用一下ConvertSpecialChar()来转换一下 string.find 里的第二个参数就可以了呢?



为了方便,我们直接这么写代码:



print(TAG, string.find("1+1=2", ConvertSpecialChar("1+1")))



测试一下,



输出:    nil






!!!WTF!!!这样不对??



什么情况,赶紧再测试一下:



print(TAG, string.find("1+1=2", "1%+1"))



输出:    1    3






???Why???这样就是对的。



赶紧打印一下函数返回值。



print(ConvertSpecialChar("1+1"))



输出:    1%+1



这是正确的预期结果。






如果你也跟我一样迷糊,别担心,马上就不会揭晓谜底了。



这个错误是作者在实现RichText的时候,遇到的错误,测试检验了1个多小时,才弄明白为什么。



原来,



string.find()不接受函数作为参数,只接受字符串


顺便看了一下 lstrlib.c ,里面包含了 Lua 的string库实现。找到 str_find_aux 函数,是这样的:


static int str_find_aux (lua_State *L, int find) { 
  size_t l1, l2;
  const char *s = luaL_checklstring(L, 1, &l1);
  const char *p = luaL_checklstring(L, 2, &l2);
  ptrdiff_t init = posrelat(luaL_optinteger(L, 3, 1), l1) - 1;
  if (init < 0) init = 0;
  else if ((size_t)(init) > l1) init = (ptrdiff_t)l1;
  if (find && (lua_toboolean(L, 4) || /* explicit request? */
      strpbrk(p, SPECIALS) == NULL)) { /* or no special characters? */
    /* do a plain search */
    const char *s2 = lmemfind(s+init, l1-init, p, l2);
    if (s2) {
      lua_pushinteger(L, s2-s+1);
      lua_pushinteger(L, s2-s+l2);
      return 2;
    }
  }
  else {
    MatchState ms;
    int anchor = (*p == '^') ? (p++, 1) : 0;
    const char *s1=s+init;
    ms.L = L;
    ms.src_init = s;
    ms.src_end = s+l1;
    do {
      const char *res;
      ms.level = 0;
      if ((res=match(&ms, s1, p)) != NULL) {
        if (find) {
          lua_pushinteger(L, s1-s+1); /* start */
          lua_pushinteger(L, res-s); /* end */
          return push_captures(&ms, NULL, 0) + 2;
        }
        else
          return push_captures(&ms, s1, res);
      }
    } while (s1++ < ms.src_end && !anchor);
  }
  lua_pushnil(L); /* not found */
  return 1;
}


看到开头的 luaL_checklstring 差不多就知道所以然了。


(在下的Lua源码看得很少很少,很多东西只有用到的时候才会来看。)


所以,在有了 ConvertSpecialChar 函数后,要先调用函数,得到转换后的字符串,再去调用 string.find,才是正确的做法。




最后,如果大家有兴趣,可以再去测试一下如下代码:


local TAG = "string.find:    "
print(TAG, string.find("1+1=2", ConvertSpecialChar("1+1").."="))


测试一下看能否得到正确的结果。




coming next...


string.gsub