如何选择合适的字符串连接方式?
Lua语法糖 .. 可以很方便的做字符串连接。
使用限制:
简单示例
local a = "1" .. "2" .. "3"
生成的Lua字节码:
1 [1] LOADK 0 -1 ; "1"
2 [1] LOADK 1 -2 ; "2"
3 [1] LOADK 2 -3 ; "3"
4 [1] CONCAT 0 0 2
先创建三个字符串,再通过一个CONCAT函数将三个字符串连接成一个字符串。
源码:
CONCAT函数(luaV_concat)会先计算连接后字符串长度,然后申请内存,并将字符串内容依次memcpy到新内存中。有一点要注意的是,通过 … 连接字符串时,会把子串全部压到堆栈上,如果子串过多,会不断的扩张栈的大小,合并完后,还会触发栈回缩,对性能有一定影响。核心代码如下:
void luaV_concat (lua_State *L, int total, int last) {
do {
// 先计算要连接的字符串的长度
size_t tl = tsvalue(top-1)->len;
for (n = 1; n < total && tostring(L, top-n-1); n++) {
size_t l = tsvalue(top-n-1)->len;
if (l >= MAX_SIZET - tl) luaG_runerror(L, "string length overflow");
tl += l;
}
// 申请对应长度的内存
buffer = luaZ_openspace(L, &G(L)->buff, tl);
// 通过memcpy拼接字符串
for (i=n; i>0; i--) { /* concat all strings */
size_t l = tsvalue(top-i)->len;
memcpy(buffer+tl, svalue(top-i), l);
tl += l;
}
}
误区:
之前一直以为每一次 .. 连接都会产生一个新的字符串常量,导致一些中间字符串的生成,但从上面解释可以看出,并不如此:
@(误区)
s = a .. b .. c .. d
==> s = ab .. c .. d
==> s = abc .. d
==> s = abcd
什么情况下会产生中间字符串,如下:
local s = ""
for i=1,3 do
s = s .. "a"
end
生成的Lua字节码:
5 [4] LOADK 1 -4 ; ""
6 [5] LOADK 2 -5 ; 1
7 [5] LOADK 3 -6 ; 3
8 [5] LOADK 4 -5 ; 1
9 [5] FORPREP 2 3 ; to 13
10 [6] MOVE 6 1
11 [6] LOADK 7 -7 ; "a"
12 [6] CONCAT 1 6 7
13 [5] FORLOOP 2 -4 ; to 10
每次循环都会调用CONCAT函数新建一个新的字符串(中间字符串),而且这些字符串在最终的字符串生成完后由于没有被引用,又马上被垃圾回收器回收,导致全局表被多次rehash。当循环的次数很多时,不仅会分配大量内存去存中间字符串,还会多次扩缩容并rehash全局表,导致效率低下。
Lua table模块内置的concat函数,将table数组部分从start到end位置元素以指定sep连接起来。
table.concat(table, sep, start, end)
使用限制:
示例:
local t = {1,2,3}
table.concat(t)
生成的Lua字节码:
1 [1] NEWTABLE 0 3 0
2 [1] LOADK 1 -1 ; 1
3 [1] LOADK 2 -2 ; 2
4 [1] LOADK 3 -3 ; 3
5 [1] SETLIST 0 3 1 ; 1
6 [2] GETGLOBAL 1 -4 ; table
7 [2] GETTABLE 1 1 -5 ; "concat"
8 [2] MOVE 2 0
9 [2] CALL 1 2 1
10 [2] RETURN 0 1
先创建一个table并初始化,然后调用concat函数。
源码:
concat内部实现函数是tconcat,如下:
static const luaL_Reg tab_funcs[] = {
{"concat", tconcat},
...
};
如果对Lua的栈不熟,看tconcat函数可能会有点不好看懂,下面简单解释下:
static int tconcat (lua_State *L) {
// table.concat的参数都存放在栈上,可以简单理解为:
// 栈1号位存的是table
// 栈2号位存的是sep
// 栈3、4号位存的是起始(start)和结束(end)的位置
// 先从2号位置取出sep,如果2号位置是nil,即没有指定sep,则sep=""
const char *sep = luaL_optlstring(L, 2, "", &lsep);
// 检查栈1号位置存的数据类型是否是一个table
luaL_checktype(L, 1, LUA_TTABLE);
// i table起始索引,如果起始索引为nil,则默认从1开始
i = luaL_optint(L, 3, 1);
// last table结束索引,如果结束索引为nil,则默认为table数组部分大小。
last = luaL_opt(L, luaL_checkint, 4, luaL_getn(L, 1));
// 申请一块buff,存合并后的数据,初始buff大小为8192
// 当buff大小用完后就直接用luaV_concat在栈上做字符串连接
luaL_buffinit(L, &b);
// 把table里面索引从i到last的值取出来,并放到buff里面,buff大小为BUFSIZ(8192)
// 每当写满一个buff,就把buff生成一个TString,并放到栈上,并把buff清空重新写
for (; i < last; i++) {
addfield(L, &b, i);
luaL_addlstring(&b, sep, lsep);
}
// 把最后一个元素放入buff
// 把buff生成一个TString,并放到栈上
if (i == last)
addfield(L, &b, i);
// 把栈上之前所有生成的TString通过luaV_concat(就是 .. 语法糖的合并函数)合并成一个TString,放到栈上,相当于返回值,供上层函数取用
luaL_pushresult(&b);
return 1;
}
从源码上看,table.concat没有频繁申请内存,只有当写满一个8192的BUFF时,才会生成一个TString,最后生成多个TString时,会有一次内存申请并合并。在大规模字符串合并时,应尽量选择这种方式。
误区:
每次都要通过索引从table里面查找对应的值,这个查找会很耗时。
这个说法不对, table.concat 只会对数组部分进行字符串拼接,通过索引查询数组时,时间复杂度为O(1)。
Lua string模块内置的format函数,和C语言的sprintf类似,可以将不同类型的数据格式化成字符串。
格式:
string.format(fmt, […])
%c - 接受一个数字, 并将其转化为ASCII码表中对应的字符
%d, %i - 接受一个数字并将其转化为有符号的整数格式
%o - 接受一个数字并将其转化为八进制数格式
%u - 接受一个数字并将其转化为无符号整数格式
%x - 接受一个数字并将其转化为十六进制数格式, 使用小写字母
%X - 接受一个数字并将其转化为十六进制数格式, 使用大写字母
%e - 接受一个数字并将其转化为科学记数法格式, 使用小写字母e
%E - 接受一个数字并将其转化为科学记数法格式, 使用大写字母E
%f - 接受一个数字并将其转化为浮点数格式
%g(%G) - 接受一个数字并将其转化为%e(%E, 对应%G)及%f中较短的一种格式
%q - 接受一个字符串并将其转化为可安全被Lua编译器读入的格式
%s - 接受一个字符串并按照给定的参数格式化该字符串
符号: 一个+号表示其后的数字转义符将让正数显示正号. 默认情况下只有负数显示符号.
占位符: 一个0, 在后面指定了字串宽度时占位用. 不填时的默认占位符是空格.
对齐标识: 在指定了字串宽度时, 默认为右对齐, 增加-号可以改为左对齐.
宽度数值
小数位数/字串裁切: 在宽度数值后增加的小数部分n, 若后接f(浮点数转义符, 如%6.3f)则设定该浮点数的小数只保留n位, 若后接s(字符串转义符, 如%5.3s)则设定该字符串只显示前n位.
示例:
local a = 1
local b = "abc"
string.format("id:%d,name:%s", a, b)
生成的Lua字节码:
1 [1] LOADK 0 -1 ; 1
2 [2] LOADK 1 -2 ; "abc"
3 [3] GETGLOBAL 2 -3 ; string
4 [3] GETTABLE 2 2 -4 ; "format"
5 [3] LOADK 3 -5 ; "id:%d,name:%s"
6 [3] MOVE 4 0
7 [3] MOVE 5 1
8 [3] CALL 2 4 1
9 [3] RETURN 0 1
先将参数a, b压栈,再调用format函数。
源码:
format内部实现函数是str_format,如下:
static const luaL_Reg strlib[] = {
...
{"format", str_format},
...
};
str_format函数并不难看懂,下面简单解释下:
static int str_format (lua_State *L) {
// 指向string.format参数在栈上的位置,可以简单的理解为:
// 1:指向第一个参数
// 2: 指向第二个参数
int arg = 1;
// 从栈上获取第一个参数:fmt,两个指针分别指向该字符串的起止位置
const char *strfrmt = luaL_checklstring(L, arg, &sfl);
const char *strfrmt_end = strfrmt+sfl;
// 初始化一个8192大小的BUFF,格式化的字符串就往里面写。
luaL_Buffer b;
luaL_buffinit(L, &b);
while (strfrmt < strfrmt_end) {
// 非%...的直接写入BUFF
if (*strfrmt != L_ESC)
luaL_addchar(&b, *strfrmt++);
// %% 直接写入%
else if (*++strfrmt == L_ESC)
luaL_addchar(&b, *strfrmt++); /* %% */
else { /* format item */
// 每找到一个%... 就写入form
// 把找到的%...,格式化后的结果写入buff
char form[MAX_FORMAT]; /* to store the format (`%...') */
char buff[MAX_ITEM]; /* to store the formatted item */
strfrmt = scanformat(L, strfrmt, form);
switch (*strfrmt++) {
// 对%c做具体格式化
case 'c': {
sprintf(buff, form, (int)luaL_checknumber(L, arg));
break;
}
...
}
// 将格式化后结果写入BUFF
// 如果BUFF大小不够,则先将BUFF生成一个TString,并压入栈上,清空BUFF重新写
luaL_addlstring(&b, buff, strlen(buff));
}
}
// 把栈上之前所有生成的TString通过luaV_concat(就是 .. 语法糖的合并函数)合并成一个TString,放到栈上,相当于返回值,供上层函数取用
luaL_pushresult(&b);
return 1;
}
从上面源码可以看出,string.format要先解析字符串,再将不同类型的数据格式化成字符串,然后写入BUFF, 写BUFF的方式和table.concat是一样的。
Lua string内置模块中另一个可以做字符串连接是的rep,不过使用局限性很大,只能重复的对某一个字符串做N次拼接。
string.rep(str, n)
使用限制:
示例:
string.rep("abc", 3)
生成的Lua字节码:
1 [1] GETGLOBAL 0 -1 ; string
2 [1] GETTABLE 0 0 -2 ; "rep"
3 [1] LOADK 1 -3 ; "abc"
4 [1] LOADK 2 -4 ; 3
5 [1] CALL 0 3 1
6 [1] RETURN 0 1
先PUSH参数到栈上,然后调用rep函数。
源码:
concat内部实现函数是str_rep,如下:
static const luaL_Reg tab_funcs[] = {
{"rep", str_rep},
...
};
str_rep代码非常简短,也非常好理解:
static int str_rep (lua_State *L) {
// 从栈1的位置获取字符串s
const char *s = luaL_checklstring(L, 1, &l);
// 从栈2的位置获取重复次数n
int n = luaL_checkint(L, 2);
// 初始化一个buff,buff大小为8192
luaL_buffinit(L, &b);
// 把字符串 s 写入到buff,重复写 n 次
// 当buff大小不够时的处理同string.format的实现。
while (n-- > 0)
luaL_addlstring(&b, s, l);
// 获得最后的结果
luaL_pushresult(&b);
return 1;
}
这四种字符串连接方式,从源码上分析,其实实现方式差不多,只是table.concat要先创建一个table再去拼接,string.format需要解析字符串,这些可能会有点耗时,但我觉得影响并不大。在选择连接方式时,只需考虑易用和代码简洁性就可以了。下面有些小建议,仅供参考:
local a = "a" .. 1 .. 2
local t = {"a", "b", "c", "d"}
local s = table.concat(t)
local a=1
local b="abc"
local s = string.format("id:%02d, name:%04s", a, b)
string.rep("abc", 3)