5.1 可变参数

第五章 VimL 函数进阶 #

在第二章中,我们已经讲叙了基本的函数定义与调用方法,以及一些函数属性的作用。但 正如大多数编程语言一样,函数是如此普遍且重要的元素。因而本章继续讨论一些有关函 数的较为高级的用法。

5.1 可变参数 #

可变参数的意义 #

一般情况下,在定义函数时指定形参,在调用函数时传入实参,且参数个数必须要与定义 时指定的参数数量相等。但在一些情况下,我们将要实现的函数功能,它的参数个数可能 是不确定,或者有些参数是可选可缺省使用默认值。这时,在函数定义中引入可变参数就 非常方便了。相对于可变参数,常规的形参也就命名参数。

  • 在函数头中,用三个点号 ... 表示可变参数,可变参数必须用于最后一个形参,如 果有其他命名参数,则必须位于 ... 之前。
  • 在函数体中,分别用 a:1 a:2 …… 等表示第一个、第二个可变参数。用 a:0 表 示可变参数的数量,a:000 是由所有可变参数组成的列表变量。
  • 命名参数最多允许 20 个,虽然大部分情况也够用了。可变参数的数量没有明确限制。
  • 调用函数时,传入的实参数量至少不低于命名参数的数量,但传入的可变参数数量可以 为 0 或多个。当没有传入可变参数时,a:0 的值为 0。

需要强调的是,只有定义了 ... 可变参数,才能在函数体中使用 a:0 a:000 a:1 等特殊变量。较好的实践是先用 a:0 判断可变参数个数,然后视情况使用 a:1 a:2 等每个可变参数。如果只传入一个实参,与使用于 a:2 变量,会发生运行时错误。此 外 a:000 就当作普通列表变量使用好了,a:000[0] 就是 a:1,因为列表元素索引 从 0 开始。

例如,可用以下函数展示可变参数的使用方法:

function! UseVarargin(named, ...)
    echo 'named argin: ' . string(a:named)

    if a:0 >= 1
        echo 'first varargin: ' . string(a:1)
    endif
    if a:0 >= 2
        echo 'second varargin: ' . string(a:2)
    endif

    echo 'have varargin: ' . a:0
    for l:arg in a:000
        echo 'iterate varargin: ' . string(l:arg)
    endfor
endfunction

你可以用 :call 调用这个函数,尝试传入不同的参数,观察其输出。可见有两种写法 获取某个可变参数,比如用 a:1a:000[0],视业务具体情况用哪种更方便。而且 a:000 还可用列表迭代方法获取每个可变参数。

不定参数示例 #

在 2.4 节,我们已经定义了一个演示之用的函数 Sum 可计算两个数之和,简化重新 截录于下:

function! Sum(x, y)
    let l:sum = a:x + a:y
    return l:sum
endfunction

现假设要计算任意个数之和,则可改为如下定义:

function! Sum(x, y, ...)
    let l:sum = a:x + a:y
    for l:arg in a:000
        let l:sum += l:arg
    endfor
    return l:sum
endfunction

这里认为调用 Sum() 时必须提供两个参数,否则求和没有意义。其实也可以定义为 Sum(...),将函数实现中的 l:sum 初始化为 0 即可。

若一个函数用 Fun(...) 定义,只声明了可变参数,则可用任意个参数调用,非常通用 。然而过于通用也表明意义不明确,良好的实践是,除非有必要,尽可能用命名参数,少 用可变参数。使用合适的参数变量名,函数的可读性增强,使用可变参数时,最好加以注 释;同时也建议在函数前面部分判断可变参数数量与数量,第一时间分别赋于另外的局部 变量,也能增加函数的可读性。

调用这个求和函数时,用 :call Sum(1, 2, 3, 4) 方式。事实上,只为这个需求的话 ,不必用可变参数,直接用一个列表变量作为参数可能更方便。如改写为:

function! SumA(args)
    let l:sum = 0
    for l:arg in a:args
        let l:sum += l:arg
    endfor
    return l:sum
endfunction

这个函数的意义是为一个列表变量内所有元素求和,以 :call Sum([1, 2, 3, 4]) 方 式调用。

