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.
356 lines
18 KiB
356 lines
18 KiB
2 years ago
|
# 第二章 VimL 语言基本语法
|
||
|
|
||
|
## 2.4 函数定义与使用
|
||
|
|
||
|
函数是可重复调用的一段程序单元。在用程序解决一个比较大的功能时,知道如何拆分多
|
||
|
个小功能,尤其是多次用到的辅助小功能,并将它们独立为一个个函数,是编程的基本素
|
||
|
养吧。
|
||
|
|
||
|
### VimL 函数语法
|
||
|
|
||
|
在 VimL 中定义函数的语法结构如下:(另参考 `:help :function`)
|
||
|
```vim
|
||
|
function[!] 函数名(参数列表) 附加属性
|
||
|
函数体
|
||
|
endfunction
|
||
|
```
|
||
|
|
||
|
在其他地方调用函数时一般用 `:call` 命令,这能触发目标函数的函数体开始执行,以
|
||
|
产生它所设计的功效。如果要接收函数的返回值,则不宜用 `:call` 命令,可用 `:echo`
|
||
|
观察函数的返回结果,或者用 `:let` 定义一个变量保存函数的返回结果。实际上,函数
|
||
|
调用是一个表达式,任何需要表达式的地方,都可植入函数调用。例如:
|
||
|
|
||
|
```vim
|
||
|
call 函数名(参数)
|
||
|
echo 函数名(参数)
|
||
|
let 返回值 = 函数名(参数)
|
||
|
```
|
||
|
|
||
|
注:这里为了阐述方便,除了关键命令,直接用中文名字描述了。因而不是有效代码,在
|
||
|
每行的前面也就不加 `:` 了。
|
||
|
|
||
|
### 函数名
|
||
|
|
||
|
函数名的命令规则,除了要遵循普通变量的命令规则外,还有条特殊规定。如果函数是在
|
||
|
全局作用域,则只能以大写字母开头。
|
||
|
|
||
|
因为 vim 内建的命令与函数都以小写字母开始,而且随着版本提升,增加新命令与函数
|
||
|
也是司空见惯的事。所以为了方便避免用户自定义命令与函数的冲突,它规定了用户定义
|
||
|
命令与函数时必须以大写字母开头。从可操作 Vim 的角度,函数与命令在很大程度上是
|
||
|
有些相似功能的。当然,如果将 VimL 视为一种纯粹的脚本语言,那函数也可以做些与
|
||
|
Vim 无关的事情。
|
||
|
|
||
|
习惯上,脚本中全局变量时会加 `g:` 前缀,但全局函数一般不加 `g:` 前缀。全局函数
|
||
|
是期望用户可以直接从命令行用 `:call` 命令调用的,因而省略 `g:` 前缀是有意义的
|
||
|
。当然更常见的是将函数调用再重映射为自定义命令或快捷键。
|
||
|
|
||
|
除了接口需要定义在全局作用域的函数外,其他一些辅助与实现函数更适合定义为脚本作
|
||
|
用域的函数,即以 `s:` 前缀的函数,此时函数名不强制要求以大写字母开头。毕竟脚
|
||
|
本作用域的函数,不可能与全局作用域的内建函数冲突了。
|
||
|
|
||
|
### 函数返回值
|
||
|
|
||
|
函数体内可以用 `:return` 返回一个值,如果没有 `:return` 语句,在函数结束后默认
|
||
|
返回 `0`。请看以下示例:
|
||
|
```vim
|
||
|
: function! Foo()
|
||
|
: echo 'I am in Foo()'
|
||
|
: endfunction
|
||
|
:
|
||
|
: let ret = Foo()
|
||
|
: echo ret
|
||
|
```
|
||
|
|
||
|
你可以将这段代码保存在一个 `.vim` 脚本文件中,然后用 `:source` 加载执行它。如
|
||
|
果你也正在用 vim 读该文档,可以用 `V` 选择所有代码行再按 `y` 复制,然后在命令
|
||
|
行执行 `:@"`,这是 Vim 的寄存器用法,这里不准备展开详述。如果你在用其他工具读
|
||
|
文档,原则上也可以将代码复制粘贴至 vim 的命令行中执行,但从外部程序复制内容至
|
||
|
vim 有时会有点麻烦,可能还涉及你的 `vimrc` 配置。因此还是复制保存为 `.vim` 文
|
||
|
件再 `:source` 比较通用。
|
||
|
|
||
|
这段示例代码执行后,会显示两行,第一行输出表示它进到了函数 `Foo()` 内执行了,
|
||
|
第二行输出表明它的默认返回值是 `0`。这个默认返回值的设定,可以想像为错误码,当
|
||
|
函数正常结束时,返回 `0` 是很正常的事。
|
||
|
|
||
|
当然,根据函数的设计需求,可以显式地返回任何表达式或值。例如:
|
||
|
```vim
|
||
|
: function! Foo()
|
||
|
: return range(10)
|
||
|
: endfunction
|
||
|
:
|
||
|
: let ret = Foo()
|
||
|
: echo ret
|
||
|
```
|
||
|
执行此例将打印出一个列表,这个列表是由函数 `Foo()` 生成并返回的。
|
||
|
|
||
|
注意一个细节,这里的 `:function!` 命令必须加 `!` 符号,因为它正在重定义原来存
|
||
|
在的 `Foo()` 函数。如果没有 `!` ,vim 会阻止你重定义覆盖原有的函数,这也是一种
|
||
|
保护机制吧。用户加上 `!` 后,就认为用户明白自己的行为就是期望重定义同名函数。
|
||
|
|
||
|
一般在写脚本时,在脚本内定义的函数,建议始终加上 `!` 强制符号。因为你在调试时
|
||
|
可能经常要改一点代码后重新加载脚本,若没有 `!` 覆盖指令,则会出错。然后在脚本
|
||
|
调试完毕后,函数定义已定稿的情况下,假使由于什么原因也重新加载了脚本,也不外是
|
||
|
将函数重定义为与原来一样的函数而已,大部分情况下这不是问题。(最好是在正常使用
|
||
|
脚本时,能避免脚本的重新加载,这需要一些技巧)
|
||
|
|
||
|
不过这需要注意的是,避免不同脚本定义相同的全局函数名。
|
||
|
|
||
|
### 函数参数
|
||
|
|
||
|
在函数定义时可以在参数表中加入若干参数,然后在调用时也须使用相同数量的参数:
|
||
|
|
||
|
```vmi
|
||
|
: function! Sum(x, y)
|
||
|
: return a:x + a:y
|
||
|
: endfunction
|
||
|
|
||
|
: let x = 2
|
||
|
: let y = 3
|
||
|
: let ret = Sum(x, y)
|
||
|
: echo ret
|
||
|
```
|
||
|
在本例中定义了一个简单的求和函数,接收两个参数;然后调用者也传入两个参数,运行
|
||
|
结果毫无惊喜地得到了结果 `5` 。
|
||
|
|
||
|
这里必须要指出的是,在函数体内使用参数 `x` 时,必须加上参数作用域前缀 `a:`,即
|
||
|
用 `a:x` 才是参数中的 `x` 形参变量。`a:x` 与函数之外的 `x` 变量(实则是 `g:x`
|
||
|
)毫无关系,如果在函数内也创建了个 `x` 变量(实则是 `l:x`),`a:x`与之也无关系
|
||
|
,他们三者是互不冲突相扰的变量。
|
||
|
|
||
|
参数还有个特性,就是在函数体内是只读的,不能被重新赋值。其实由于函数传参是按值
|
||
|
传递的。比如在上例中,调用 `Sum(x, y)` 时,是把 `g:x` 与 `g:y` 的值分别拷贝给
|
||
|
参数 `a:x` 与 `a:y` ,你即使能对 `a:x` `a:y` 作修改,也不会影响外面的 `g:x`
|
||
|
`g:y`,函数调用结束后,这种修改毫无影响。然而,VimL 从语法上保证了参数不被修改
|
||
|
,使形参始终保存着当前调用时实参的值,那是更加安全的做法。
|
||
|
|
||
|
为了更好地理解参数作用域,改写上面的代码如下:
|
||
|
```vmi
|
||
|
: function! Sum(x, y)
|
||
|
: let x = 'not used x'
|
||
|
: let y = 'not used y'
|
||
|
:
|
||
|
: echo 'g:x = ' . g:x
|
||
|
: echo 'l:x = ' . l:x
|
||
|
: echo 'a:x = ' . a:x
|
||
|
: echo 'x = ' . x
|
||
|
:
|
||
|
: let l:sum = a:x + a:y
|
||
|
: return l:sum
|
||
|
: endfunction
|
||
|
|
||
|
: let x = 2
|
||
|
: let y = 3
|
||
|
: let ret = Sum(-2, -3)
|
||
|
: echo ret
|
||
|
```
|
||
|
在这个例子中,调用函数 `Sum()` 时,不再传入全局作用域的 `x` `y` 了,另外传入两
|
||
|
个常量,然后在函数体内查看各个作用域的 `x` 变量值。
|
||
|
|
||
|
结果表明,在函数体内,直接使用 `x` 代表的是 `l:x`,如果在函数内没定义局部变量
|
||
|
`x`,则使用 `x` 是个错误,它也不会扩展到全局作用域去取 `g:x` 的值。如果要在函
|
||
|
数内使用全局变量,必须指定 `g:` 前缀,同样要使用参数也必须使用 `a:` 前缀。
|
||
|
|
||
|
虽然在函数体内默认的变量作用域就是 `l:` ,但我还是建议在定义局部变量时显式地
|
||
|
写上 `l:`,就如定义 `l:sum` 这般。虽然略显麻烦,但语义更清晰,更像 VimL 的风格
|
||
|
。函数定义一般写在脚本文件,只用输入一次,多写两字符不多的。
|
||
|
|
||
|
至于脚本作用域变量,读者可自行将示例保存在文件中,然后也创建 `s:x` `s:y` 变量
|
||
|
试试。当然了,在正常的编程脚本中,请不要故意在不同作用域创建同名变量,以避免不
|
||
|
必要的麻烦。(除非在某些特定情境下,按设计意图有必要用同名变量,那也始终注意加
|
||
|
上作用域前缀加以区分)
|
||
|
|
||
|
### 函数属性:abort
|
||
|
|
||
|
VimL 在定义函数时,在参数表括号之后,还可以选择指定几个属性。虽然在帮助文档
|
||
|
`:help :function` 中也称之为 `argument`,不过这与在调用时要传入的参数是完全不
|
||
|
同的东西。所以在这我称之为函数属性。文档中称之为 `argument` 是指它作为
|
||
|
`:function` 这个 `ex 命令` 的参数,就像我们要定义的函数名、参数表也是这个命令
|
||
|
的 “参数”。
|
||
|
|
||
|
至 Vim8.0 ,函数支持以下几个特殊属性:
|
||
|
|
||
|
* `abort`,中断性,在函数体执行时,一旦发现错误,立即中断运行。
|
||
|
* `range`,范围性,函数可隐式地接收两个行地址参数。
|
||
|
* `dict`, 字典性,该函数必须通过字典键来调用。
|
||
|
* `closure`,闭包性,内嵌函数可作为闭包。
|
||
|
|
||
|
其中后面两个函数属性涉及相对高深的话题,留待第五章的函数进阶继续讨论。这里先只
|
||
|
讨论前两个属性。
|
||
|
|
||
|
为理解 `abort` 属性,我们先来看一下,vim 在执行命令时,遇到错误会怎么办?
|
||
|
```vim
|
||
|
: echomsg 'before error'
|
||
|
: echomsg error
|
||
|
: echomsg 'after error'
|
||
|
```
|
||
|
在这个例子中,第二行是个错误,因为 `echo` 要求表达式参数,但 `error` 这个词是
|
||
|
未定义变量。这里用 `echomsg` 代替 `echo` 是因为 `echomsg` 命令的输出会保存在
|
||
|
vim 的消息区,此后可以用 `:message` 命令重新查看;而 `echo` 只是临时查看。
|
||
|
|
||
|
将这几行语句写入一个临时脚本,比较 `~/.vim/vimllearn/cmd.vim` ,然后用命令加载
|
||
|
`:source ~/.vim/vimllearn/cmd.vim` 。结果表明,虽然第二行报错了,但第三行仍然
|
||
|
执行了。
|
||
|
|
||
|
不过,如果在 vim 下查看该文档,将这几行复制到寄存器中,再用 `:@"` 运行,第三行
|
||
|
语句就似乎不能被执行到了。然而这不是主流用法,可先不管这个差异。
|
||
|
|
||
|
然后,我们将错误语句放在一个函数中,看看怎样?
|
||
|
|
||
|
```vim
|
||
|
: function! Foo()
|
||
|
: echomsg 'before error'
|
||
|
: echomsg error
|
||
|
: echomsg 'after error'
|
||
|
: endfunction
|
||
|
:
|
||
|
: echomsg 'before call Foo()'
|
||
|
: call Foo()
|
||
|
: echomsg 'after call Foo()'
|
||
|
```
|
||
|
将这个示例保存在 `~/.vim/vimllearn/t_abort1.vim`,然后 `:source` 运行。结果错
|
||
|
误之后的语句也都将继续执行。
|
||
|
|
||
|
在函数定义行末加上 `abort` 参数,改为:
|
||
|
```vim
|
||
|
: function! Foo() abort
|
||
|
```
|
||
|
重新 `:source` 执行。结果表明,在函数体内错误之后的语句不再执行,但是调用这个
|
||
|
出错函数之后的语句仍然执行。
|
||
|
|
||
|
现在你应该明白 `abort` 这个函数属性的意义了。一个良好的编程习惯是,始终在定义函数时
|
||
|
加上这个属性。因为一个函数我们期望它执行一件相对完整独立的工作,如果中间出错了
|
||
|
,为何还有必要继续执行下去。立即终止这个函数,一方面便于跟踪调试,另一方面避免
|
||
|
在错误的状态下继续执行可能造成的数据损失。
|
||
|
|
||
|
那为什么 vim 的默认行为是容忍错误呢?想想你的 `vimrc` ,如果中间某行不慎出错了
|
||
|
,如果直接终止运行脚本,那你的初始配置可能加载很不全了。Vim 在最初提供函数功能
|
||
|
,可能也只是作为简单的命令包装重用,所以延续了这种默认行为。但是当 VimL 的函数
|
||
|
功能可以写得越来越复杂时,为了安全性与调试,立即终止的 `abort` 行为就很有必要
|
||
|
了。
|
||
|
|
||
|
如果你写的某个函数,确实有必要利用容忍错误这个默认特性,当然你可以选择不加
|
||
|
`abort` 这个属性。不过最好还是重新想想你的函数设计,如果真有这需求,是否直接写
|
||
|
在脚本中而不要写在函数中更合适些。
|
||
|
|
||
|
### \*函数属性:range
|
||
|
|
||
|
函数的 `range` 属性,表明它很好地继承了 Vim 风格,因为很多命令之前都支持带行地
|
||
|
址(或数字)参数的。不过 `range` 只影响一些特定功能的函数与函数使用方式,而在
|
||
|
其他情况下,有没有 `range` 属性影响似乎都不大。
|
||
|
|
||
|
首先,只有在用 `:call Fun()` 调用函数时,在 `:call` 之前有行地址(也叫行范围)
|
||
|
参数时,`Fun()` 函数的 `range` 属性才有可能影响。
|
||
|
|
||
|
那么,什么又是行地址参数呢。举个例子,你在 Vim 普通模式下按 `V` 进入选择模式,
|
||
|
选了几行之后,按冒号 `:`,然后输入 `call Fun()`。你会发现,在选择模式下按冒号
|
||
|
进入 ex 命令行时,vim 会自动在命令行加上 `'<,'>`。所以你实际将要运行的命令是
|
||
|
`:'<,'>call Fun()`。`'<` 与 `'>` 是两个特殊的 `mark` 位置,分别表示最近选区的
|
||
|
第一行与最后一行。你也可以手动输入地址参数,比如 `1,5call Fun()` 或 `1,$call
|
||
|
Fun()`,其中 `$` 是个特殊地址,表示最后一行,当前行用 `.` 表示,还支持 `+` 与
|
||
|
`-` 表示相对当前行的相对地址。
|
||
|
|
||
|
总之,当用带行地址参数的 `:{range}call` 命令调用函数时,其含义是要在这些行范围
|
||
|
内调用一个函数。如果该函数恰好指定了 `range` 属性,那么就会隐式地额外传两个参数
|
||
|
给这个函数,`a:firstline` 表示第一行,`a:lastline` 表示最后一行。
|
||
|
|
||
|
比如若用 `:1,5call Fun()` 调用已指定 `range` 属性的函数 `Fun()` ,那么在
|
||
|
`Fun()` 函数体内就能直接使用 `a:firstline` 与 `a:lastline` 这两个参数了,其值
|
||
|
分别为 `1` 与 `5`。如果用 `:'<,'>call Fun()` 调用,vim 也会自动从标记中计算出
|
||
|
实际数字地址来传给 `a:firstline` 与 `a:lastline` 参数。函数调用结束后,光标回
|
||
|
到指定范围的第 1 行,也就是 `a:firstline` 那行。
|
||
|
|
||
|
如果用 `:1,5call Fun()` 调用时,`Fun()` 却没指定 `range` 属性时。那又该怎办,
|
||
|
`Fun()` 函数内没有 `a:firstline` 与 `a:lastline` 参数来接收地址啊?此时,vim
|
||
|
会采用另一种策略,在指定的行范围内的每一行调一次目标函数。按这个实例,vim 会调
|
||
|
用 5 次 `Fun()` 函数,每次调用时分别将当前光标置于 1 至 5 行,如此在 `Fun()`
|
||
|
函数内就可直接操作 “当前行” 了。整个调用结束后,光标停留在范围内的最后一行。
|
||
|
|
||
|
函数的 `range` 属性的工作原理就是这样,然则它有什么用呢?如果函数在操作 vim 中
|
||
|
的当前 buffer 是极有用的。举个例子:
|
||
|
```vim
|
||
|
" File: ~/.vim/vimllearn/frange.vim
|
||
|
|
||
|
function! NumberLine() abort
|
||
|
let l:sLine = getline('.')
|
||
|
let l:sLine = line('.') . ' ' . l:sLine
|
||
|
call setline('.', l:sLine)
|
||
|
endfunction
|
||
|
|
||
|
function! NumberLine2() abort range
|
||
|
for l:line in range(a:firstline, a:lastline)
|
||
|
let l:sLine = getline(l:line)
|
||
|
let l:sLine = l:line . ' ' . l:sLine
|
||
|
call setline(l:line, l:sLine)
|
||
|
endfor
|
||
|
endfunction
|
||
|
|
||
|
finish
|
||
|
|
||
|
测试行
|
||
|
测试行
|
||
|
测试行
|
||
|
测试行
|
||
|
测试行
|
||
|
```
|
||
|
在这个脚本中,定义了一个 `NumberLine()` 不带 `range` 属性的函数,与一个带
|
||
|
`range` 属性的 `NumberLine2()` 函数。它们的功能差不多,就是给当前 buffer 内的
|
||
|
行编号,类似 `set number` 效果,只不过把行号写在文本行之前。
|
||
|
|
||
|
这里用到的几个内建函数稍作解释下,`getline()` 与 `setline()` 分别表示获取与设定
|
||
|
文本行,它们的第一个参数都是行号,当前行号用 `'.'`表示。 `line('.')` 也表示获取
|
||
|
当前行号。
|
||
|
|
||
|
如果你正用 vim 编辑这个脚本,直接用 `:source %` 加载脚本,然后将光标移到
|
||
|
`finish` 之后,选定几行,按冒号进入命令行,调用 `:'<,'>call NumberLine()` 或
|
||
|
`:'<,'>call NumberLine2()` 看看效果。可用 `u` 撤销修改。然后可将光标移到其他地
|
||
|
方,手动输入数字行号代替自动添加的 `'<,'>` 试试看。
|
||
|
|
||
|
最后,关于使用 `range` 属性的几点建议:
|
||
|
|
||
|
* 如果函数实现的功能,不涉及读取或修改当前 buffer 的文本行,完全不用管 `range`
|
||
|
属性。但在调用函数时,也请避免在 `:call` 之前加行地址参数,那样既无意义,还
|
||
|
导致重复调用函数,影响效率。
|
||
|
* 如果函数功能就是要操作当前 buffer 的文本行,则根据自己的需求决定是否添加
|
||
|
`range` 属性。有这属性时,函数只调用一次,效率高些,但要自己编码控制行号,略
|
||
|
复杂些。
|
||
|
* 综合建议就是,如果你懂 `range` 就用,不懂就不用。
|
||
|
|
||
|
### \*函数命令
|
||
|
|
||
|
`:function` 命令不仅可用来(在脚本中)定义函数,也可以用来(在命令行中)查看函
|
||
|
数,这个特性就如 `:command` `:map` 一样的设计。
|
||
|
|
||
|
* `:function` 不带参数,列出所有当前 vim 会话已定义的函数(包括参数)。
|
||
|
* `:function {name}` 带一个函数名参数,必须是已定义的函数全名,则打印出该函数
|
||
|
的定义。由此可见,vim 似乎通过函数名保存了一份函数定义代码的拷贝。
|
||
|
* `:function /{pattern}` 不需要全名,按正则表达式搜索函数,因为不带参数的
|
||
|
`:function` 可能列出太多的函数,如此可用这个命令过滤一下,但是也只会打印函数
|
||
|
头,不包括函数体的实现代码,即使只匹配了一个函数。
|
||
|
* `:function {name}()` 请不要在命令行中使用这种方式,在函数名之后再加小括号,
|
||
|
因为这就是定义一个函数的语法!
|
||
|
|
||
|
### \*函数定义 snip
|
||
|
|
||
|
在实际写 vim 脚本中,函数应该是最常用的结构单元了。然后函数定义的细节还挺多,
|
||
|
`endfunction` 这词也有点长(脚本中不建议缩写)。如果你用过 `ultisnips` 或其他
|
||
|
类似的 snip 插件,则可考虑将常用函数定义的写法归纳为一个 snip。
|
||
|
|
||
|
作为参考示例,我将 `fs` 定义为写 `s:函数` 的代码片断模板:
|
||
|
|
||
|
```vim
|
||
|
snippet fs "script local function" b
|
||
|
" $1:
|
||
|
function! s:${1:function_name}(${2}) abort "{{{
|
||
|
${3:" code}
|
||
|
endfunction "}}}
|
||
|
endsnippet
|
||
|
```
|
||
|
|
||
|
关于 ultisnips 这插件的用法,请参考:https://github.com/SirVer/ultisnips
|
||
|
|
||
|
### 小结
|
||
|
|
||
|
函数是构建复杂程序的基本单元,请一定要掌握。函数必须先定义,再调用,通过参数与
|
||
|
返回值与调用者交互。本节只讲了 VimL 函数的基础部分,函数的进阶用法后面另有章节
|
||
|
专门讨论。
|