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;

使用递归的方式来逐个处理可变长参数列表;