5.2 函数引用

优质
小牛编辑
126浏览
2023-12-01

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:fnrSums: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:fnrSums:fnrProd 才保存在 s: 字典内,其键 就是变量名 fnrSumfnrProd,其值就是相应的函数引用。显然,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 为变量名前缀也未尝不可。