然而需要注意的是,并非所有用可变参数的函数,都适合将可变参数改为一个列表变量。

默认参数示例 #

在 VimL 的内置函数中,格式化字符串的 printf() 就是接收任意个参数的例子。另外 还有大量内置函数是支持默认参数的,如将列表所有元素连接成一个字符串的 join() 。这种情况与不定参数略有不同,它能接收的有效参数个数是确实的,只是在调用时后面 一个或几个参数可以省略不传,不传实参的话就自动采用了某个默认值而已。

比如我们也可以自己实现一个类似的函数 Join():

function! Join(list, ...)
    if a:0 > 0
        let l:sep = a:1
    else
        let l:sep = ','
    endif
    return join(a:list, l:sep)
endfunction

虽然可以(更低效率)用循环连接字符串,但这时为简明说明问题,直接调用内置的 join() 完成实际工作了。关键点是提供了另一个逗号作为默认分隔字符,通过 a:0 来判断传入的可变参数个数,再给分隔字符赋以合适的初始值。 其实这个 if 分 支可以直接用 get() 函数代替:let l:sep = get(a:000, 0, ',')。 这用起来更为 简洁,不过用 if 分支明确写出来,更容易扩充其他逻辑,即使是用 echo 打印个简 单的日志。

间接调用含可变参数的函数 #

一般情况下,函数都不是独立完成工作的,往往还需要调用其他的函数。假如一个支持可 变参数的函数内,要调用另一个支持可变参数的函数,给后者传递的参数依赖于前者接收 的不确定的参数,这情况就似乎变得复杂了。

为说明这种应用场景,先参照上述 Sum() 函数再定义一个类似的连乘函数:

function! Prod(x, y, ...)
    let l:prod = a:x * a:y
    for l:arg in a:000
        let l:prod = l:prod * l:arg
    endfor
    return l:prod
endfunction

注:VimL 支持 += 操作符,却不支持 *= 操作符,请参阅 :h +=

然后再定义一个更上层的函数,根据一个参数分发调用连加 Sum() 或 连乘 Prod() 函数,传入剩余的不定参数:

function! Calculate(operator, ...)
    echo Join(a:000, a:operator)
    if a:operator ==# '+'
        " let l:result = Sum(...)
        " let l:result = Sum(a:000)
    elseif a:operator ==# '*'
        " let l:result = Prod(...)
        " let l:result = Prod(a:000)
    endif
    return l:result
endfunction

echo Calculate('+', 1, 2, 3, 4)
echo Calculate('*', 1, 2, 3, 4)

在这个示例函数中,第一行的 echo 语句用于调试打印,不论是用刚才自定义的 Join() 或内置的 join() 函数都能正常工作。但是在随后的 if 分支中, 不论是 Sum(...) 还是 Sum(a:000) 都不能达到预期效果,虽然它作为“伪代码” 很好地表达了使用意途,所以先将其注释了。

先分析原因,Sum(...) 是语法错误。因为 ... 只能用于函数头表示不定参数,却不 能在函数体中表示接收的所有不定参数。a:000 可以表示所有不定参数,但它只是一个 列表变量,调用 Sum(a:0000) 时只传了一个参数变量,而原来定义的 Sum() 函数要 求至少两个参数,所以也会出错误,因为相当于调用 Sum([1,2,3,4]) 也是错误的。

解决办法是用 call() 函数间接调用,它的第一个参数是一个函数,第二个参数正是一 个列表,这个列表内的所有元素将传入第一个参数所代表的函数进行调用。例如,这语句 :echo call('Sum', [1,2,3,4]) 能正常工作。于是可将 Calculate() 函数改写:

function! Calculate(operator, ...)
    if a:0 < 2
        echoerr 'expect at leat 2 operand'
        return
    endif

    echo Join(a:000, a:operator)
    if a:operator ==# '+'
        let l:result = call('Sum', a:000)
    elseif a:operator ==# '*'
        let l:result = call('Prod', a:000)
    endif

    return l:result
endfunction

这里再作了另一个优化,先对不定参数个数作了判断,不足 2 个时返回错误。