You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
249 lines
12 KiB
249 lines
12 KiB
2 years ago
|
# 第五章 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()` 创建一个函数引用,其参数就是所要引用的函数名(引号字
|
||
|
符串),既可以是内置函数也可以是自定义函数的名字。例如:
|
||
|
```vim
|
||
|
: 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()` 函数改为如下使用函数引用的方式(为简便起见,略过参数检测):
|
||
|
|
||
|
```vim
|
||
|
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:` 脚本局部变量。例如:
|
||
|
|
||
|
```vim
|
||
|
" 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:` 的作用域:
|
||
|
|
||
|
```vim
|
||
|
" 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:` 前缀开始的脚本局部变量),那么在加载该脚本时,将回显如下信息:
|
||
|
|
||
|
```vim
|
||
|
{'fnrSum': function('<SNR>77_sum'), 'fnrProd': function('<SNR>77_prod')}
|
||
|
```
|
||
|
表明在这次 vim 会话环境中,`s:sum()` 函数名实际上是 `<SNR>77_sum`,也可以直接
|
||
|
用这个名字来调用该函数,如在命令行中输入
|
||
|
```vim
|
||
|
: 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` 加载:
|
||
|
|
||
|
```vim
|
||
|
" 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:` 前缀的函数引用变量,被自动地收集保存在一个特
|
||
|
殊字典中。这表明函数引用与普通变量“无差别”的同等地位,可以用在任何需要变量的地
|
||
|
方。比如,我们也可以主动地将函数引用保存在一个列表中,以实现某些特殊功能:
|
||
|
|
||
|
```vim
|
||
|
" 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` 为变量名前缀也未尝不可。
|