lua 里其实也颇多奇技淫巧,使用时应三思。
如果你读过 kepler 的代码,就会发现,多次编译这种技巧用的很多,甚至迭代几次使用。即,第一次加载代码时,用一段 lua 程序生成真正需要的源代码,然后再将其编译出来。
由于 lua 的编译速度相当快,而且这种迭代编译的过程仅仅在程序加载的时候进程一次,故而可以带来性能的提高:一些在系统初始化时可以决定的参数(比如从配置文件中读出来的数据)直接编译为常量置入程序中。
云风写了一小段 lua 程序,简化这种迭代编译的过程,算作周末自娱吧。
例如这样一个例子,我们在程序中需要用一个常量,这个常量可能是通过加载配置文件得到。假设允许编译期计算,我们可以这样:
function foo()
for i=1,| config "max" | do
execute(i)
end
end
这个例子里,循环的终值是通过调用 config "max" 得到的,如果每次运行这个程序时都去查询 config 必然影响效率。我们需要在程序加载时,第一次得到 config "max" 的结果即可。
这里,我使用 | 夹在中间 | 表示这段代码应该在加载时运行。
有点像 php 写网页用的模版?呵呵,可以接着往下看。
姑且我们把这种技术叫做代码模版吧,对于 C 程序员,则更接近于宏替换,C++ 程序员看来可能是一个高级 template 技巧。不过 lua 能做的更强一些。
我来演示一下,代码模版的上下文变量。
| ALPHA = math.pi / 4 |
function foo(a)
return a * math.sin(|ALPHA|)
end
这个例子里,一开始给代码模版变量 ALPHA 赋了值为 pi/4 。然后在下面的函数中,使用了这个变量。这段代码经过处理后,会把常量 0.78539816339745 (pi/4) 直接编译进入最终执行的代码。而 ALPHA 这个变量,将只在模版预处理期间可见。
下面再看一个更实用一点的例子。
有时候,我们需要一个增强版的 unpack 。我们知道,lua 自带的 unpack 可以把一直数组(只有连续数字下标的 table)展开成一串返回值,但是对用字符串或别的东西做 key 的 table 无能为力。
function unpackex(tbl,args)
local ret={}
for _,v in ipairs(args) do
table.insert(ret,tbl[v])
end
return unpack(ret)
end
下面我们可以用这个函数展开一个数组,
print ( unpackex( { one=1,two=2,three=3 }, { "one","two","three" } ))
unpackex 将按第 2 个参数表中给出的字符串次序解开第一个参数表。这里我们将看到输出 1,2,3 。
btw, 如果你真的需要一个高效的 unpackex ,推荐用 C 来实现,这样能省掉其中的临时 table ret 。
接下来,我们在使用 unpackex 时,还有可能遇到一个性能问题。因为第二个参数表通常是个常量数组,但由于 lua 的语义,下列函数中的这个常量表,可能在每次进入函数 foo 时构造一个新的出来。
function foo(tbl)
return unpackex(tbl,{"one","two","three"})
end
如果 foo 对性能很敏感,有经验的 lua 程序员或许会这样优化一下:
local const_list={"one","two","three"}
function foo(tbl)
return unpackex(tbl,const_list)
end
但这破坏了代码的直观。如果有了代码模版,我们实际上可以写成这样:
function foo(tbl)
return unpackex(tbl,| {"one","two","three"} |)
end
ok. 现在基本能看出来这个代码模版做了些什么。
它在分析源码字符串时,碰到 | 夹住的部分,将立即运行,获得结果。如果没有结果(例如只是运行一些代码,无返回值),则跳过。如果有结果值(可以由 return 返回,也可以本身是一个表达式),先判断类型是否是一个简单类型(nil ,boolean ,number 或 string ),就把值直接插入源码。如果是一个复杂类型,则创建一个 local 变量记住结果,并将这个 local 变量插入代码。
这里,简单类型 string 会被加上 [=[ 这样的括号插入。而有时候,我们需要直接把字符串插进去。
例如,有时候我们期望 lua 有 C 的 include 那种功能,直接把一文本文件插入源码。
为了效率,我们通常在每个 .lua 文件最前面写上诸如:
local pairs=pairs
local table=table
local string=string
这样的语句。
这可以提高 lua 程序访问内置 api 的效率(减少一次对全局表的查询)。
但每个文件前都写这么一串过于繁琐,也容易出错。如果有 include 功能就好了。那么就让我们实现一个:
function include(filename)
local f=assert(io.open(filename))
local ret=f:read "*a"
f:close()
return ret
end
这个函数可以读入一段文本返回。
代码模版可以通过 |# include "local.lua"|
这样的语法,将 include 函数的返回值插入代码。(注:遇到 | 后紧跟一个 # ,这个区间的返回值就必须是一个 string ,并且这个 string 将被插入代码)
最后,来看看代码模版的实现吧,使用 lua 本身就可以轻易搞定。有兴趣的同学点这里。