lua中的可变长参数...不是第一类值,因此在实际应用中会遇到一些问题
一、可变长参数的存储问题
我们都知道,使用如下写法会导致运行报错,因为...不是一个变量,它是匿名的,不可以在不同的作用域内使用,因为编译器将无法识别...到底是哪一个作用域的;
function tuple(...)
return function()
return ...
end
end
如下是一个例子,我们使用trace对一个传入的函数进行包装,添加了它的执行日志,然后将其执行结果返回,这就需要我们将多返回值存储起来,并在返回时unpack;
如下,我们直接使用{...}和table.unpack的方式,这种方式会有一个问题,缺失了一个nil;这是因为nil并不会显式的存储在table中,尤其是使用{...}方式,它会忽略尾部的nil;
function trace(f)
return function(...)
print('begin', f)
local result = {f(...)}
print('end', f)
return table.unpack(result)
end
end
test = trace(function(x, y, z)
print('calc', x, y, z)
return x + y, z
end)
print('return', test(1, 2, nil))
1.使用{n = n, ...}和unpack with n的方式
既然内置的{...}和table.unpack不能满足需求,就可以自己扩展;
function pack2(...)
return {n = select('#', ...), ...}
end
function unpack2(t)
return table.unpack(t, 1, t.n)
end
function trace(f)
return function(...)
print('begin', f)
local result = pack2(f(...))
print('end', f)
return unpack2(result)
end
end
test = trace(function(x, y, z)
print('calc', x, y, z)
return x + y, z
end)
print('return', test(1, 2, nil))
2.使用nil占位符
如下,给NIL一个占位符,使其能够存储在table中,这样就可以用数组型table来存储可变参数了;
local NIL = {}
print(NIL)
function pack2(...)
local n = select('#', ...)
local t = {}
for i = 1, n do
local v = select(i, ...)
t[i] = (v == nil) and NIL or v
end
return t
end
function unpack2(t)
if #t == 0 then
return
end
local v = table.remove(t, 1)
if v == NIL then
v = nil
end
-- v = (v == NIL) and nil or v
return v, unpack2(t)
end
注意:这里有一个使用a and b or c的被注释掉了,这里不可以这么用,因为只要b为nil,那么该表达式的返回值一定是c;这是因为and和or都是短路求值,多个and连接的操作数返回第一个不为真的数,多个or连接的操作数返回第一个不为假的数;Lua中and、or的一些特殊使用方法;
3.对上面的unpack2进行优化
上面的unpack2只能操作整个table,而如果需要对table的局部进行处理,则需要如下
function unpack2(t, s, e)
s = s or 1
e = e or #t
if e < s then
return
end
local v = table.remove(t, s)
if v == NIL then
v = nil
end
return v, unpack2(t, s, e - 1)
end
function unpack2(t, s, e)
s = s or 1
e = e or #t
if e < s then
return
end
local v = t[s]
if v == NIL then
v = nil
end
return v, unpack2(t, s + 1, e)
end
4.CPS(Continuation Pass Style)方案
Continuation-passing style(CPS)是指将控制流(Control flow)显式的当做参数传递的编程风格;
上述所解决的问题焦点在于如何pack和unpack,那么如果不需要pack和unpack,也就是不需要临时变量存储,而是直接将多返回值作为可变长参数值进行传递;
如下,使用helper函数来执行结尾剩下的逻辑,而把前面逻辑结果作为参数传递给helper函数,使其接替执行;
function trace(f)
local helper = function(...)
print('end', f)
return ...
end
return function(...)
print('begin', f)
return helper(f(...))
end
end
test = trace(function(x, y, z)
print('calc', x, y, z)
return x + y, z
end)
print('return', test(1, 2, nil))
二、连接两个列表
如下所示,三个函数g,f,e分别返回一组值,但是最终打印结果却没有连接起来,因为print可以接收多个参数,但只有最后一个才是可变长参数,多余参数不被抛弃,而其它多返回值除了第一个都被抛弃掉了;
local g = function()
return 1, 2, 3
end
local f = function()
return 4, 5, 6
end
local e = function()
return 7, 8, 9
end
print(g(), f(), e())
-- 1 4 7 8 9
采用了递归方式将所有可变长参数逐个取出返回,可以解决该问题;如下,helper函数为关键,它多了一个参数a,其实就是每次都从可变参数列表中取一个,并且将可变参数数量减一,直到最终数量为0;
function helper(f, n, a, ...)
if n > 0 then
return f()
end
return a, helper(f, n - 1, ...)
end
function combine(f, ...)
local n = select('#', ...)
return helper(f, n, ...)
end
print(combine(function()
return 0, 1, 2
end, 3, 4, 5))
三、选择列表中的前n个数
类似于之前连接两个列表,即将可变长参数的前n个数递归取出即可,每次取出一个;
function helper(n, a, ...)
if n == 0 then
return
end
return a, helper(n - 1, ...)
end
function first(k, ...)
local n = select('#', ...)
k = k or n
k = k < n and k or n
return helper(k, ...)
end
print(first(2, 10, 9, 8, 7))
四、将一个数连接到列表尾部
递归取出可变长参数列表,最后返回要连接的数
function helper(k, n, a, ...)
if n == 0 then
return k
end
return a, helper(k, n - 1, ...)
end
function append(k, ...)
local n = select('#', ...)
return helper(k, n, ...)
end
print(append(2, 5, 6, 7))
五、反转一个列表
这个问题可以直接拿到所有参数,然后倒序打印,但是需要占内存空间;
如果数量很大,内存不够,则可以使用递归的方式;
反转一个列表,即把第一个放到剩余列表的最后,然后对新列表,再把第一个放到最后,依次循环即可;它就相当于使用上面的append函数,每次拼接一个,直到n==0结束;
function helper2(n, a, ...)
if n > 0 then
return append(a, helper2(n - 1, ...))
end
end
function reverse(...)
local n = select('#', ...)
return helper2(n, ...)
end
print(reverse(1, 2, 3))
六、实现map function
传递一个函数,一个参数列表,返回参数列表在函数映射下的所有结果;
function helper(f, n, a, ...)
if n > 0 then
return f(a), helper(f, n - 1, ...)
end
end
function map(f, ...)
return helper(f, select('#', ...), ...)
end
print(map(function(x)
return x *x
end, 1, 2, 3))
七、实现filter function
对参数列表进行过滤筛选:
function helper(f, n, a, ...)
if n > 0 then
if f(a) then
return a, helper(f, n - 1, ...)
else
return helper(f, n - 1, ...)
end
end
end
function grep(f, ...)
return helper(f, select('#', ...), ...)
end
print(grep(function(x)
return x % 2 == 0
end, 1, 2, 3, 4))
八、遍历可变长参数列表
最简单的方式就是使用select循环遍历;
如果参数中确定没有nil,也可以使用ipairs;
最复杂的就是扩展一个vararg迭代器,用于遍历可变长参数列表;
function variter(...)
for n = 1, select('#', ...) do
local v = select(n, ...)
print(v)
end
end
function variter2(...)
for _, v in ipairs({...}) do
print(v)
end
end
function vararg(...)
local i, t, l = 0, {}
l = select('#', ...)
for n = 1, l do
t[n] = select(n, ...)
end
return function()
i = i + 1
if i <= l then
return i, t[i]
end
end
end
function variter3(...)
for k, v in vararg(...) do
print(k, v)
end
end
variter('a', 'b', nil, 'c')
variter2('a', 'b', nil, 'c')
variter3('a', 'b', nil, 'c')
九、归纳总结
主要思路有以下几种:
使用n给列表计数,然后unpack时就不会丢失尾部的nil;
使用递归的方式来逐个处理可变长参数列表;