5.1 可变参数
在第二章中,我们已经讲叙了基本的函数定义与调用方法,以及一些函数属性的作用。但 正如大多数编程语言一样,函数是如此普遍且重要的元素。因而本章继续讨论一些有关函 数的较为高级的用法。
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:1
或 a: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 个时返回错误。