假如我们正在开发一段处理字符串的程序,比如逐行地读取一个文件。典型的代码可能形如:
local buff = "" for line in io.lines() do buff = buff .. line .. "\n" end
虽然这段 Lua
语言代码看似能够正常工作,但实际上在处理大文件时却可能导致巨大的性能开销。例如,在一台电脑上用这段代码读取一个 4.5MB
大小的文件需要超过 30
秒的时间。我们来分析以下原因,假设每行有 20
个字节,当我们读取了大概 2500
行后, buff
就会变成一个 50KB
大小的字符串。在 Lua
语言中进行字符串连接 buff .. line .. "\n"
时,会创建一个 50020
字节的新字符串,然后从 buff
中复制 50000
字节中到这个新字符串。这样,对于后续的每一行, Lua
语言都需要移动大概 50KB
且还在不断增长的内存。因此,该算法的时间复杂度是二次方的。在读取了 100
行(仅 2K
B)以后, Lua
语言就已经移动了至少 5MB
内存。当 Lua
语言完成了 350KB
的读取后,它已经至少移动了 50GB
的数据。(这个问题不是 Lua
语言特有的:在其他语言中,只要字符串是不可变值,就会出现类似的问题,其中最有名的例子就是 Java
)
在现实场景中,上述情况的发生是很小概率的事件,对于较小的字符串,上述循环并没什么问题。当读取整个文件时, Lua
语言提供了带有参数的函数 io.read("a")
来一次性地读取整个文件。不过,有时候我们必须面对这个问题。 Java
提供了 StringBuffer
类来解决这个问题,而在 Lua
语言中,我们可以把一个表当做字符串缓冲区,其关键是使用函数 table.concat
,这个函数会将指定列表中的所有字符串连接起来并返回连接后的结果。使用函数 concat
可以这样重写上述循环:
local t = {} for line in io.lines() do t[#t + 1] = line .. "\n" end local s = table.concat(t)
之前的代码读取同样的文件需要超过半分钟,而上述实现则只需要不到 0.05
秒。(不过尽管如此,读取整个文件最好还是使用带有参数 "a"
的 io.read
函数。)
继续优化,函数 concat
还有第二个可选参数,用于指定插在字符串间的分隔符。有了这个分隔符,我们就不必在每行后插入换行符了:
local t = {} for line in io.lines() do t[#t + 1] = line end local s = table.concat(t, "\n") .. "\n"
虽然函数 concat
能够在字符串之间插入分隔符,但我们还需要增加最后一个换行符。最后一次字符串连接创建了结果字符串的一个副本,这个副本可能已经相当长了。虽然没有直接的选项能够让函数 concat
插入这个额外的分隔符,但可以想办法绕过,只需要在字符串 t
后面添加一个空字符串就行了:
t[#t + 1] = "" s = table.concat(t, "\n")
现在,正如我们所期望的那样,函数 concat
会在结果字符串的最后添加一个换行符。