Lua學習與交流——Lua string庫經驗分享

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章