第五章 VimL 函数进阶
5.2 函数引用
关于函数引用的帮助文档先给传送门 :h FuncRef
(注意大写)。很多函数的高级用法 都在函数引用基础上建立的。
函数引用的意义
继续接着上一节的内容引申来讲。例如在 Calculate()
函数中间接调用 Sum()
时须 用如下语法: call('Sum', a:000)
,'Sum'
函数名须用引号括起来当作一个字符串 参数传入。
如果尝试执行 :echo call(Sum, [1,2,3,4])
就会报 E121
的“未定义变量”错误。也 就是说,Sum
是一个自己定义的函数,但函数与变量在 VimL 中有本质的不同,而 call()
要求一个变量作为参数,所以不能直接将函数传入。然而这个变量又要求能代 表函数,所以 VimL 就需要一个“函数引用”的概念。
就这个特殊的 call()
而言,在第一参数中将一个函数名用引号括起的字符串也能达到 引用一个函数的目的,但这显然是不正式不通用的。函数引用也是一个变量,不过是另一 种特殊的变量(值)类型,不应该与简单的字符串变量类型混淆。
在其他一些编程(脚本)语言中,函数是所谓的一等公民,即与变量的地位一样,可以用 变量的地方,也可以用函数。但在 VimL 设计之初,函数与变量两个不同次元的东西。只 有在引入了函数引用之后,函数引用与变量才是相同的东西。
函数引用的定义
可以内置函数 function()
创建一个函数引用,其参数就是所要引用的函数名(引号字 符串),即可以是内置函数也可以是自定义函数的名字。例如:
: let Fnr_Sum = function('Sum')
: echo type(Fnr_Sum)
: echo Fnr_Sum(1,2,3,4)
上例创建一个变量 Fnr_Sum
,它引用自定义函数 Sum()
。查看这个变量的类型,显示 是 2
,这就是函数引用的类型(v:t_func
)。然后这个函数引用可以像原来那个一样 调用,也就是后面接括号传入参数列表。
函数引用变量与函数本身的关系,就与之前所述的列表(或字典)变量与列表(或字典) 实体之间的关系。在常规运用场合中,一般可不必理会其中的差异,凡是要求函数调用的 地方,都可以用函数引用代替。而且,函数引用作为一个变量,使用范围将更加灵活。因 为 VimL 的变量是弱类型的,在使用变量时不检查变量类型,所以在任何使用变量的地方 ,也都可以使用函数引用代替。当然,你不能试图对函数使用进行加减乘除这样的操作, 那会触发运行时错误,函数引用主要(也许是唯一)支持的操作就是调用。
VimL 的变量名自有其规则(见第二章),而函数引用的变量名在此规则上还有更严格一 点的限制,就是也必须也以大写字母开头。这是因为要与函数名的规则吻合。因为从代码 语法上看一个函数调用,无从分辨它是函数引用还是函数本身。主要注意如下几点:
- 函数引用变量也可以加作用域前缀,如果加了
s:
w:
t:
或t:
这几个前缀, 则不再要求变量名主体以大写字母开始了,因为这种情况下不会有歧义。参数作用域前 缀a:
用于函数引用之前,也不必大写字母。 - 如果在函数引用变量名之前加全局作用域前缀
g:
或局部作用域前缀l:
,仍然要 求其变量名主体以大写字母开头。因为这两种前缀是可以省略的,要保证省略后的等价 的“裸”调用仍然合乎函数调用规则。 - 函数引用变量名,不能与已有的自定义函数名相同,否则也会发生歧义,vim 将无从分 辨是触发调用函数引用呢,还是触发调用同名函数本身。
- 函数引用变量名允许与已存在的其他变量名重名,只不过其含义是重定义或覆盖原变量 的意义,虽然语法上合法,但不建议这么做。
再次提醒一下,function
这个“关键字”,即是一个命令名,也是一个内置函数名。用 :function
命令是创建或定义一个函数,则 function()
函数则是创建或定义一个函 数引用(其参数须是已由 :function
命令创建的函数名,或内置函数名)。命令与函 数是完全不同空间次元的东西,也与变量互不相关。如果你愿意,甚至也可以自定义一个 叫 function
的变量,但最好不要这样做。
在 VimL 中,有很多内置函数与命令重名,用于实现相似的功能。上节刚用到过的 call()
函数与 :call
命令也是这种情况。在查 vim 帮助文档时,查函数时在后面 加对空括号,查命令时在前面加个冒号。另一方面,VimL 的内置变量名都是以 v:
前 缀的,这倒不必担心混淆。
函数引用的使用
下面再讲解函数引用的使用建议与示例。仍以上节末用于实现不定参数连加或连乘的 Calculate()
函数为例。
将函数引用作为参数传递
首先,不建议使用全局的函数引用变量。因为用 :function
命令定义的函数是全局的 ,尽量不要将函数引用也定义在全局作用域中,避免麻烦。例如,可将上节的 Calculate()
函数改为如下使用函数引用的方式(为简便起见,略过参数检测):
function! CalculateR(operator, ...)
if a:operator ==# '+'
let l:Fnr = function('Sum')
elseif a:operator ==# '*'
let l:Fnr = function('Prod')
endif
let l:result = call(l:Fnr, a:000)
return l:result
endfunction
这里先根据参数一创建一个函数引用 Fnr
,在函数内定义的变量都是局部变量,l:
前缀可选。然后这个函数引用也可以作为参数传给 call()
函数,它能同时处理作为函 数名的字符串变量类型或函数引用类型,反正都是用以访问实际所调函数的手段;也不妨 认为在之前传入字符串时,call()
函数也会自动先调用 function()
获得函数引用 。
脚本局部函数及引用
上面改写的 CalculateR()
函数有一处不太好,就是每次调用都要重新创建 l:Fnr
这个相同的函数引用变量,略显低效。在实践中,函数定义一般是写在单独的脚本中,因 此函数引用也可以定义为 s:
脚本局部变量。例如:
" File: ~/.vim/vimllearn/funcref.vim
let s:fnrSum = function('Sum')
let s:fnrProd = function('Prod')
function! CalculateRs(operator, ...)
if a:operator ==# '+'
let l:Fnr = s:fnrSum
elseif a:operator ==# '*'
let l:Fnr = s:fnrProd
endif
let l:result = call(l:Fnr, a:000)
return l:result
endfunction
注意,如前所述,s:
前缀的函数引用变量可用小写开头,l:
或缺省前缀的函数引用 须大写开头。这里主要为演示不同前缀的函数函数引用变量,其实 l:Fnr
中间变量也 可省去,直接将 call()
调用语用写在 if
分支中。
这样,s:fnrSum
与 s:fnrProd
函数(引用)就是私有的了,只能在该脚本内使用, 而 CalculateRs()
函数仍定义为全局函数,提供为外部公用接口。但是,那两个私有 变量引用的仍是公用的函数 Sum()
与 Prod()
。如果想再要隐藏,可以将这两个函数 也定义为 s:
的作用域:
" File: ~/.vim/vimllearn/funcref.vim
function! s:sum(...)
let l:sum = 0
for l:arg in a:000
let l:sum += l:arg
endfor
return l:sum
endfunction
function! s:prod(...)
let l:prod = 1
for l:arg in a:000
let l:prod = l:prod * l:arg
endfor
return l:prod
endfunction
let s:fnrSum = function('s:sum')
let s:fnrProd = function('s:prod')
echo s:
这里的 s:sum()
函数对原 Sum()
略有修改,不再强制要求至少两个参数。同时函数 名加上 s:
前缀后,也不再强制要求以大定字母开头。当用 function()
创建函数引 用时,须将 's:sum'
整个字符串当作该脚本局部数字的“名字”传入为参数。
然后,重点迷惑来了,脚本内的 s:sum()
实际函数名其实并不是 's:sum'
!这只是 语法上规定的书写文法。在 vim 内部,会将 s:
前缀的函数名替换为 <SNR>编号_
。 其中编号是指 vim 在加载该文件时对其赋与的编号。可用 :scriptnames
命令查看当 前 vim 所加载过的所有脚本,一般情况下编号为 1
的第一个加载文件就是你的起始配 置文件 vimrc
,然后每次加载脚本时顺序编号。所以 s:sum()
脚本私有函数的实际 名字是动态变化的,在不同的 vim 会话中加载时机极可能不一样,其编号中缀也就不一 样了。
如果在脚本末尾加上 echo s:
这个语句(s:
是一个特殊字典,保存着该脚本内定义 的所有以 s:
前缀开始的脚本局部变量),那么在加载该脚本时,将回显如下信息:
{'fnrSum': function('<SNR>77_sum'), 'fnrProd': function('<SNR>77_prod')}
表明在这次 vim 会话环境中,s:sum()
函数名实际上是 <SNR>77_sum
,也可以直接 用这个名字来调用该函数,如在命令行中输入
: echo <SNR>77_sum(1, 2, 3, 4)
是能正常工作中的。
因此,看似脚本局部私有的 s:sum()
实际上是被转化成了 <SNR>77_sum()
全局公有 函数。其中 <SNR>77_
前缀在在某些地方也可用特殊符号 <SID>
表示。当然,任何 正常的人,都不会采用后者来调用函数,况且脚本编号都是临时赋与的不保存一致性,于 是也算达到了作用域隐藏的目的。
另外,还有一点要注意的是,s:sum()
是函数,不是变量,所以它不会被保存在 s:
字典内。只有函数引用变量 s:fnrSum
与 s:fnrProd
才保存在 s:
字典内,其键 就是变量名 fnrSum
与 fnrProd
,其值就是相应的函数引用。显然,vim 不能自作主 张地自动为 s:sum()
创建一个名为 s:sum
的函数引用变量,甚至我们自己也不能手 动用 :let ... function()
语句创建名为 s:sum
的函数引用变量,否则在调用 s:sum(1,2,3,4)
是就会发生语法歧义。但是,我们能用它创建其他类型的变量,如在 脚本末尾加入如下代码并重新用 :source
加载:
" File: ~/.vim/vimllearn/funcref.vim
" let s:sum = function('s:sum') " 错误
" let s:prod = function('s:prod')
let s:sum = '1+2+3+4'
let s:prod = '1*2*3*4'
echo s:
echo s:sum(1,2,3,4)
echo s:prod(1,2,3,4)
可以把 s:sum
赋值为字符串类型变量,然后 s:sum()
函数并未失去定义,仍然可正 常调用。所以,s:
作用域前缀用于变量与函数前有着不同的实现意义。s:sum()
函 数本质上是 <SNR>77_sum()
函数,与 s:sum
变量大有不同。然而,正常的程序猿非 常不建议玩这样的杂耍。
将函数引用收集在列表中
在前一示例中,在脚本中创建的 s:
前缀的函数引用变量,被自动地收集保存在一个特 殊字典中。这表明函数引用与普通变量“无差别”的同等地位,可以用在任何需要变量的地 方。比如,我们也可以主动地将函数引用保存在一个列表中,以实现某些特殊功能:
" File: ~/.vim/vimllearn/funcref.vim
let s:operator = [function('s:sum'), function('s:prod')]
function! CalculateA(...)
for l:Operator in s:operator
let l:result = call(l:Operator, a:000)
echo l:result
endfor
endfunction
这里,我们定义了一个列表变量 s:operator
,其元素都是能接收不定参数的运算函数 的引用。然后在函数 CalculateA()
中遍历该列表,为每个函数传递参数进行计算。这 是个全局函数,所以加载脚本后,可直接在命令行中执行 :call CalculateA(1,2,3,4)
验看结果。
仍然要注意的是,在 for
循环中,循环变量 l:Operater
仍然要以大写字母开头, 才能接收 s:operator
列表内的函数引用变量。否则,若以小写字母的话,有可能省去 l:
前缀,写出类似 operator(1,2,3,4)
的函数调用,这就有语法错误了,因为小写 字母的函数名调用,都保留给 VimL 的内置函数。
良好的实践是,始终以大写字母开头命名函数引用变量,不管什么作用域前缀;如果不嫌 麻烦,再以 Fnr
为变量名前缀也未尝不可。