注意: 在本 vim9.txt 帮助文件里,以 vim9script 开始的 Vim9 脚本代码块 (以及
以 vim9cmd 开始的单行语句) 会使用 Vim9 脚本语法高亮。它们还可以被直
接执行,从而直观地看到运行后输出的结果。具体来说,用 :'<,'>source
(见 :source-range ) 来执行,也就是,先可视选择内容,再用 V 进入可视
行模式,然后输入 :so 。例如,试试运行一下下面的 Vim9 脚本:
vim9script
echowindow "Welcome to Vim9 script!"
也有不应去执行的代码示例 - 它们解释概念,但不需要示例可执行。这样的代
码会使用通用的代码语法高亮,如下例:
def ThisFunction() # 局部于脚本
def g:ThatFunction() # 全局
export def Function() # 用于 import 和 import autoload
1. 什么是 Vim9 脚本? Vim9-script
Vim 脚本随着时间推移日渐庞大,同时需要保持后向兼容。这意味着过去所做的错误决定
常常不能再改变,而和 Vi 的兼容性限制了可能的解决方案。执行颇慢,每行在每次执行
时都要先解析。
Vim9 脚本的主要目标是大幅提高性能。这是通过把命令编译为可有效执行的指令完成
的。可以期待执行速度有成十上百倍的提高。
一个次要目标是避免 Vim 特定的构造,而尽量和包括 JavaScript、TypeScript 和 Java
这样的主流编程语言接近。
只有不 100% 后向兼容才能达到所需的性能提高。例如,要使函数参数可通过 "a:" 字典
获取,会增加不少开销。其它一些差别,比如错误处理的方式,则更细微一些。
Vim9 脚本语法、语义和行为会应用于:
- 使用 :def 命令定义的函数
- 首个命令是 vim9script 的脚本文件
- 在上述上下文中,定义的自动命令
- 使用 vim9cmd 命令修饰符前缀的命令
Vim9 脚本文件里使用 :function 时,仍然会用老式语法并使用最高的
scriptversion 。不过,这容易混淆,不鼓励。
Vim9 脚本和老式的 Vim 脚本可以混合。重写旧脚本并非必需,它们会继续工作。但可用
一些 :def 函数来编写要求快速的代码。
:vim9[cmd] {cmd} :vim9 :vim9cmd
使用 Vim9 脚本语法、语义和执行方式来解析和执行 {cmd}。在手动键
入的命令、 :function 定义的函数和老式脚本里都可用。
下列的简短例子展示了老式 Vim 脚本命令和 :vim9cmd (因此使用
Vim9 脚本的上下文) 的异同。外观看来很相似,但区别不止是语法
上,还有语义和执行方式上。 >vim
call popup_notification('entrée'[5:]
\ ->str2list()->string(), #{time: 7000})
vim9cmd popup_notification('entrée'[5 :]
->str2list()->string(), {time: 7000})
<
备注: 1) 之所以输出结果不同,是因为 Vim9 脚本使用字符索引,而
老式 Vim 脚本使用字节索引 - 见 vim9-string-index 。
2) 语法也有区别。Vim9 脚本里:
- "[5 :]" 里的空格是必须的 (见 vim9-white-space )。
- 续行符 "\" 是非必须的。
- "#" (以避免给字典键加上引号) 既非必要,也不合法 -
见 #{} 。
E1164
:vim9cmd 不能单独使用;必须后跟一个命令。
:leg[acy] {cmd} :leg :legacy
使用老式脚本语法、语义和执行方式来解析和执行 {cmd}。只在 Vim9
脚本或 :def 函数里有用。要得到和上例等价的结果 (注释说明了为
什么输出结果会不同):
vim9script
# 老式脚本上下文 - 所以,创建弹出并显示 [769, 101]
legacy call popup_notification('entrée'[5:]
\ ->str2list()->string(), #{time: 7000})
# Vim9 脚本上下文 - 所以,创建弹出并显示 [101]
popup_notification('entrée'[5 :]
->str2list()->string(), {time: 7000})
Vim9 脚本里,脚本局部变量可用前缀 "s:",和老式 Vim 脚本的语法
相同,不过在 Vim9 脚本里,此前缀是可选的,因为变量缺省就是局部
于脚本的。下例展示了语法上的差异: 在 Vim9 脚本里,"k" 定义了脚
本局部变量,而在老式 Vim 脚本的上下文里,须用 "s:k" 来访
问。
vim9script
var k: string = "Okay"
echo k
legacy echo s:k
E1189
在已编译的 Vim9 脚本里,用于控制流的命令不支持 :legacy 。
例如:
vim9script
def F_1189()
if v:version == 900
# E1189: Cannot use :legacy with this command: endif
legacy endif
enddef
F_1189()
E1234
:legacy 不能单独使用;必须后跟一个命令。
2. 和老式 Vim 脚本的差异 vim9-differences
总览
E1146
使用 Vim9 脚本和 :def 函数最常见差异的简要小结;细节见后:
- 注释以 # 开始而不是 ":
echo "hello" # 注释
- 很少需要反斜杠用来作续行符:
echo "hello "
.. yourName
.. ", how are you?"
- 很多地方需要空格以提高可读性,见 vim9-white-space 。
- 赋值不用 :let E1126 ,变量用 :var 声明:
var count = 0
count += 3
- 常量可用 :final 和 :const 声明:
final matches = [] # 之后可再扩充列表
const names = ['Betty', 'Peter'] # 不能改变
- :final 不再能用作 :finally 的缩写。
- 变量和函数缺省局部于脚本。
- 函数声明要带参数类型和返回值类型:
def CallMe(count: number, message: string): bool
- 调用函数不用 :call :
writefile(['done'], 'file.txt')
- 不能用以下的旧式的 Ex 命令:
:Print
:append
:change
:d 后跟 'd' 或 'p'。
:insert
:k
:mode
:open
:s 只带标志位
:t
:xit
- 有些命令,特别是用于流控制的命令,不能缩短。例如, :throw 不能写成 :th 。
vim9-no-shorten
- 不可用花括号名字。
- 命令前的范围必须用冒号前缀:
:%s/this/that
- 不再可以用 "@r" 来执行寄存器,需要冒号前缀或使用 :exe :
:exe @a
- 除非特别指出,使用最高的 scriptversion 。
- 定义表达式映射时,在定义所在的脚本上下文里计算表达式。
- 字符串索引以字符而不是字节计: vim9-string-index
- 可能意想不到的一些差异: vim9-gotchas 。
# 开始的注释
老式 Vim 脚本以双引号开始脚本注释。Vim9 脚本则用 # 开始脚本注释。
# 声明
var count = 0 # 出现的次数
原因是双引号也是字符串的开始,在很多地方,尤其是表达式内部换行处,很难区分双引
号真正的意思,因为字符串和注释都可以后跟任意文本。为避免混淆,现在只识别 # 注
释。这和外壳脚本和 Python 程序完全相同。
Vi 里 # 是带行号列出文本的命令。Vim9 脚本可用 :number 代替。
:101 number
为提高可读性,命令和开始注释的 # 之间必须有一个空格:
var name = value # 注释
var name = value# 出错!
E1170
不要以 #{ 开始注释,它看来像老式的字典常数,有歧义时这会报错。#{{ 和 #{{{ 则可
以,它们用来启动折叠。
开始读取脚本文件时 vim 不知道是 vim9 脚本,直到找到 vim9script 命令为止。
在那之前,需要用老式注释:
" 老式注释
vim9script
# vim9 注释
这很难看,最好在第一行就放上 vim9script :
vim9script
# vim9 注释
老式 Vim 脚本中 # 也用于轮换文件名。Vim9 脚本里,要改用 %%。## 可改用 %%% (代
表所有参数)。
Vim9 函数
E1099
由 :def 定义的函数进行编译。执行成倍加快,通常有 10 到 100 倍的加速。
许多错误在编译时已经可以发现,而不用等到函数执行的时候。有严格的语法,以确保代
码易读易理解。
以下情形之一时进行编译:
- 函数第一次调用时
- 脚本中在函数定义后遇到 :defcompile 命令时
- 函数使用 :disassemble 时。
- 函数被已编译的函数调用或用作函数引用时 (为了检查参数和返回类型)
E1091 E1191
编译失败后,下次调用时不会再次尝试编译,而会报错: "E1091: Function is not
compiled: {name}"。
遇到尚未创建的用户命令时,编译会失败。这种情况可用 execute() 在运行时调用该
命令。
def MyFunc()
execute('DefinedLater')
enddef
:def 不像 :function 那样有 "range"、"abort"、"dict" 或 "closure" 这样的选
项。 :def 函数总会在错误时中止 (除非命令使用了 :slient! 或错误在 :try 块
里捕获),不接受范围、不能是 "dict" 函数,而且总可以有闭包。
vim9-no-dict-function E1182
可用 Vim9 类 ( Vim9-class ) 而不是 "字典函数"。也可以显式传递入字典:
def DictFunc(self: dict<any>, arg: string)
echo self[arg]
enddef
var ad = {item: 'value', func: DictFunc}
ad.func(ad, 'item')
但可以调用老式字典函数:
func Legacy() dict
echo self.value
endfunc
def CallLegacy()
var d = {func: Legacy, value: 'text'}
d.func()
enddef
必须指定参数类型和返回类型。可用 "any" 类型,和老式函数一样,此时类型检查会在
运行时完成。
E1106
参数用不带 "a:" 的名字访问,就像其它语言一样。没有 "a:" 字典或 "a:000" 列表。
vim9-variable-arguments E1055 E1160 E1180
类似于 TypeScript,通过给定名字和列表类型的末项参数来定义可变参数。例如,数值
的列表:
def MyFunc(...itemlist: list<number>)
for item in itemlist
...
如果函数参数可选 (有缺省值),传递 v:none 作为参数会使用其缺省值。用于在使用
缺省值的参数之后需要指定其它参数的场合。例如:
def MyFunc(one = 'one', last = 'last')
...
enddef
MyFunc(v:none, 'LAST') # 首个参数使用缺省值 'one'
vim9-ignored-argument E1181
参数 "_" (下划线) 可用于忽略参数值。用于回调中不需要提供参数值但要给出一个参数
以匹配调用的场合。例如,使用 map() 时需要两个参数,键和值,要忽略键:
map(numberList, (_, v) => v * 2)
"_" 参数多次使用不会报错。也不需要指定类型。
函数和变量缺省局部于脚本
vim9-scopes
在 Vim9 脚本中,用 :function 或 :def 来指定脚本级别的新函数时,函数局部于
脚本,就像老式脚本里用了 "s:" 前缀那样。"s:" 前缀可选。要定义全局函数或变量,
必须使用 "g:" 前缀。脚本里要被别人导入和在自动载入脚本定义的函数,需要使用
"export",才能被别处使用。
def ThisFunction() # 局部于脚本
def s:ThisFunction() # 局部于脚本
export def Function() # 用于 import 和 import autoload
E1075
用 :function 或 :def 来指定 :def 函数内的嵌套函数时,如果没给出命名空
间,此嵌套函数局部于定义所在的代码块。不能用传递字符串参数的 function() 形式
访问,但函数引用本身可以:
def Outer()
def Inner()
echo 'inner'
enddef
var Fok = function(Inner) # 正确
var Fbad = function('Inner') # 不行
细节: 这是因为 "Inner" 实际会变成函数引用,指向一个名字内部生成的函数。
函数内不可以定义局部于脚本的函数。可以定义局部函数并赋值给局部于脚本的函数引用
(函数必须在脚本级别声明)。可以用 "g:" 前缀来定义全局函数。
引用函数时如果不带 "s:" 或 "g:" 前缀,Vim 会依次搜索函数:
- 在函数作用域内,在块作用域内
- 在脚本作用域内
导入的函数可根据 :import 命令给出的前缀找到。
因为使用局部于脚本的函数引用时可以不带 "s:",其名字必须以大写字母开头,即使用
了 "s:" 前缀也是如此。老式脚本里因为不能以 "funcref" 来进行引用,所以可用
"s:funcref"。而 Vim9 脚本里这种直接引用的形式是可以的,因此必须使用
"s:Funcref" 形式,以防其名和内建函数起冲突。
vim9-s-namespace E1268
Vim9 脚本级别不支持使用 "s:" 前缀。所有不带前缀的函数和变量都是局部于脚本的。
:def 函数里,"s:" 的使用取决于脚本: 老式脚本里的局部于脚本的变量和函数要用
"s:",而 Vim9 脚本里它们不用 "s:"。这和文件其余部分你能看到的用法匹配。
老式函数里必须使用 "s:",见前。不管脚本是 Vim9 还是老式的都是如此。
在所有情况下,函数必须先定义,然后才能使用。所谓使用,是指函数调用时,或
:defcompile 导致调用被编译时,或调用它的函数被编译时 (以确定返回类型)。
其结果是,无命名空间的函数和变量通常可在其定义所在或导入的脚本内找到。而全局函
数和变量可以在任何地方定义 (要找到需要点运气!用 :verbose 往往可以看到它上次
是在哪里设置的)。
E1102
全局函数还是可以在几乎任何时候定义和删除。vim9 脚本里,局部于脚本的函数在脚本
载入时定义一次,且不能自己删除或替代 (但脚本重载时是会的)。
编译函数时和遇到函数调用时如果函数 (还) 未定义,不触发 FuncUndefined 自动命
令。如果需要,可用自动载入函数,或调用老式函数,那里会触发 FuncUndefined 。
重载 Vim9 脚本时缺省清除函数或变量
vim9-reload E1149 E1150
再次载入同一老式 Vim 脚本时,不会删除任何东西,脚本里的命令会替代原有的变量和
函数,创建新的内容,而被删除的东西还会悬挂在那里。
再次载入同一 Vim9 脚本时,删除所有已有的局部于脚本的函数和变量,以便从干净的状
态开始。这可用于开发插件时新版本的实验。如果换了任何名字,不需要担心旧名字还存
在。
如果确实要保留项目,可用:
vim9script noclear
可在脚本中使用此命令,以便在脚本重载时,在某些情况下用 finish 命令放弃载入。
例如,如果设置了某个缓冲区局部选项为函数时,函数不需要再次定义:
vim9script noclear
setlocal completefunc=SomeFunc
if exists('*SomeFunc')
finish
endif
def SomeFunc()
....
用 :var、:final 和 :const 声明变量
vim9-declaration :var E1079
E1017 E1020 E1054 E1087 E1124
局部变量须用 :var 定义。局部常量须用 :final 或 :const 定义。我们把两者都
称为 "变量"。
变量可以局部于脚本、函数或代码块:
vim9script
var script_var = 123
def SomeFunc()
var func_var = script_var
if cond
var block_var = func_var
...
变量只在定义所在的块和嵌套块中可见。块定义结束后,变量不再可访问:
if cond
var inner = 5
else
var inner = 0
endif
echo inner # 出错!
变量必须在使用之前进行声明:
var inner: number
if cond
inner = 5
else
inner = 0
endif
echo inner
不过对简单值有更简短和更快的方法:
var inner = 0
if cond
inner = 5
endif
echo inner
E1025 E1128
要有意识地从之后的代码中隐藏某个变量,可用代码块:
{
var temp = 'temp'
...
}
echo temp # 出错!
用户命令里这特别有用:
command -range Rename {
var save = @a
@a = 'some expression'
echo 'do something with ' .. @a
@a = save
}
自动命令也是:
au BufWritePre *.go {
var save = winsaveview()
silent! exe ':%! some formatting command'
winrestview(save)
}
不过,用 :def 函数可能更好些。
E1022 E1103 E1130 E1131 E1133
E1134
变量声明时如果带类型但不带初始块,其值初始为假值 (布尔型)、空 (字符串、列表、
字典,等等) 或零 (数值、any,等等)。特别重要的是,使用 "any" 类型时,缺省值为
数值零。例如声明列表时,可以加入项目:
var myList: list<number>
myList->add(7)
初始化变量为 null 值,如 null_list 时,和不初始化变量是不同的。以下例子会抛
出错误:
var myList = null_list
myList->add(7) # E1130: Cannot add to null list
E1016 E1052 E1066
Vim9 脚本中不能用 :let 。已有的变量可直接赋值,不需要任何命令。全局、窗口、标
签页、缓冲区和 Vim 变量也是一样,因为它们并非真正通过声明产生,但它们也可用
:unlet 删除。
E1065
不能用 :va 来声明变量,必须使用全名 :var 来书写。这是为了方便阅读。
E1178
:lockvar 不能用于局部变量。可用 :const 和 :final 代替。
exists() 和 exists_compiled() 函数不能用局部变量或参数。
E1006 E1041 E1167 E1168 E1213
变量、函数和函数参数不能隐藏在同一脚本文件里之前定义或导入的变量和函数。
变量可以隐藏 Ex 命令,如有需要请给变量换名。
全局变量必须带上 "g:" 前缀,即使脚本级别的也是如此。
vim9script
var script_local = 'text'
g:global = 'value'
var Funcref = g:ThatFunction
全局函数必须带上 "g:" 前缀:
vim9script
def g:GlobalFunc(): string
return 'text'
enddef
echo g:GlobalFunc()
自动载入函数不需要 "g:" 前缀。
vim9-function-defined-later
尽管可以不带 "g:" 前缀调用全局函数,编译时它们必须已存在。加上 "g:" 前缀后,函
数可以在其后再定义。例如:
def CallPluginFunc()
if exists('g:loaded_plugin')
g:PluginFunc()
endif
enddef
但如果像下面这么做,编译时会报错 "PluginFunc" 不存在,即使 "g:loaded_plugin"
不存在:
def CallPluginFunc()
if exists('g:loaded_plugin')
PluginFunc() # 报错,函数找不到
endif
enddef
可用 exists_compiled() 来避免此错误,但函数可能也不会被调用了,即使
"g:loaded_plugin" 之后被定义了也不会:
def CallPluginFunc()
if exists_compiled('g:loaded_plugin')
PluginFunc() # Function 可能永远不会被调用
endif
enddef
因为 `&opt = value` 现在用来给选项 "opt" 赋值,所以不再能用 ":&" 来重复
:substitute 命令了。
vim9-unpack-ignore
解包赋值里,下划线可用于忽略列表项目,类似于函数参数的忽略方法:
[a, _, c] = theList
要忽略其余项目:
[a, b; _] = longList
E1163 E1080
可以使用解包记法,一次声明多于一个变量。每个变量可给出类型或从值推断:
var [v1: number, v2] = GetValues()
仅当有带值列表的时候才建议使用此记法,一行声明一个变量更易阅读和修改。
常量
vim9-const vim9-final
常量如何工作因语言而异。有些语言把不能重新赋其它值的变量当作常量。JavaScript
是一个例子。其它语言则会使值也不可更改,因此如果常量使用了列表,列表本身也不可
改变。Vim9 可以兼有两者。
E1021 E1307
可用 :const 使变量及其值为常量。可用于复合结构以确保其不会被修改。示例:
const myList = [1, 2]
myList = [3, 4] # 出错!
myList[0] = 9 # 出错!
myList->add(3) # 出错!
:final E1125
可用 :final 使变量为常量,而值仍然可更改。Java 用户对此很熟悉。例如:
final myList = [1, 2]
myList = [3, 4] # 出错!
myList[0] = 9 # 正确
myList->add(3) # 正确
常量写入全大写 ALL_CAPS 是惯例,但不必如此。
常量限制只适用于值本身,而不是其引用的值。
final females = ["Mary"]
const NAMES = [["John", "Peter"], females]
NAMES[0] = ["Jack"] # 出错!
NAMES[0][0] = "Jack" # 出错!
NAMES[1] = ["Emma"] # 出错!
NAMES[1][0] = "Emma" # 正确,现在 females[0] == "Emma"
省略 :call 和 :eval
E1190
可直接调用函数而无需 :call :
writefile(lines, 'file')
:call 还可用,但不鼓励。
方法只要以标识符开始或不可能是 Ex 命令,可直接调用而无需 eval 。函数的 "(" 或
"->" 必须后跟内容,不能有换行。例如:
myList->add(123)
g:myList->add(123)
[1, 2, 3]->Process()
{a: 1, b: 2}->Process()
"foobar"->Process()
("foobar")->Process()
'foobar'->Process()
('foobar')->Process()
在罕见情形下函数名和 Ex 命令有二义性,用 ":" 前缀来明确你想用的是 Ex 命令。例
如,既有 :substitute 命令,又有 substitute() 函数。如果一行以
substitute( 开始,会假定使用函数,要使用命令版本,加上冒号前缀:
:substitute(模式 (替代 (
如果表达式以 "!" 开始,解读为外壳命令,而不是条件取反。因而,以下是外壳命令:
!shellCommand->something
要用 "!" 进行取反,把表达式放在括号里:
(!expression)->Method()
注意 尽管变量在使用前必须先定义,函数在定义前就可以调用。为了允许函数间能相互
依赖,这是有必要的。但因此效率会稍稍低些,因为函数需要依名查找。且函数名的拼写
错误只有在函数被调用时才能发现。
省略 function()
表达式中,用户定义函数可用作函数引用,而无需通过 function() 。这时会检查参数
类型和返回类型。函数必须已经有定义。
var Funcref = MyFunction
而使用 function() 时,返回类型是 "func",一个可带任何数量参数和任何返回类型
(包括 void) 的函数。如果 function() 的参数用上引号,函数可以延后定义。
匿名函数使用 => 而不是 ->
vim9-lambda
在老式脚本里,"->" 既用于函数调用,也用于匿名函数,这里可能产生混淆。另外,找
到 "{" 时解析器需要知道它是匿名函数还是字典的开始,现在由于参数类型的使用,这
里的情况更复杂。
为避免这些问题,Vim9 脚本的匿名函数使用不同的语法,这和 JavaScript 类似:
var Lambda = (arg) => expression
var Lambda = (arg): type => expression
E1157
匿名函数在直到 "=>" 为止的参数里不允许换行 (这样 Vim 才能识别带括号的表达式和
匿名函数参数的区别)。这样可以:
filter(list, (k, v) =>
v > 0)
这样不可以:
filter(list, (k, v)
=> v > 0)
这样也不可以:
filter(list, (k,
v) => v > 0)
但可用反斜杠在解析发生前,把行进行连接:
filter(list, (k,
\ v)
\ => v > 0)
vim9-lambda-arguments E1172
在老式脚本里,匿名函数调用时可带任意多个额外参数,没办法警告不要这么做。在
Vim9 脚本里参数数目必须匹配。如果要接受任何参数,或任何额外参数,可用 "..._",
这使函数接受 vim9-variable-arguments 。例如:
var Callback = (..._) => 'anything'
echo Callback(1, 2, 3) # 显示 "anything"
inline-function E1171
此外,匿名函数可以包含 {} 包围的多个语句:
var Lambda = (arg) => {
g:was_called = 'yes'
return expression
}
比如可用来实现计数器:
var count = 0
var timer = timer_start(500, (_) => {
count += 1
echom 'Handler called ' .. count
}, {repeat: 3})
结尾的 "}" 必须位于行首。后面可跟其它字符,例如:
var d = mapnew(dict, (k, v): string => {
return 'value'
})
"{" 之后不能跟随命令,那里只可以有注释。
command-block E1026
定义用户命令时也可用代码块。在代码块内使用 Vim9 语法。
这里是个使用嵌入文档 (here-docs) 的例子:
com SomeCommand {
g:someVar =<< trim eval END
ccc
ddd
END
}
如果语句包含字典,其结束花括号不能写在行首。否则,被解析为代码块的结束。下面这
样不行:
command NewCommand {
g:mydict = {
'key': 'value',
} # 错误: 会识别为代码块的结束
}
把 '}' 放在最后项目之后可以解决这个问题:
command NewCommand {
g:mydict = {
'key': 'value' }
}
理据: "}" 不能在命令之后出现,因为需要对命令进行解析才能找到它。为了一致起见,
"{" 之后也不能跟随命令。不幸的是,这就意味着不接受 "() => { command }",必须
使用换行。
vim9-curly
为了不让字典常量的 "{" 被识别为语句块,把它包围在括号里:
var Lambda = (arg) => ({key: 42})
和命令块的开始有歧义的话也是如此:
({
key: value
})->method()
自动续行
vim9-line-continuation E1097
许多情况下,表达式显然会在下一行继续。这些情况不需要在行前加入反斜杠 (见
line-continuation )。例如,当列表跨越多行时:
var mylist = [
'one',
'two',
]
字典跨越多行时:
var mydict = {
one: 1,
two: 2,
}
使用函数调用时:
var result = Func(
arg1,
arg2
)
对不在 []、{} 或 () 内的表达式中的二元操作符而言,可在操作符之前或之后断行。例
如:
var text = lead
.. middle
.. end
var total = start +
end -
correction
var result = positive
? PosFunc(arg)
: NegFunc(arg)
方法调用的 "->" 和成员访问的句号之前都可断行:
var result = GetBuilder()
->BuilderSetWidth(333)
->BuilderSetHeight(777)
->BuilderBuild()
var result = MyDict
.member
命令里如果有一组命令作为参数,行首的 | 字符指示行的继续:
autocmd BufNewFile *.match if condition
| echo 'match'
| endif
注意 这意味着 here 文档里首行不能以竖线开始:
var lines =<< trim END
| 这不行
END
要么用一空行开始,要么不要用 here 文档。或者暂时在 'cpoptions' 里加入 "C" 标志
位:
set cpo+=C
var lines =<< trim END
| 这样可以
END
set cpo-=C
如果 here 文档在函数内出现,'cpoptions' 必须在 :def 前设置,在 :enddef 后恢
复。
在续行还需要反斜杠的地方,如断开长 Ex 命令时,注释可用 '#\ ' 开始:
syn region Text
\ start='foo'
#\ 注释
\ end='bar'
就像在老式脚本里可用 '"\ ' 一样。即使续行不需要反斜杠和以竖线开始行的情况,也
要如此:
au CursorHold * echom 'BEFORE bar'
#\ 一些注释
| echom 'AFTER bar'
E1050
要识别出现在行首的操作符,需要在范围之前加上冒号。要把 "start" 加上 "print":
var result = start
+ print
相当于:
var result = start + print
而要赋值 "start" 并显示一行:
var result = start
:+ print
范围后必须跟 Ex 命令。不带冒号时不用 :call 就可以调用函数,但在范围后需要:
MyFunc()
:% call MyFunc()
注意 +cmd 参数不需要冒号:
edit +6 fname
函数声明中也可以在参数间换行:
def MyFunc(
text: string,
separator = '-'
): string
因为续行不容易识别,命令的解析必须更严格。例如,因为首行有错,下面的第二行会看
作一个单独的命令:
popup_create(some invalid expression, {
exit_cb: Func})
现在,"exit_cb: Func})" 实际是一个合法的命令: 保存任何改动到文件 "_cb: Func})"
并退出。为了避免此种错误,Vim9 脚本中在多数命令名和参数间必须有空白。 E1144
不过,不能识别作为命令参数的命令里的续行。例如 "windo echo expr" 里不能识别
"expr" 内部的换行。
注意:
- "enddef" 不能用于续行的开始,它会结束当前函数。
- 赋值语句的 LHS 不接受换行。特别是列表解包 :let-unpack 。这样可以:
[var1, var2] =
Func()
这样不行:
[var1,
var2] =
Func()
- :echo 、 :execute 和类似命令的参数与参数之间不接受换行。这样可以:
echo [1,
2] [3,
4]
这样不行:
echo [1, 2]
[3, 4]
- 有些情况 Vim 解析命令有困难,尤其是一个命令用作另一个命令的参数的时候,如
:windo 。这些情况续行必须使用反斜杠。
空白
vim9-white-space E1004 E1068 E1069 E1074 E1127 E1202
Vim9 脚本强制空白的正确使用。以下不再允许:
var name=234 # 出错!
var name= 234 # 出错!
var name =234 # 出错!
"=" 前后必须有空白:
var name = 234 # 正确
命令之后开始注释的 # 之前必须有空白字符:
var name = 234# 出错!
var name = 234 # 正确
多数操作符需要用空白包围。
子列表 (切片) 表达式中的 ":" 两边需要空白,出现在开始和结束处的除外:
otherlist = mylist[v : count] # v:count 有不同含义
otherlist = mylist[:] # 建立列表的备份
otherlist = mylist[v :]
otherlist = mylist[: v]
以下位置不允许空白:
- 函数名和 "(" 之间:
Func (arg) # 出错!
Func
\ (arg) # 出错!
Func
(arg) # 出错!
Func(arg) # 正确
Func(
arg) # 正确
Func(
arg # 正确
)
E1205
:set 命令在选项名和后续的 "&"、"!"、"<"、"="、"+="、"-=" 或 "^=" 之间不能有
空白。
没有花括号扩展
不能使用 curly-braces-names 。
不忽略命令修饰符
E1176
不接受命令修饰符的命令如果用上命令修饰符,会报错。
E1082
另外,使用了命令修饰符但后面不跟命令,现在也是错误。
字典常量
vim9-literal-dict E1014
传统上 Vim 支持使用 {} 语法的字典常量:
let dict = {'key': value}
后来越来越明显地,使用简单文本键非常常见,所以引入了支持后向兼容的常量字典:
let dict = #{key: value}
不过,此种 #{} 语法不同于任何已有的语言。因为使用常量并使用表达式作为键更为常
见,也考虑到 JavaScript 也使用此种语法,使用 {} 形式的字典常量被认为是更有用的
语法。Vim9 脚本里,{} 形式使用常量键:
var dict = {key: value}
这种形式可以用于字母数字字符,下划线和连字符。如果使用其它字符,使用单引号或双
引号:
var dict = {'key with space': value}
var dict = {"key\twith\ttabs": value}
var dict = {'': value} # 空键
E1139
如果键需要表达式,可用方括号,就像 JavaScript 那样:
var dict = {["key" .. nr]: value}
键类型可以是字符串、数值、布尔型或者浮点数。其它类型会报错。不带 [] 时直接用作
字符串值,保留引导的零。而用 [] 的表达式会先经过计算再转换为字符串。这样引导的
零就会被丢弃:
var dict = {000123: 'without', [000456]: 'with'}
echo dict
{'456': 'with', '000123': 'without'}
浮点数只能在 [] 里工作,因为如果不是的话,句号不被接受:
var dict = {[00.013]: 'float'}
echo dict
{'0.013': 'float'}
没有 :xit、:t、:k、:append、:change 或 :insert
E1100
这些命令很容易会和局部变量名混淆。
:x 或 :xit 可用 :exit 替代。
:t 可用 :copy 替代。
:k 可用 :mark 替代。
比较符
字符串的比较符不考虑 'ignorecase' 选项。
因此 "=~" 和 "=~#" 一样。
"is" 和 "isnot" ( expr-is 和 expr-isnot ) 用于字符串时,现在总是返回假值。老
式脚本里它们只是比较字符串内容,而在 Vim9 脚本里它们比较是否同一,因为字符串
使用时会先复制,因此两个字符串永不会等价 (有一天字符串不是复制而采用引用计数,
这可能会改变)。
错误后中断
老式脚本里,遇到错误时,Vim 会继续执行后续行。这会引起一长串错误,需要 CTRL-C
才能停止。Vim9 脚本的命令执行会在第一个错误处停止。例如:
vim9script
var x = 不-存-在
echo '不会执行'
For 循环
E1254
循环变量不能事先声明:
var i = 1
for i in [1, 2, 3] # 出错!
但可用全局变量:
g:i = 1
for g:i in [1, 2, 3]
echo g:i
endfor
老式 Vim 脚本有一些技巧来实现在列表句柄上的 for 循环,以删除当前或前一项目。在
Vim9 脚本中,正常使用索引就可以了,如果项目被删除,列表中的对应项目会跳过。
示例老式脚本:
let l = [1, 2, 3, 4]
for i in l
echo i
call remove(l, index(l, i))
endfor
会显示:
1
2
3
4
而在编译后的 Vim9 脚本中,会得到:
1
3
一般而言,不应改变正在遍历的列表。如果需要的话,先建立备份。
遍历列表的列表时,可以修改内层的列表。循环变量是 "final",它不能改变,但它的值
可以改变。
E1306
循环深度,即 :for 和 :while 循环加起来,不能超过 10 层。
条件和表达式
vim9-boolean
多数情况下,条件和表达式就像其它语言里那样的用法。有些值和老式 Vim 脚本有所不
同:
值 老式 Vim 脚本 Vim9 脚本
0 准假值 准假值
1 准真值 准真值
99 准真值 出错!
"0" 准假值 出错!
"99" 准真值 出错!
"text" 准假值 出错!
用于 "??" 操作符和使用 "!" 时,不会报错,每个值都是准假值或准真值。这很像
JavaScript,唯一的例外是空列表和字典也被视为准假值:
类型 何时为准真值
bool true、v:true 或 1
number 非零
float 非零
string 非空
blob 非空
list 非空 (和 JavaScript 不同)
tuple 非空 (和 JavaScript 不同)
dictionary 非空 (和 JavaScript 不同)
func 有函数名时
special true 或 v:true
job 非 NULL 时
channel 非 NULL 时
class 非 NULL 时
object 非 NULL 时 (TODO: 应为 isTrue() 返回 true 时)
布尔操作符 "||" 和 "&&" 期待值为布尔型、零或一:
1 || false == true
0 || 1 == true
0 || false == false
1 && true == true
0 && 1 == false
8 || 0 出错!
'yes' && 0 出错!
[] || 99 出错!
"!" 用作反转操作符时,任何类型都不会报错,结果为布尔型。"!!" 可用于把任何值转
换为布尔型:
!'yes' == false
!![] == false
!![1, 2, 3] == true
" .." 用作字符串连接时,简单类型的参数总是转化为字符串。
'hello ' .. 123 == 'hello 123'
'hello ' .. v:true == 'hello true'
简单类型是数值、浮点数、特殊类型和布尔型。其它类型可用 string() 。
false true null null_blob null_channel
null_class null_dict null_function null_job
null_list null_object null_partial null_string
E1034
Vim9 脚本可以使用如下预定义值:
true
false
null
null_blob
null_channel
null_class
null_dict
null_function
null_job
null_list
null_tuple
null_object
null_partial
null_string
true 等价于 v:true 、 false 等价于 v:false 而 null 等价于 v:null 。
null 类型为 "special",而其它的 "null_" 值可根据其名推定类型。null 值和空值
处理方式通常相同,但不绝对。这些值可用于清除局部于脚本的变量,因为它们不能用
:unlet 删除。例如:
var theJob = job_start(...)
# 让作业自己做事
theJob = null_job
这些值也可用于参数的缺省值:
def MyFunc(b: blob = null_blob)
# 注意: 和 null 比较,而不是 null_blob,
# 以区分缺省值和空 blob。
if b == null
# 没给出 b 参数
更多和 null 的测试的信息可见 null-compare 。
可以把 null 和任何值比较,不会给出类型错误。但把 null 和数值、浮点或布尔型
比较总返回 false 。这和老式脚本不同,那里把 null 和零或 false 比较会返回
true 。
vim9-false-true
转换布尔型为字符串时使用 false 和 true ,而不像老式脚本那样用 v:false 和
v:true 。 v:none 没有对应的 none 替代,在其他语言中它没有类似的结构。
vim9-string-index
字符串用 [idx] 索引 或 [idx : idx] 取切片时使用的是字符索引而非字节索引。组合
字符包含在内。例如:
echo 'bár'[1]
在老式脚本里,这会得到字符 0xc3 (一个非法字节),在 Vim9 脚本里这会得到字符串
'á'。
负索引从结尾处开始计算,"[-1]" 代表末字符。要排除末字符,用 slice() 。
如果要组合字符分别计算,可用 strcharpart() 。
索引超出范围时,返回空字符串。
老式脚本中,接受 "++var" 和 "--var",不报错也没有效果。Vim9 脚本中则会报错。
零开始的数值不被识别为八进制,只有 "0o" 开始的数值才识别为八进制:
"0o744". scriptversion-4
要注意什么
vim9-gotchas
Vim9 设计和常用的编程语言相近,而同时试图支持老式的 Vim 命令。这里不得不做出某
些妥协。这里是若干出人意外之处的一个小结。
Ex 命令范围需要冒号前缀。
-> 老式 Vim: 右移前行
->func() Vim9: 继续行上的方法调用
:-> Vim9: 右移前行
%s/a/b 老式 Vim: 在所有行上替代
x = alongname
% another Vim9: 续行上的取余操作符
:%s/a/b Vim9: 在所有行上替代
't 老式 Vim: 跳转到位置标记 t
'text'->func() Vim9: 方法调用
:'t Vim9: 跳转到位置标记 t
有些 Ex 命令在 Vim9 脚本中和赋值语句会引起混淆:
g:name = value # 赋值
:g:pattern:cmd # :global 命令
要避免 :global 或 :substitute 命令和表达式或赋值相混淆,当这些命令缩写为单
字母时,不能使用若干分隔符: ':'、'-' 和 '.'。
g:pattern:cmd # 非法命令 - 出错
s:pattern:repl # 非法命令 - 出错
g-pattern-cmd # 非法命令 - 出错
s-pattern-repl # 非法命令 - 出错
g.pattern.cmd # 非法命令 - 出错
s.pattern.repl # 非法命令 - 出错
另外,命令和分隔符之间也不能有空格:
g /pattern/cmd # 非法命令 - 出错
s /pattern/repl # 非法命令 - 出错
:def 定义的函数会对整个函数进行编译。老式函数可以中途放弃,因而以下的行不会
被立即解析:
func Maybe()
if !has('feature')
return
endif
use-feature
endfunc
而 Vim9 函数会作为一个整体被编译:
def Maybe()
if !has('feature')
return
endif
use-feature # 可能会报编译错误
enddef
一个临时解决方法是把它分割为两个函数:
func Maybe()
if has('feature')
call MaybyInner()
endif
endfunc
if has('feature')
def MaybeInner()
use-feature
enddef
endif
或者把不支持的代码放在 if 块里,带上计算值为假值的常量表达式:
def Maybe()
if has('feature')
use-feature
endif
enddef
为此也可用 exists_compiled() 函数。
vim9-user-command
编译函数的另一个副作用是编译时要对用户命令的存在与否进行检查。如果用户命令在之
后定义,会报错。下面这样可以:
command -nargs=1 MyCommand echom <q-args>
def Works()
MyCommand 123
enddef
但这样会报错,"MyCommand" 无定义:
def Works()
command -nargs=1 MyCommand echom <q-args>
MyCommand 123
enddef
一个临时解决方法是用 :execute 间接执行命令:
def Works()
command -nargs=1 MyCommand echom <q-args>
execute 'MyCommand 123'
enddef
注意 在不识别的命令里,不检查 "|" 和后跟的命令。下例会报丢失 endif 的错误:
def Maybe()
if has('feature') | use-feature | endif
enddef
其它差异
除非被显式覆盖,模式的用法假定置位了 'magic'。
不使用 'edcompatible' 选项值。
不使用 'gdefault' 选项值。
以下 wiki 可能对您有用。这是 Vim9 脚本的一位早期试用者编写的:
https://github.com/lacygoill/wiki/blob/master/vim/vim9.md
:++ :--
新增了 ++ 和 -- 命令,类似于加一或减一:
++var
var += 1
--var
var -= 1
在表达式中使用 ++var 或 --var 目前还不支持。
3. 新风格函数 fast-functions
:def
:def[!] {name}([arguments])[: {return-type}]
定义名为 {name} 的新函数。在下面的行给出函数体,直到配
对的 :enddef 为止。
E1073
在同一脚本的局部级别上,不能重用 {name}:
vim9script
def F_1073()
enddef
def F_1073() # E1073: 名字已被定义: <SNR>...
enddef
E1011
{name} 必须少于 100 字节长。
E1077
{arguments} 是零或多个参数声明组成的序列。有以下三种形
式:
{name}: {type}
{name} = {value}
{name}: {type} = {value}
第一种形式指定必选参数,声明里必须提供类型。示
例:
vim9script
def F_1077(x): void
# E1077: x 缺少参数类型
enddef
在第二种形式里,因为声明没有显式提供类型,Vim 会根据赋
值自动推导参数的类型。在第二三两种形式里,调用者省略该
可选参数时,都会默认使用 {value} 值。示例:
vim9script
def SecondForm(arg = "Hi"): void
echo $'2. arg 的类型为 "{arg->typename()}" ' ..
$'而 arg 的缺省值为 "{arg}"'
enddef
SecondForm()
def ThirdForm(arg2: number = 9): void
echo $'3. arg2 的缺省值为 {arg2}'
enddef
ThirdForm()
E1123
:def 定义的函数里,调用内建函数时,参数必须以逗号分
隔:
vim9script
def F_1123(a: number, b: number): void
echo max(a b)
# E1123: 参数前缺少逗号: b)
enddef
F_1123(1, 2)
E1003 E1027 E1096
:return 所用的值类型必须匹配 {return-type}。
{return-type} 省略或为 "void" 时,函数不允许返回任何
值。示例:
vim9script
def F_1003(): bool
return # E1003: 缺少返回值
enddef
F_1003()
vim9script
def F_1027(): bool
echo false # E1027: 缺少返回语句
enddef
F_1027()
vim9script
def F_1096(): void
return false # E1096: 在没有返回类型的函数中返
enddef
F_1096()
E1056 E1059
采用 ": {return-type}" 形式时,不能省略 {return-type}
部分 (也就是,只有一个冒号)。": " 的前面也不能有空格。
示例: >vim
def F_1056():
# E1056: 期待类型:
enddef
def F_1059() : bool
# E1059: 冒号前不允许有空白:...
enddef
<
:def 定义的函数只有在实际调用、使用 :disassemble
或 :defcompile 时 (示例可见 :disassemble ),才会被
编译为指令序列。那时才会报告语法和类型错误。
E1058
在 :def 或 :function 里可以嵌套另一个 :def ,最多
可达 49 层。如果试图嵌套 50 或更深层,会报错 E1058 。
E1117
[!] 只能用于老式 Vim 脚本,因为它允许函数重定义 (和
:function ! 相同)。在 Vim9 脚本里,不接受 ! 形式,因
为脚本局部函数不能被删除或重定义,不过,重新载入相同的
脚本可以移除函数的定义。另外,也不能用 ! 来重定义嵌套
函数。示例: >vim
" 老式 Vim 脚本的 :def! 示例
def! LegacyFunc()
echo "def! 在老式 Vim 脚本里合法"
enddef
call LegacyFunc()
<
vim9script
def Func()
def! InnerFunc()
# E1117: 不能对嵌套的 :def 使用 !
enddef
enddef
Func()
vim9script
def! F_477(): void # E477: 不能使用 "!"
enddef
vim9script
def F_1084(): void
enddef
delfunction! F_1084
# E1084: 无法删除 Vim9 函数 F_1084
注意: 通用错误 E1028 ("编译 :def 函数失败") 代表编译
时出现了无法确定的错误。如果此错误可以重现,请在
https://github.com/vim/vim/issues 处提交报告,因为它可
能反映了 Vim 已有错误报告系统的不足。
:enddef E1057 E1152 E1173
:enddef 结束 :def 定义的函数。必须单独起一行。示例:
vim9script
def MyFunc()
echo 'Do Something' | enddef
# E1057: 缺少 :enddef
vim9script
def F_1173()
enddef echo 'X'
# E1173: 在 enddef 后发现的文本: echo 'X'
vim9script
def F_1152()
function X()
enddef # E1152: 不匹配的 enddef
enddef
以下 wiki 可能对您有用。这是 Vim9 脚本的一位早期试用者编写的:
https://github.com/lacygoill/wiki/blob/master/vim/vim9.md
如果 :def 函数是在 Vim9 脚本里定义的,可以不经 "s:" 前缀直接访问脚本局部变
量。但这些变量必须在函数编译之前被定义。无法通过条件判断来跳过未声明的变量 (例
如通过 exists() ) 以避免错误发生。例如:
vim9script
def MyVim9def()
echo unus # 回显 1
# echo s:unus # 会出错 E1268 (在 Vim9 脚本中不能使用 s:
if exists('duo')
# echo duo # 会出错 E1001 (找不到变量: duo)
endif
enddef
var unus: number = 1
MyVim9def() # MyVim9def 此时被编译 ("duo" 此时尚不存在)
var duo: number = 2
如果 :def 函数是在老式脚本里定义的,脚本局部变量可用 "s:" 前缀,也可不用。不
过,使用 "s:" 会将变量解析延迟到运行时进行,从而避免因为变量尚不存在触发编译错
误,如下例所示: >vim
" 老式 Vim 脚本
def! MyLegacyDef(): void
echo [unus, s:unus] # 回显 [1, 1]
# (如果取消下行的注释) 第一次执行 'echo s:duo' 会报错 E121 并导致
# 编译错误;后续再执行时,会回显 2:
# echo s:duo
if exists("s:duo")
# 首次执行时: 跳过回显;后续再执行时: 回显 2
echo s:duo
endif
if exists("duo")
# (如果取消下行的注释) 第一次执行 'echo s:duo' 会报错 E1001 并
# 导致编译错误;后续再执行时,会回显 2:
# echo duo
endif
enddef
let s:unus = 1
call MyLegacyDef() " 调用 MyLegacyDef() 并进行编译 (如果还没编译过)
let s:duo = 2
< E1269
Vim9 脚本里脚本局部变量必须在脚本级别声明,而不能在 :def 函数里创建,也不
能在老式函数里试图使用 "s:" 前缀来声明脚本局部变量。例如:
vim9script
function F_1269()
let s:i_wish = v:true
endfunction
F_1269()
# E1269: 在函数中不能创建 Vim9 脚本变量: s:i_wish
:defc :defcompile
:defc[ompile] 编译尚未编译的在当前脚本中定义的函数和类
( class-compile )。编译中如有任何错误,会报错。
示例: 执行下例中的前三行时 (直到 enddef 为止 (包
含)),不会报错,因为尚未编译该 Vim9 :def 函数。但当
执行完整的四行时,编译会失败:
vim9script
def F_1027(): string
enddef
defcompile F_1027 # E1027: 缺少返回语句
:defc[ompile] MyClass 编译类里的所有方法。(例见 :disassemble 。)
:defc[ompile] {func}
:defc[ompile] debug {func}
:defc[ompile] profile {func}
有需要的话编译函数 {func}。"debug" 和 "profile" 指定不
同的编译模式。
编译中如有任何错误,会报错。
{func} 也可以是 "ClassName.functionName",以编译指定类
中的函数或方法。
{func} 还可以是 "ClassName",以编译指定类中的所有函数
和方法。
:disa :disassemble
:disa[ssemble] {func} 显示 {func} 生成的指令序列 (反汇编)。
用于调试和测试。
如果 {func} 不存在,报错 E1061 。
{func} 也可以是 "ClassName.functionName",用以反汇编指
定类中的函数。
下例展示了如何向 :defcompile 传递 class 参数,同时
向 :disassemble 传递 "ClassName.functionName" 参数
(光标会定位在可视执行脚本的末行):
vim9script
class Line
var lnum: number
def new(this.lnum)
enddef
def SetLnum()
cursor(this.lnum, 52)
enddef
endclass
defcompile Line
disassemble Line.SetLnum
var vlast: Line = Line.new(line("'>"))
vlast.SetLnum() # 光标会定位在此处->_
:disa[ssemble] profile {func}
类似于 :disassemble 但指令用于刨视。
:disa[ssemble] debug {func}
类似于 :disassemble 但指令用于调试。
注意: 命令行补全 {func} 时,脚本局部函数会显示相应的 <SNR> 前缀。取决于包括
wildmenumode() 在内的选项设置,补全可能会作用于 "s:"、"<S" 或者函数
名本身。(例如,假定 Vim 启动时传递了 -u NONE,":disa s:" 和
c_CTRL-E 在补全时都会列出脚本局部函数名。)
局限
计算字符串表达式时,局部于 `:def 函数的变量不可见。下例展示了局部于脚本的常量
"SCRIPT_LOCAL" 可见,但局部于函数的常量 "DEF_LOCAL" 却不可见:
vim9script
const SCRIPT_LOCAL = ['A', 'script-local', 'list']
def MapList(scope: string): list<string>
const DEF_LOCAL: list<string> = ['A', 'def-local', 'list']
if scope == 'script local'
return [1]->map('SCRIPT_LOCAL[v:val]')
else
return [1]->map('DEF_LOCAL[v:val]')
endif
enddef
echo 'script local'->MapList() # 回显 ['script-local']
echo 'def local'->MapList() # E121: 未定义的变量: DEF_LOCAL
这里 map 参数是字符串表达式,它的计算会在函数作用域之外。要避免此局限,在 Vim9
脚本里,可用匿名函数代替:
vim9script
def MapList(): list<string>
const DEF_LOCAL: list<string> = ['A', 'def-local', 'list']
return [1]->map((_, v) => DEF_LOCAL[v])
enddef
echo MapList() # 回显 ['def-local']
对于未编译的命令,如 :edit ,可用反引号扩展 backtick-expansion ,那里局部作
用域可见。例如:
vim9script
def EditNewBlah()
var fname: string = 'blah.txt'
split
edit `=fname`
enddef
EditNewBlah() # 创建新分割,显示缓冲区 'blah.txt'
同一循环里定义的闭包或者会分享同一变量的引用,或者每个闭包会有变量的不同备份,
这取决于变量声明所在的位置。变量在循环之外声明时,所有的闭包会引用相同的共享变
量。下例展示了 "outloop" 变量只有一个备份时会有的效果:
vim9script
var flist: list<func>
def ClosureEg(n: number): void
var outloop: number = 0 # outloop 在循环外声明!
for i in range(n)
outloop = i
flist[i] = (): number => outloop # 闭包都引用同一变量
endfor
echo range(n)->map((i, _) => flist[i]())
enddef
ClosureEg(4) # 回显 [3, 3, 3, 3]
放置在列表里的所有闭包都引用相同的实例,该变量最终的值为 3。
不过,如果变量在循环内声明,每个闭包会获得自己专有的备份,如下例所示:
vim9script
var flist: list<func>
def ClosureEg(n: number): void
for i in range(n)
var inloop: number = i # inloop 在循环内声明
flist[i] = (): number => inloop # 闭包引用不同的 inloop 实例
endfor
echo range(n)->map((i, _) => flist[i]())
enddef
ClosureEg(4) # 回显 [0, 1, 2, 3]
让每个闭包有自己的上下文的另一方法是调用函数,在其中定义闭包:
vim9script
def GetClosure(i: number): func
var infunc: number = i
return (): number => infunc
enddef
var flist: list<func>
def ClosureEg(n: number): void
for i in range(n)
flist[i] = GetClosure(i)
endfor
echo range(n)->map((i, _) => flist[i]())
enddef
ClosureEg(4) # 回显 [0, 1, 2, 3]
E1271
闭包必须在其定义所在的上下文里进行编译,以便找到在该上下文里的变量。大多数情况
下这没有问题,但在编译函数后,再用 :breakadd 标记该函数进行调试时会出问题。
为了避免这些问题,请在编译外层函数前就定义好断点。
E1248
有些情况下,如捕获局部变量的 Vim9 闭包先转换为字符串再执行时,会出错。这是因为
字符串执行的上下文不能访问闭包定义所在的上下文里的局部变量。例如:
vim9script
def F_1248(): void
var n: number
var F: func = () => {
n += 1
}
try
execute printf("call %s()", F)
catch
echo v:exception
endtry
enddef
F_1248() # Vim(call):E1248: 从无效上下文中调用了闭包
在 Vim9 脚本里,循环变量在循环结束后不再有效。下例中定时器会回显 0 到 2,每个
数值一行。不过,如果在 :endfor 之后试图使用 "n" 变量,会报错 E121 :
vim9script
for n in range(3)
var nr: number = n
timer_start(1000 * n, (_) => {
echowindow nr
})
endfor
try
echowindow n
catch
echo v:exception
endtry
备注: 定时器中 :echowindow 很有用,因为它会使得信息在弹出中出现,
触发时不会干扰用户正在进行中的操作。
把 :function 转换为 :def
convert_legacy_function_to_vim9
convert_:function_to_:def
要把 :function 转换为 :def 函数需要不少改动。这里列出一些需要的改动:
- 把用于声明变量的 let 改为 var 、 const 或 final 之一,并删除所有
script-variable 的 "s:" 前缀。
- 把 func 或 function 改为 def 。
- 把 endfunc 或 endfunction 改为 enddef 。
- 为每个函数参数加上合适的类型 (或者 "any")。
- 删除所有 function-argument 的 "a:" 前缀。
- 删除不再适用的选项,如 :func-range 、 :func-abort 、 :func-dict 和
:func-closure 。
- 如果函数有返回值,加上返回类型。(如果函数不返回任何值,最好加上 "void"。)
- 删除那些在 Vim9 里不再需要的作为续行符的反斜杠。
- 给 g: 、 b: 、 w: 、 t: 或 l: 变量赋值时删除 let 。
- 使用 Vim9 脚本语法改写 lambda 表达式 (见 vim9-lambda )。
- 改变注释的引导符,注释以 # (以空白前导) 开始,而不再是 "。
- 在表达式里必要的地方插入空格 (见 vim9-white-space )。
- 把用于字符串连接的 "." 改为 " .. "。(替代方案是使用 interpolated-string 。)
下面的老式 Vim 脚本和 Vim9 脚本示例展示了上述的所有区别。首先给出老式 Vim 脚本
版本: >vim
let s:lnum=0
function Leg8(arg) abort
let l:pre=['结果',
\': ']
let b:arg=a:arg
let s:lnum+=2
let b:arg*=4
let l:result={pre->join(pre,'')}(l:pre)
return l:result.(b:arg+s:lnum)"注释前不需空白
endfunction
call Leg8(10)->popup_notification(#{time: 3000})" 弹出 '结果: 42'
等价的 Vim9 脚本:
vim9script
var lnum: number
def Vim9(arg: number): string
final pre = ['结果',
': ']
b:arg = arg
lnum += 2
b:arg *= 4
const RESULT: string = ((lpre) => join(lpre, ''))(pre)
return RESULT .. (b:arg + lnum) # #-开头的注释前需要空白
enddef
Vim9(10)->popup_notification({time: 3000}) # 弹出 '结果: 42'
备注: 此例也展示了 (和 :def 函数的改写无关):
- 老式 #{} 里的 "#" 被删除 - 见 vim9-literal-dict
- :call 被省略 (Vim9 脚本里允许,但不必要)
在表达式选项里调用 :def 函数
expr-option-function
若干选项如 'foldexpr' 的值是一个表达式,该表达式会先经过计算再取得需要的值。这
里的计算会有相当负担。一个尽量减少负担同时也使选项值保持简单的方法,是先定义一
个编译过的函数,然后设置选项为该函数不带参数的调用。例如:
vim9script
def MyFoldFunc(): string
# 此模式匹配从行首 (^) 开始,后跟一个数位、一个句号、一个空格或制
# 表和一个非英文字符所组成的行,并且下一行必须为空行
return getline(v:lnum) =~
'^[[:digit:]]\.[[:blank:]][^\%u0001-\%u00ff]'
&& getline(v:lnum + 1)->empty() ? '>1' : '1'
enddef
set foldexpr=MyFoldFunc()
set foldmethod=expr
norm! zM
警告: 此脚本在本 vim9.cnx 帮助缓冲区的一级标题级别上,创建和应用折叠。(执行
脚本后,在普通模式下可用 zR 以打开所有的折叠。)
4. 类型 vim9-types
支持以下类型,这里也显示了它们对应的内部 v:t_TYPE 变量:
number v:t_number
string v:t_string
func v:t_func
func: {type} v:t_func
func({type}, ...) v:t_func
func({type}, ...): {type} v:t_func
list<{type}> v:t_list
dict<{type}> v:t_dict
float v:t_float
bool v:t_bool
none v:t_none
job v:t_job
channel v:t_channel
blob v:t_blob
class v:t_class
object v:t_object
typealias v:t_typealias
enum v:t_enum
enumvalue v:t_enumvalue
tuple<{type}> v:t_tuple
tuple<{type}, {type}, ...> v:t_tuple
tuple<...list<{type}>> v:t_tuple
tuple<{type}, ...list<{type}>> v:t_tuple
void
E1031 E1186
这些类型可用于声明,但没有任何简单值会对应 "void" 类型。试图使用 void 作为值类
型会报错。示例:
vim9script
def NoReturnValue(): void
enddef
try
const X: any = NoReturnValue()
catch
echo v:exception # E1031: 不能使用空值
try
echo NoReturnValue()
catch
echo v:exception # E1186: 表达式不产生值: NoReturnValue()
endtry
endtry
E1008 E1009 E1010 E1012
不合乎规格的声明和不匹配的类型会报错。下面例子会分别给出错误 E1008、E1009、
E1010 和 E1012:
vim9cmd var l: list
vim9cmd var l: list<number
vim9cmd var l: list<invalidtype>
vim9cmd var l: list<number> = ['42']
Vim 没有数组类型,而用列表或元组代替。这些类型也可使用字面量 (常量)。下列中,
[5, 6] 是列表常量,而 (7, ) 是元组常量。回显的列表也是一个列表常量:
vim9script
var l: list<number> = [1, 2]
var t: tuple<...list<number>> = (3, 4)
echo [l, t, [5, 6], (7, )]
tuple-type
元组类型可用下列的方式声明:
tuple<number> 带单个类型 Number 的项目的元组
tuple<number, string> 带两个类型 Number 和 String 的项目的元组
tuple<number, float, bool> 带三个类型 Number 、 Float 和 Boolean 的项
目的元组
tuple<...list<number>> 带可变参数零或多个类型 Number 的项目的元组
tuple<number, ...list<string>> 带一个类型 Number 的项目,后跟零或多个类型
String 的项目的元组
示例:
var myTuple: tuple<number> = (20,)
var myTuple: tuple<number, string> = (30, 'vim')
var myTuple: tuple<number, float, bool> = (40, 1.1, true)
var myTuple: tuple<...list<string>> = ('a', 'b', 'c')
var myTuple: tuple<number, ...list<string>> = (3, 'a', 'b', 'c')
variadic-tuple E1539
可变参数元组有零或多个相同类型的项目。可变参数元组必须以列表类型结尾。例如:
var myTuple: tuple<...list<number>> = (1, 2, 3)
var myTuple: tuple<...list<string>> = ('a', 'b', 'c')
var myTuple: tuple<...list<bool>> = ()
vim9-func-declaration E1005 E1007
vim9-partial-declaration
vim9-func-type
偏函数和函数可用以下方式声明:
func 任何类型的函数引用,不检查参数和返回值类型
func: void 任何数量和类型的参数,无返回值
func: {type} 任何数量和类型的参数,特定返回类型
func() 无参数的函数,无返回值
func(): void 同上
func(): {type} 无参数的函数,指定返回类型
func({type}) 指定参数类型的函数,无返回值
func({type}): {type} 指定参数类型和返回类型的函数
func(?{type}) 指定可选参数类型的函数,无返回值
func(...list<{type}>) 带可变数目的指定类型的参数列表的函数,无返回值
func({type}, ?{type}, ...list<{type}>): {type}
带以下的函数:
- 必选参数的类型
- 可选参数的类型
- 可变数目参数列表的类型
- 返回类型
如果返回类型为 "void",函数什么都不返回。
引用也可是 Partial ,此时它保存了额外参数和/或字典,而这些对调用者是不可见
的。因为调用方式相同,声明方式也是一致的。以下使用偏函数的交互示例会提示输入圆
的半径,并返回其面积,精确到两位小数:
vim9script
def CircleArea(pi: float, radius: float): float
return pi * radius->pow(2)
enddef
const AREA: func(float): float = CircleArea->function([3.14])
const RADIUS: float = "输入半径: "->input()->str2float()
echo $"\n半径为 {RADIUS} 的圆的面积为 " ..
$"{AREA(RADIUS)} (π 精确到两位小数)"
vim9-typealias-type
可用 :type 定义定制类型 ( typealias )。定制类型必须以大写字母开头 (避免与当
前或之后新增的内建类型名字起冲突),这和用户函数类似。下例创建完全平方的列表,
并报告其 type() (14,typealias 的类型值) 和 typename() :
vim9script
type Ln = list<number>
final perfect_squares: Ln = [1, 4, 9, 16, 25]
echo "Typename (Ln): " ..
$"type() 为 {Ln->type()} 而 typename() 为 {Ln->typename()}"
E1105
typealias 自身不能转换为字符串:
vim9script
type Ln = list<number>
const FAILS: func = (): string => {
echo $"{Ln}" # E1105: 无法将 typealias 转换为字符串
}
vim9-class-type vim9-interface-type
vim9-object-type
class 、 object 和 interface 都可用作类型。以下的交互示例会提示输入浮点
数,并返回两种不同形状的面积。此示例也会同时报告类、对象和界面的 type() 和
typename() :
vim9script
interface Shape
def InfoArea(): tuple<string, float>
endinterface
class Circle implements Shape
var radius: float
def InfoArea(): tuple<string, float>
return ('圆 (π × r²)', 3.141593 * this.radius->pow(2))
enddef
endclass
class Square implements Shape
var side: float
def InfoArea(): tuple<string, float>
return ('正方形 (s²)', this.side->pow(2))
enddef
endclass
const INPUT: float = "输入浮点数: "->input()->str2float()
echo "\n形状的面积:"
var myCircle: object<Circle> = Circle.new(INPUT)
var mySquare: object<Square> = Square.new(INPUT)
final shapes: list<Shape> = [myCircle, mySquare]
for shape in shapes
const [N: string, A: float] = shape.InfoArea()
echo $"\t- {N} 的面积是 {A}"
endfor
echo "\n\t\ttype()\ttypename()\n\t\t------\t----------"
echo $"Circle\t\t{Circle->type()}\t{Circle->typename()}"
echo $"Square\t\t{Square->type()}\t{Square->typename()}"
echo $"Shape\t\t{Shape->type()}\t{Shape->typename()}"
echo $"MyCircle\t{myCircle->type()}\t{myCircle->typename()}"
echo $"MySquare\t{mySquare->type()}\t{mySquare->typename()}"
echo $"shapes\t\t{shapes->type()}\t{shapes->typename()}"
vim9-enum-type vim9-enumvalue-type
enum 也可用作类型 ( v:t_enum )。保存枚举值的变量在运行时会有枚举值类型
( v:t_enumvalue )。以下的交互示例会提示输入一个字符,并返回正方形或菱形的相关
信息。此示例也会同时报告枚举和枚举值的 type() 和 typename() :
vim9script
enum Quad
Square('四条', '只有'),
Rhombus('相对的', '没有')
var eq: string
var ra: string
def string(): string
return $"\n{this.name} 有" ..
$"{this.eq}等边,而且{this.ra}直角\n\n"
enddef
endenum
echo "菱形 (r) 还是正方形 (s)?"
var myQuad: Quad = getcharstr() =~ '\c^R' ? Quad.Rhombus : Quad.Square
echo myQuad.string() .. "\ttype()\ttypename()"
echo $"Quad \t{Quad->type()} \t{Quad->typename()}"
echo $"myQuad\t{myQuad->type()}\t{myQuad->typename()}"
备注: 此脚本使用内建方法 "string()" ( object-string() )。
Quad 和 myQuad 的 typename() 相同 ("enum<Quad>"),但 type() 不
同 (myQuad 返回 16,枚举值的类型值,而 Quad 返回 15,枚举的类型
值)。
变量类型和类型转换
variable-types
Vim9 脚本或 :def 函数中声明的变量都有类型,或者通过显式指定,或者通过初始化
推导得出。
全局、缓冲区、窗口和标签页变量没有特定类型。因而,其值可在任何时候改变,类型也
会同时随之改变。为此,编译后的代码假定这些变量使用 "any" 类型。
如果希望有更严格的类型检查,这会有问题。例如,声明列表时:
var l: list<number> = [1, b:two]
因为 Vim 不知道 "b:two" 的类型,表达式类型会因此变成 list<any>。这种情况下,运
行时会在赋值前检查列表是否匹配声明的类型。
type-casting
要进行更精确的类型检查,可使用类型转换。这样,在构造列表前会先检查变量的类型,
而不是在赋值前检查列表项是否匹配声明的类型。例如:
var l: list<number> = [1, <number>b:two]
这里的类型转换会检查 "b:two" 是否为数值,如果不是会报错。
下例展示其中的区别。在函数引用变量 "NTC" 里,Vim 推断表达式 "[1, b:two]" 的类
型为 list<any>,然后会检查它是否可以赋值给返回值类型 list<number>。而在函数引
用变量 "TC" 里,类型转换会使 Vim 先检查 "b:two" 是否为 <number> 类型:
vim9script
b:two = '2'
const NTC: func = (): list<number> => {
return [1, b:two]
}
disassemble NTC # 3 CHECKTYPE list<number> stack [-1]
try
NTC()
catch
echo v:exception .. "\n\n" # 预期 list<number> 但得到 list<any>
endtry
const TC: func = (): list<number> => {
return [1, <number>b:two]
}
disassemble TC # 2 CHECKTYPE number stack [-1]
try
TC()
catch
echo v:exception # 预期 number 但得到 string
endtry
备注: 可以从错误信息的差异看出,类型检查是在不同的阶段发生的。
E1104
类型转换的语法是 "<{type}>"。如果缺失了 "<" ( E121 ) 或者 ">" ( E1104 ),会报
错。另外,"<" 之后 ( E15 ) 或 ">" 之前 ( E1068 ) 不能有空格,以避免和小于号以
及大于号操作符混淆。
虽然类型转换会强制类型检查,但是它不会更改变量的值以及类型。如果要改变类型,可
用 string() 函数来转换为字符串型,或者用 str2nr() 来把字符串转换为数值。
应用在链式表达式上时,类型转换必须和最终的结果兼容。例如:
vim9script
# 以下这些类型转换可行
echo <list<any>>[3, 2, 1]->extend(['Go!'])
echo <string>[3, 2, 1]->extend(['Go!'])->string()
echo <tuple<...list<number>>>[3, 2, 1]->list2tuple()
# 以下类型转换会失败
echo <number>[3, 2, 1]->extend(['Go!'])->string()
E1272
如果在不期待类型的上下文中使用类型,会报错 E1272。例如:
:vim9cmd echo islocked('x: string')
备注: 本例必须在 Vim 的命令行上运行,不能用 :source 。
E1363
如果类型不完整,例如对象所属的类未知,报错 E1363。例如:
vim9script
var E1363 = null_class.member # E1363: Incomplete type
另一个和 null 对象相关的错误是 E1360 :
vim9script
var obj = null_object
var E1360 = obj.MyMethod() # E1360: Using a null object
类型推导
type-inference
显式声明类型有很多优点,包括精确的类型检查和更清晰的错误信息。不过,Vim 在类型
省略时常常可以自动推断类型。例如,以下每个变量的类型都可以被推导出来, type()
和 typename() 会回显推断后的类型:
vim9script
echo "\t type()\t typename()"
var b = true | echo $"{b} \t {b->type()} \t {b->typename()}"
var f = 4.2 | echo $"{f} \t {f->type()} \t {f->typename()}"
var l = [1, 2] | echo $"{l} \t {l->type()} \t {l->typename()}"
var n = 42 | echo $"{n} \t {n->type()} \t {n->typename()}"
var s = 'yes' | echo $"{s} \t {s->type()} \t {s->typename()}"
var t = (42, ) | echo $"{t} \t {t->type()} \t {t->typename()}"
列表、元组和字典的类型来自其值的公共类型。如果所有值为相同类型,则使用该类型。
如果有不同类型的混合,则用 "any" 类型。下例中,每个常量回显的 typename() 展
示了这些情况:
vim9script
echo [1, 2]->typename() # list<number>
echo [1, 'x']->typename() # list<any>
echo {ints: [1, 2], bools: [false]}->typename() # dict<list<any>>
echo (true, false)->typename() # tuple<bool, bool>
函数引用的公共类型里,如果参数数目不尽相同,会用 "(...)" 来指明参数数目不确
定。下例展示了 "list<func(...): void>":
vim9script
def Foo(x: bool): void
enddef
def Bar(x: bool, y: bool): void
enddef
var funclist = [Foo, Bar]
echo funclist->typename()
Vim9 脚本里脚本局部变量会进行类型检查,即使对在老式函数里声明的变量也会。
例如:
vim9script
var my_local = (1, 2)
function Legacy()
let b:legacy = [1, 2]
endfunction
Legacy()
echo $"{my_local} 的类型是 {my_local->type()} ({my_local->typename()})"
echo $"{b:legacy} 的类型是 {b:legacy->type()} ({b:legacy->typename()})"
E1013
一个列表、元组或字典声明类型后,该类型会与之捆绑。类似地,即使未显式声明类型,
Vim 也会用推断出来的类型与之捆绑。不管哪种情况,如果在此之后有表达式试图改变类
型,会报错 E1013。下例展示了类型推断和 E1013:
vim9script
var lb = [true, true] # 两个 bool,所以 Vim 会推断为 list<bool> 类型
echo lb->typename() # 回显 list<bool>
lb->extend([0]) # E1013 参数 2: 类型不匹配,...
如果要接受多种类型的列表,可以显式使用 <any>,或者声明时用空列表来初始化 (或者
两者兼有,如 `list<any> = []`)。示例:
vim9script
final la: list<any> = []
echo la->extend(['two', 1])
final le = []
echo le->extend(la)
类似地,要接受多种类型的字典:
vim9script
final da: dict<any> = {}
echo da->extend({2: 2, 1: 'One'})
final de = {}
echo de->extend(da)->string()
另外,虽然元组本身是只读的,但可以通过 "any" 或空元组来实现能接受多种类型的元
组连接操作:
vim9script
var t_any: tuple<...list<any>> = (3, '2')
t_any = t_any + (true, )
echo t_any
var t_dec_empty = ()
t_dec_empty = t_dec_empty + (3, '2', true)
echo t_dec_empty
如果列表常量或字典常量没有和变量绑定,则其类型可变,如下例所示:
vim9script
echo [3, 2, 1]->typename() # list<number>
echo [3, 2, 1]->extend(['Zero'])->typename() # list<any>
echo {1: ['One']}->typename() # dict<list<string>>
echo {1: ['One']}->extend({2: [2]})->typename() # dict<list<any>>
更严格的类型检查
type-checking
在老式 Vim 脚本中,在本应是数值的地方使用了字符串,Vim 会自动转换为数值。这对
像 "123" 这样实际为数值的情形很方便,但对不以数值开始的字符串会出现意料不到的
问题 (且不报错)。这种特性常常会导致很难发现的漏洞。例如,在老式 Vim 脚本里,下
例会回显 "1": >vim
echo 123 == '123'
<
不过,如果不小心多了一个空格,就会回显 "0": >vim
echo 123 == ' 123'
<
E1206
在 Vim9 脚本中,类型检查会更严格。只要使用的值匹配期待的类型,绝大多数地方会和
以前一样正常工作。例如,在老式 Vim 脚本和 Vim9 脚本里,在期待字典的地方试图用
其他类型都会报错: >vim
echo [8, 9]->keys()
vim9cmd echo [8, 9]->keys() # E1206: 参数 1 需要字典
<
E1023 E1024 E1029 E1030
E1174 E1175 E1210 E1212
不过,有些情况下, Vim9 脚本会报错,而老式脚本里不会,这会导致后向不兼容。下面
的示例展示了各种不兼容的情况。先给出老式 Vim 脚本中不报错时的行为。然后再展示
相同的命令在 Vim9 脚本里触发的错误。
- 期待布尔值时使用了数值 (0 或 1 除外): >vim
echo v:version ? v:true : v:false
vim9cmd echo v:version ? true : false # E1023: 将整数作布尔值使用...
<
- 期待字符串时使用了数值: >vim
echo filter([1, 2], 0)
vim9cmd echo filter([1, 2], 0) # E1024: 将整数作字符串使用
<
- 期待数值时不使用数值: >vim
" 此例中,Vim 脚本把 v:false 视作 0
function Not1029()
let b:l = [42] | unlet b:l[v:false]
endfunction
call Not1029() | echo b:l
<
vim9script
def E1029(): void
b:l = [42] | unlet b:l[false]
enddef
E1029() # E1029: 预期 number 但得到 bool
- 将字符串用作数值: >vim
let b:l = [42] | unlet b:l['#'] | echo b:l
vim9cmd b:l = [42] | vim9cmd unlet b:l['#'] # E1030: 将字符串作整...
<
- 参数需要字符串时不使用字符串:
echo substitute('Hallo', 'a', 'e', v:true)
vim9cmd echo substitute('Hallo', 'a', 'e', true) # E1174: 需要字符串
- 参数需要非空字符串时使用空串:
echo exepath('')
vim9cmd echo exepath('') # E1175: 参数 1 需要非空字符串
- 参数需要数值时不使用数值: >vim
echo gettabinfo('a')
vim9cmd echo gettabinfo('a') # E1210: 参数 1 需要整数
<
- 参数需要布尔值时不使用布尔值: >vim
echo char2nr('¡', 2)
vim9cmd echo char2nr('¡', 2) # E1212: 参数 2 需要布尔值
<
- 赋值需要数值时不使用数值 ( E521 ): >vim
let &laststatus='2'
vim9cmd &laststatus = '2'
<
- 赋值需要字符串时不使用字符串 ( E928 ): >vim
let &langmenu = 42
vim9cmd &langmenu = 42 # E928: 需要字符串
<
- 将 Special 用 'is' 作比较有些情况下会失败 ( E1037 , E1072 ): >vim
" 回显 1,因为两次比较均返回 true
echo v:null is v:null && v:none is v:none
" 回显 0,因为所有比较均返回 false
echo v:none is v:null || v:none is 8 || v:true is v:none
" Vim9 脚本里以下均报错
vim9cmd echo v:null is v:null # E1037: 不能对 special 使用 'is'
vim9cmd echo v:none is v:none # E1037: 不能对 special 使用 'is'
vim9cmd echo v:none is v:null # E1037: 不能对 special 使用 'is'
vim9cmd echo v:none is 8 # E1072: 不能比较 special 和 number
vim9cmd echo v:true is v:none # E1072: 不能比较 bool 和 special
<
备注: 在最后两例中,Vim9 脚本中如果用 v:none 会报错,但若改用
null (等同于 v:null - 见 v:null ) 则会返回 false :
vim9script
echo null is 8 # false
echo true is null # false
- 期待布尔值时使用字符串 ( E1135 ): >vim
echo '42' ? v:true : v:false
vim9cmd echo '42' ? true : false # E1135: 将字符串作布尔值使用: "42"
<
- 期待数值时使用布尔值 ( E1138 ): >vim
let &laststatus=v:true
vim9cmd &laststatus = true
<
- 参数需要字符串时不使用字符串 ( E1174 ) >vim
echo substitute('Hallo', 'a', 'e', v:true)
vim9cmd echo substitute('Hallo', 'a', 'e', true) # E1174: 需要字符串
<
一个后果是,如果项目类型已声明或被推导得到,传递给 map() 的列表或字典不能再
更改其项目类型。下例中,Vim9 脚本里会报错但老式 Vim 脚本里不会: >vim
" 老式 Vim 脚本会将 s:mylist 修改为 ['item 0', 'item 1']
let s:mylist = [0, 1]
call map(s:mylist, {i -> $"item {i}"})
echo s:mylist
<
vim9script
var mylist = [0, 1] # Vim 推导 mylist 为 list<number>
map(mylist, (i, _) => $"item {i}") # E1012: 类型不匹配...
此错误之所以发生,是因为 map() 会试图修改列表项为字符串值,而这和列表已声明
的类型不相符。
为了避免这个问题,可用 mapnew() 代替 map() 。它会创建新列表,未指定类型时
Vim 会自动推导其类型。下例分别展示了推导出的和显式声明的类型:
vim9script
var mylist = [0, 1]
var infer = mylist->mapnew((i, _) => $"item {i}")
echo [infer, infer->typename()]
var declare: list<string> = mylist->mapnew((i, _) => $"item {i}")
echo [declare, declare->typename()]
这里的关键概念是,带有已声明或推导得到类型的变量,不能随着包含该变量的容器类型
改变而改变。不过,下面情况可以接受类型的 "改变":
- 使用容器常量 (不和任何变量绑定),或者
- 使用 copy() 或 deepcopy() 的方法链式调用。
这两种情况在下例中都有所展示:
vim9script
# 列表常量
echo [1, 2]->map((_, v) => $"#{v}")
echo [1, 2]->map((_, v) => $"#{v}")->typename()
# 用 deepcopy() 的方法链式调用
var mylist = [1, 2]
echo mylist->deepcopy()->map((_, v) => $"#{v}")
echo mylist->deepcopy()->map((_, v) => $"#{v}")->typename()
echo mylist
这里背后的逻辑是,列表被传递和修改时,如果类型已被声明或推导,必须保证已经声
明/推导得到的类型保持不变,这样才能依靠类型来匹配已声明的类型。而对列表常量或
被完整复制的列表,不需要类型安全,因为原先的列表未被修改 (如果上例中 "echo
mylist" 所示)。
如果项目类型未声明而被推导为 "<any>",即使之后所有的项目变成了相同类型,推导类
型不会改变。不过,使用 mapnew() 时,新的推导会使新列表正确反映新类型。
例如:
vim9script
# list<any>
var mylist = [1, '2'] # 混合类型,也就是 list<any>
echo (mylist, mylist->typename()) # ([1, '2'], 'list<any>')
mylist->map((_, v) => $"item {v}") # 所有值都成了字符串类型
echo (mylist, mylist->typename()) # 都是字符串,但还是 list<any>
# mapnew()
var newlist = mylist->mapnew((_, v) => v)
echo (newlist, newlist->typename()) # newlist 变成了 list<string>
extend() 和 extendnew() 情况也类似,同样地,前者可用列表常量,所以下例没问
题:
vim9cmd echo [1, 2]->extend(['3']) # [1, 2, 3]
但下例不行:
vim9script
var mylist: list<number> = [1, 2]
echo mylist->extend(['3']) # E1013: 参数 2: 类型不匹配...
要扩充类型确定的已有列表,必须用 extendnew() ,除非扩充内容和列表的现有类型匹
配 (也包括 "any")。例如,要先扩充一个相同类型的项目,再扩充一个不同类型的项
目,可用:
vim9script
var mylist: list<number> = [1, 2]
mylist->extend([3])
echo mylist->extendnew(['4']) # [1, 2, 3, '4']
E1158
Vim9 脚本不支持 flatten() ,因为它的目的就是修改类型。即使对列表常量也是如此
(这一点和 map() 和 extend() 不同)。必须使用 flattennew() 代替:
vim9cmd [1, [2, 3]]->flatten() # E1158: Vim9 中不能用 flatten
vim9cmd echo [1, [2, 3]]->flattennew() # [1, 2, 3]
赋值给指定参数的函数引用 (见 vim9-func-declaration ) 会严格检查参数的类型。
下例可以正常工作:
vim9script
var F_name_age: func(string, number): string
F_name_age = (n: string, a: number): string => $"Name: {n}, Age: {a}"
echo F_name_age('Bob', 42)
而下例会报错 E1012 (类型不匹配):
vim9script
var F_name_age: func(string, number): string
F_name_age = (n: string, a: string): string => $"Name: {n}, Age: {a}"
可变数目参数必须使用相同类型,如下例所示:
vim9script
var Fproduct: func(...list<number>): number
Fproduct = (...v: list<number>): number => reduce(v, (a, b) => a * b)
echo Fproduct(3, 2, 4) # 回显 24
<any> 可用于需要混合类型的函数:
vim9script
var FlatSort: func(...list<any>): any
FlatSort = (...v: list<any>) => flattennew(v)->sort('n')
echo FlatSort(true, [[[5, 3], 2], 4]) # 回显 [true, 2, 3, 4, 5]
注意: 使用 <any> 的匿名函数赋值给的函数引用不会跳过类型检查。函数引
用的声明类型仍然适用,如下例所示,如果类型不匹配,仍然会在运行
时或编译时报错:
vim9script
var FuncSN: func(string): number
FuncSN = (v: any): number => v->str2nr()
echo FuncSN('162')->nr2char() # 回显 ¢
echo FuncSN(162)->nr2char()) # E1013 (运行时报错)
vim9script
var FuncSN: func(string): number
FuncSN = (v: any): number => v->str2nr()
def FuncSNfail(): void
echo FuncSN('162')->nr2char() # 没有回显,因为 ...
echo FuncSN(162)->nr2char() # 编译时报错
enddef
FuncSNfail()
没有指定参数的函数引用不进行参数类型检查。下例中 FlexArgs 第一次调用时接受字符
串参数,第二次调用时接受列表参数:
vim9script
var FlexArgs: func: string
FlexArgs = (s: string): string => $"It's countdown time {s}..."
echo FlexArgs("everyone")
FlexArgs = (...values: list<string>): string => join(values, ', ')
echo FlexArgs('3', '2', '1', 'GO!')
E1211 E1217 E1218 E1219 E1220 E1221 E1222
E1223 E1224 E1225 E1226 E1228 E1235 E1238
E1251 E1253 E1256 E1297 E1298 E1301 E1528
E1529 E1530 E1531 E1534
大多数内建函数会检查类型,以便查找错误。下面的单行 :vim9 调用内建函数的命令
展示了许多类型检查相关的错误:
vim9 9->list2blob() # E1211: 参数 1 需要列表
vim9 9->ch_close() # E1217: 参数 1 需要通道或任务
vim9 9->job_info() # E1218: 参数 1 需要任务
vim9 [9]->cos() # E1219: 参数 1 需要浮点数或整数
vim9 {}->remove([]) # E1220: 参数 1 需要字符串或整数
vim9 null_channel->ch_evalraw(9) # E1221: 参数 1 需要字符串或 blob
vim9 9->col() # E1222: 参数 1 需要字符串或列表
vim9 9->complete_add() # E1223: 参数 1 需要字符串或字典
vim9 setbufline(9, 9, {}) # E1224: 参数 3 需要字符串或整数或列
vim9 9->count(9) # E1225: String, List, Tuple or Dict
vim9 9->add(9) # E1226: 参数 1 需要列表或 blob
vim9 9->remove(9) # E1228: 需要列表、字典或 blob
vim9 getcharstr('9') # E1235: Bool or number required for
vim9 9->blob2list() # E1238: 需要 blob
vim9 9->filter(9) # E1251: List, Tuple, Dictionary, Bl
vim9 9->reverse() # E1253: String, List, Tuple or Blob
vim9 9->call(9) # E1256: 参数 1 需要字符串或函数
vim9 null_dict->winrestview() # E1297: Non-NULL Dictionary require
vim9 {}->prop_add_list(null_list) # E1298: Non-NULL List required for
vim9 {}->repeat(9) # E1301: String, Number, List, Tuple
vim9 9->index(9) # E1528: List or Tuple or Blob
vim9 9->join() # E1529: List or Tuple required for
vim9 9->max() # E1530: List or Tuple or Dictionary
vim9 9->get(9) # E1531: Argument of get() must be a
vim9 9->tuple2list() # E1534: Tuple required for argument
保留作将来使用: E1227 E1250 E1252
E1227: 参数 %d 需要列表或字典
E1250: %s 的参数必须是列表、字符串、字典或 blob
E1252: 参数 %d 需要字符串、列表或 blob
变量种类、缺省和 null 处理
variable-categories null-variables
变量有以下三个种类:
原始类型 number、float、boolean
容器类型 string、blob、list、tuple、dict
特殊类型 function、job、channel、user-defined-object
声明没有初始块的变量时,必须显式提供类型。每个种类有不同的缺省初始块语法。
原始类型缺省值为类型特定的初始值。该初始值的 empty() 值为真,但不等于
null :
vim9script
var n: number | echo [n, n->empty(), n == null] # [0, 1, false]
var f: float | echo [f, f->empty(), f == null] # [0.0, 1, false]
var b: bool | echo [b, b->empty(), b == null] # [false, 1, false]
容器类型缺省值为空容器。只有字符串类型的缺省值等于 null :
vim9script
var s: string | echo [s, s->empty(), s == null] # ['', 1, true]
var z: blob | echo [z, z->empty(), z == null] # [0z, 1, false]
var l: list<string> | echo [l, l->empty(), l == null] # [[], 1, false]
var t: tuple<any> | echo [t, t->empty(), t == null] # [(), 1, false]
var d: dict<number> | echo [d, d->empty(), d == null] # [{}, 1, false]
特殊类型缺省值为 null :
vim9script
var F: func | echo [F, F == null] # [function(''), true]
var j: job | echo [j, j == null] # ['no process', true]
var c: channel | echo [c, c == null] # ['channel fail', true]
class Class
endclass
var o: Class | echo [o, o == null] # [object of [unknown], true]
enum Enum
endenum
var e: Enum | echo [e, e == null] # [object of [unknown], true]
备注: empty() 说明了作业、通道和对象类型何时为空。
Vim 没有你熟悉的 null 值;但它有若干预定义的 null_<type> 值,如
null_string 、 null_list 、 null_job 。原始类型则没有相应的 null_<type>。
null_<type> 的典型用法有:
- 用于清除变量,释放其资源,
- 用于作为函数定义里的参数缺省值 (例见 null_blob ),或
- 赋值给容器或特殊类型变量,用于之后和 null 作比较 (例见 null-compare )。
对于像 job 这样的特殊类型变量而言,null_<type> 可用于清理资源。例如:
vim9script
var mydate: list<string>
def Date(channel: channel, msg: string): void
mydate->add(msg)
enddef
var myjob = job_start([&shell, &shellcmdflag, 'date'], {out_cb: Date})
echo [myjob, myjob->job_status()]
sleep 2
echo $"当前日期时间是 {mydate->join('')}"
echo [myjob, myjob->job_status()]
myjob = null_job # 清除变量;并释放作业资源。
echo myjob
对于容器类型变量而言,将空容器值赋给变量也可以用来清理资源。例如:
vim9script
var perfect: list<number> = [1, 4]
perfect->extend([9, 16, 25])
perfect = []
echo perfect
使用空容器而不是 null_<type> 来清除容器类型变量可以避免 null-anomalies 描述
的 null 的复杂性。
容器类型变量和特殊类型变量使用不同的初始块语法。对于容器类型而言:
- 未初始化的容器类型变量缺省为空,但不等于 null (未初始化的字符串除
外)
- 用 []、()、{}、""、或 0z 初始化的容器为空,但不等于 null 。
- 用 null_<type> 初始化的容器缺省为空,且该值等于 null 。
下例中,未初始化的列表 ("lu") 和 [] 初始化的列表 ("li") 是等价的,也无法区分,
而 "ln" 是 null 容器,它与空容器相似但并不等价 (见 null-anomalies )。
vim9script
# 未初始化: 空容器,不等于 null
var lu: list<any>
echo ['lu', $"empty={lu->empty()}", $"null={lu == null}"]
# 已初始化: 空容器,不等于 null
var li: list<any> = []
echo ['li', $"empty={li->empty()}", $"null={li == null}"]
# 已初始化: 空容器,等于 null
var ln: list<any> = null_list
echo ['ln', $"empty={ln->empty()}", $"null={ln == null}"]
特殊类型变量缺省等于 null。下例中作业的不同初始化方式等价且无法区分:
vim9script
var j1: job
var j2: job = null_job
var j3 = null_job
echo (j1 == j2) == (j2 == j3) # true (等价,无法区分)
声明列表、元组或字典时,如果项目类型未指定且无法推导,类型会缺省判定为
"any":
vim9script
var [t1, t2] = [(), null_tuple]
echo $'t1 是 {t1->typename()} 而 t2 也是 {t2->typename()}'
元组和函数 (或者偏函数) 还有若干不同的声明方式。见 tuple-type 、
variadic-tuple 和 vim9-func-declaration 。
null-compare
对于常见的和 null 比较的语法,如果要求空容器和 null 容器不相等,比较时不要使用
null_<type>。这是因为,Vim9 脚本里,虽然 null_<type> == null ,但是,将:
- 空容器和 null 比较会返回 false ,但将
- 空容器和 null_<type> 比较会返回 true 。
所以,请和 null 相比较,而不是 null_<type>。示例:
vim9script
var bonds: dict<list<string>> = {g: ['007', '008'], o: ['007', '009']}
def Search(query: string): list<string>
return query == "\r" ? null_list : bonds->get(query, [])
enddef
echo "金手指 (g) 还是八爪女 (o)?: "
const C: string = getcharstr()
var result: list<string> = C->Search()
if result == null # <<< _此处不要使用_ null_list!
echo "错误: 没有输入"
else
echo result->empty() ? $"没有 '{C}' 的匹配" : $"{result}"
endif
备注: 使用 "result == null_list" 而非 "result == null" 会无法区分错
误 (未输入任何选择) 和合法 (但找不到匹配) 的结果,因为
[] == null_list 而 [] != null。
概念上,可以将 null_<type> 构造想像为通用的 null 容器和有类型的 empty 容器
的一种混合/桥梁,兼有两种属性。下一小节会给出关于比较结果的更多细节。
null-details null-anomalies
本小节描述使用 null 和 null_<type> 的问题;下面列举 null 比较各种可能的结果。
有些情况下,如果熟悉了 vim9 的 null 语法,程序员还是可以选择使用 null_<type>
来进行比较和/或用于其他情景。
在文档的其他地方有说过: "null 值和空值处理方式通常相同,但不绝对”。例如,不能
在 null 容器上添加元素:
vim9script
var le: list<any> = []
le->add('Okay') # le 现在是 ['Okay']
var ln = null_list
ln->add("E1130") # E1130: 不能加到 null 列表
如同 null-compare 已经解释过的, null 、null_<type> 和 empty 容器三者之间
不满足传递关系。下例简单回顾一下:
vim9cmd echo (null_dict == {}, null_dict == null, {} != null)
唯一的例外是未初始化的字符串。它等于 null (和 null_string 是同一实例)。
'is' 操作符 ( expr-is ) 可用于判断字符串是否未初始化:
vim9script
var s: string
echo s == null_string # true
echo s is null_string # true (相同实例)
echo s == null # true (可能意想不到)
echo s is null # false (并非相同实例)
其它容器类型的行为则有所不同,用 'is' 和相应的 null_<type> 比较时,会返回
false :
vim9script
var d: dict<any>
echo d == null_dict # true
echo d is null_dict # false (并非相同实例)
echo d == null # false (意料之中)
echo d is null # false (并非相同实例)
这里关键的区别是,未初始化的字符串的实现是 null_string ,而未初始化的列表、字
典、元组或 blob 的实现是空容器 (分别是 []、{}、() 和 0z)。所以,这些类型的未初
始化值和它们相应的 null_<type> 相等但并非相同实例,如下例所示:
vim9script
var t: tuple<any>
echo t == null_tuple # true
echo t is null_tuple # false
不过,用 null_<type> 初始化的变量不仅和 null_<type> 相等,也和 null 相等。
示例:
vim9script
var t: tuple<any> = null_tuple
echo t == null_tuple # true
echo t is null_tuple # true
echo t == null # true
未初始化的容器类型变量都不等于 null,只有未初始化的字符串除外,这一点,上面已
有解释。所以,下例都会回显 true :
vim9script
var b: blob | echo b != null
var d: dict<any> | echo d != null
var l: list<any> | echo l != null
var t: tuple<any> | echo t != null
var s: string | echo s == null
未初始化的特殊类型变量都等于 null,所以,下例都会回显 true :
vim9script
var c: channel | echo c == null
var F: func | echo F == null
var j: job | echo j == null
class Class
endclass
var nc: Class | echo nc == null
enum Enum
endenum
var ne: Enum | echo ne == null
备注: 像作业这样的特殊类型变量缺省为 null,所以它们没有对应的空值。
用空值初始化的容器类型变量等于 null_<type>,所以,下例都会返回 true :
vim9script
var s: string = "" | echo s == null_string
var b: blob = 0z | echo b == null_blob
var l: list<any> = [] | echo l == null_list
var t: tuple<any> = () | echo t == null_tuple
var d: dict<any> = {} | echo d == null_dict
不过,用空值初始化的容器类型变量不等于 null,所以,下例都会返回 true :
vim9script
var s: string = "" | echo s != null
var b: blob = 0z | echo b != null
var l: list<any> = [] | echo l != null
var t: tuple<any> = () | echo t != null
var d: dict<any> = {} | echo d != null
generic-functions
5. 泛型函数
泛型函数允许使用相同的函数但带不同的类型参数,而保留对参数和返回值的类型检查。
这样可以提供类型安全性和代码重用性。
声明
generic-function-declaration
E1553 E1554
泛型函数用紧跟函数名后的尖括号 "<" 和 ">" 里的类型参数声明其类型变量。多种类型
名以逗号分隔:
def[!] {函数名}<{类型} [, {类型}]>([参数])[: {返回类型}]
{函数体}
enddef
generic-function-example
然后在函数签名和函数体里,这些类型参数可以和任何其他类型一样被使用。下例把两个
列表组合为元组的列表:
vim9script
def Zip<T, U>(first: list<T>, second: list<U>): list<tuple<T, U>>
const LEN: number = ([first->len(), second->len()])->min()
final result: list<tuple<T, U>> = []
for i in range(LEN)
result->add((first[i], second[i]))
endfor
return result
enddef
var n: list<number> = [61, 62, 63]
var s: list<string> = ['a', 'b', 'c']
echo $"Zip example #1: {Zip<number, string>(n, s)}"
echo $"Zip example #2: {Zip<string, number>(s, n)}"
type-variable-naming E1552
type-parameter-naming
如上例所示,一般惯例是,类型变量使用单个大写字母 (如 T、U、A 等等)。虽然可以有
多于一个字母,但必须以大写字母开头。下例中,"Ok" 合法,但 "n" 不是:
vim9script
def MyFail<Ok, n>(): void
enddef
# E1552: Type variable name must start with an uppercase letter: n...
E1558 E1560
函数可以必须作为泛型函数或常规函数之一来声明和使用 - 但不能同时是两者。下面的
Vim9 脚本显示这些错误:
vim9script
My1558<number>()
# E1558: Unknown generic function: My1558
vim9script
def My1560(): void
enddef
My1560<string>()
# E1560: Not a generic function: My1560
E1561
类型参数名不能和其他标识符冲突:
vim9script
def My1561<D, E, D>(): D
enddef
# E1561: Duplicate type variable name: D
vim9script
enum E
Yes, No
endenum
def My1041<E>(): E
enddef
# E0141: Redefining script item "E"
调用泛型函数
generic-function-call
要调用泛型函数,在函数名和参数列表之间的 "<" 和 ">" 里指定具体类型:
MyFunc<number, string, list<number>>()
注意: 本小节有几个可用的示例,可以直接执行,包括
generic-function-example 。
E1555 E1556 E1557 E1559
传递给函数的类型实参数目必须和声明中类型参数的数目匹配。不允许空类型列表。
示例:
vim9script
def My1555<>(): void
enddef
# E1555: Empty type list specified for generic function ...
vim9script
def My1556<T>(): void
enddef
My1556<bool, bool>()
# E1556: Too many types specified for generic function ...
vim9script
def My1557<T, U>(): void
enddef
My1557<bool>()
# E1557: Not enough types specified for generic function ...
vim9script
def My1559<T>(): T
enddef
My1559()
# Vim(eval):E1559: Type arguments missing for generic function ...
任何 Vim9 类型 ( vim9-types ) 都可用于泛型函数的具体类型。
以下各处不接受空格:
- 函数名和 "<" 之间 ( E1068 )
- ">" 和左括号 "(" 之间 ( E1068 ),或
- "<" 和 ">" 之间,分隔类型的逗号之后所需的空格除外 ( E1202 )。
和常规函数一样,泛型函数可被导出和导入。见 :export 和 :import 。
泛型函数可在另一个常规或泛型函数里定义。示例:
vim9script
def Outer(): void
# 返回列表的首个项目,或者缺省值
def FirstOrDefault<T, U>(lst: list<T>, default: U): any
return lst->len() > 0 ? lst[0] : default
enddef
echo FirstOrDefault<string, bool>(['B', 'C'], false) # 回显 B
echo FirstOrDefault<number, number>([], 42) # 回显 42
enddef
Outer()
使用类型变量作为类型参数
类型变量也可被传递为类型参数。例如:
vim9script
# T 声明为类型参数
# 它可用作 'value' 参数类型和返回值类型
def Id<T>(value: T): T
return value
enddef
# U 声明为类型参数
# 它可用作 'value' 参数类型和返回值类型
def CallId<U>(value: U): U
# U 是类型变量,可被传递/用作类型参数
return Id<U>(value)
enddef
echo CallId<string>('I am') .. ' ' .. CallId<number>(42)
这可用于像字典列表或像下例那样的列表字典这样的复杂数据结构:
vim9script
def Flatten<T>(x: list<list<T>>): list<T>
final result: list<T> = []
for inner in x
result->extend(inner)
endfor
return result
enddef
const ENGLISH: list<dict<string>> = [{1: 'one'}, {2: 'two'}]
const MANDARIN: list<dict<string>> = [{1: '壹'}, {2: '贰'}]
const ARABIC_N: list<dict<number>> = [{1: 1}, {2: 2}]
echo Flatten<dict<string>>([ENGLISH, MANDARIN])
echo Flatten<dict<any>>([ENGLISH, ARABIC_N])
在 "Flatten<T>" 里,"T" 是声明的类型参数。而在函数的其他地方,"T" 是引用该类型
参数的类型变量。
泛型类方法
Vim9 类方法可以是泛型函数:
vim9script
class Config
var settings: dict<any>
def Get<T>(key: string): T
return this.settings[key]
enddef
endclass
var c: Config = Config.new({timeout: 30, debug: true})
echo c.Get<number>('timeout')
echo c.Get<bool>('debug')
E1432 E1433 E1434
基类的泛型类方法可被子类的泛型方法覆盖。两个方法的类型变量数量必须一致。而已具
体化的类方法则不能被泛型方法覆盖,反之亦然。
泛型函数引用
函数引用 ( Funcref ) 可为泛型函数。这样可以创建对应指定类型的函数工厂:
vim9script
# 匹配字符串里的指定字符或匹配列表里的字符十进制值。
# 注意: '*' 的十进制值 是 42 (U+002A)
var c: string = "*"
var char_dec: tuple<string, string> = (c, c->char2nr()->string())
def Matcher<T>(pattern: string): func(T): bool
return (value: T): bool => match(value, pattern) >= 0
enddef
var StringMatch = Matcher<string>(char_dec[0])
echo "*+"->StringMatch() # true (有 *)
echo ",-"->StringMatch() # false
var ListMatch = Matcher<list<number>>(char_dec[1])
echo [42, 43]->ListMatch() # true (有 42)
echo [44, 45]->ListMatch() # false
编译和反汇编泛型函数
:defcompile 命令可用于编译泛型函数,须指定具体类型的列表:
defcompile MyFunc<number, list<number>, dict<string>>
:disassemble 命令可用于列出泛型函数生成的汇编指令列表:
disassemble MyFunc<string, dict<string>>
disassemble MyFunc<number, list<blob>>
限制和未来计划
目前,Vim 不支持:
- 类型变量的类型推导: 调用泛型函数时所有的类型必须显式指定。
- 类型约束: 类型变量不能限定为指定的类或界面 (例如,
`T extends SomeInterface`)。
- 缺省类型参数: 提供类型参数未显式指定时的的缺省类型。
6. 命名空间、导入和导出
vim9script vim9-export vim9-import
可编写 Vim9 脚本,被其它脚本导入。具体地说,有意地导出若干项目,使之被其它脚本
所用。导出项目的脚本被其它脚本导入,后者就可以使用那些导出项目了。导出脚本的其
余项目依然局部于脚本而不能被导入脚本访问。
此机制的存在是为了编写被其它脚本执行 (导入) 的脚本,同时保证其它脚本只能访问你
要它们访问的项目。这也避免了使用全局命名空间,后者有名字冲突的风险。比如当有两
个功能相近的插件的时候。
可显式使用全局命名空间来作弊。这只应该用于真正需要全局的情况。
命名空间
vim9-namespace
为了识别可导入的文件,文件出现的第一个语句必须是 vim9script 语句 ( vim9-mix
介绍一个例外)。它告知 Vim 脚本在自己的命名空间而不是全局命名空间里被解释。如果
文件这样开始:
vim9script
var myvar = 'yes'
那么 "myvar" 只存在于此文件中。如果没有 vim9script ,其它脚本和函数可用
g:myvar 进行访问。
E1101
文件级别的变量和老式脚本里的局部 "s:" 变量非常类似,但省略 "s:"。而且不能被删
除。
和以前一样,Vim9 脚本中仍然可用全局 "g:" 命名空间。还有 "w:"、"b:" 和 "t:" 命
名空间。它们的共同点是变量不声明,没有特定类型且可以删除。 E1304
:vim9script 一个副作用是 'cpoptions' 选项设为 Vim 缺省值,类似于:
:set cpo&vim
其中一个效果是 line-continuation 总是打开。
脚本结束时则会恢复 'cpoptions' 原先的值,在脚本里增加或删除的标志位也会从原先
的值里进行相应的增删,以达到相同效果。标志位的顺序可能会变化。
这不适用于启动时执行的 vimrc 文件。
vim9-mix
有一个办法可以在一个脚本文件里同时使用老式和 Vim9 语法:
" 这里是 _老式 Vim 脚本_ 注释
if !has('vim9script')
" 这里是 _老式 Vim 脚本_ 注释/命令
finish
endif
vim9script
# 从这里开始是 _Vim9 脚本_ 注释/命令
echowindow $"has('vim9script') == {has('vim9script')}"
这样就可以编写脚本,在可能的情况下利用 Vim9 脚本的语法,但能防止在 Vim 版本不
支持 'vim9script' 时,执行该命令导致出错。
注意 Vim9 语法 在 Vim 9 之前有过改变,所以使用正确语法的脚本 (如 "import from"
而不是 "import") 可能会报错。如果不想如此,检查 v:version >= 900 会更安全些
(因为 Vim 8.2 补丁 3965 就会使 "has('vim9script')" 返回 v:true )。有时检查更
晚版本会更谨慎些。Vim9 脚本的特性集会继续增长,例如,如果使用了元组 (在 Vim
9.1 补丁 1232 引入),更好的条件是:
if !has('patch-9.1.1232')
echowindow $"Fail: Vim 没有补丁 9.1.1232"
finish
endif
vim9script
echowindow $"Pass: 版本号 {v:versionlong}。 继续中 ..."
不管使用哪种 vim 混合条件,只能有两种方法可以工作:
1. "if" 语句计算为假,跳过直到 endif 部分为止的命令, vim9script 是第一
个真正执行的命令。
2. "if" 语句计算为真,执行直到 endif 部分为止的命令,而 finish 在到达
vim9script 前退出。
导出
:export :exp
可以这样导出项目:
export const EXPORTED_CONST = 1234
export var someValue = ...
export final someValue = ...
export const someValue = ...
export def MyFunc() ...
export class MyClass ...
export interface MyClass ...
export enum MyEnum ...
E1043 E1044
就像这里暗示的,只能导出常数、变量、 :def 函数、类、界面和枚举。
E1042
:export 只能用于 Vim9 脚本的脚本级别。
导入
:import :imp E1094 E1047 E1262
E1048 E1049 E1053 E1071 E1088 E1236
导出项目可在另一个脚本里被导入。导入语法有两种形式。简单形式:
import {filename}
{filename} 是计算结果为字符串的表达式。此形式里的文件名必须以 ".vim" 结尾,
".vim" 之前的部分会成为该命名空间的脚本局部名。例如:
import "myscript.vim"
这使得 "myscript.vim" 里的所有导出项目都可用 "myscript.item" 的形式访问。
:import-as E1257 E1261
如果名字太长或有二义性,以下形式可用于指定别名:
import {longfilename} as {name}
这里 {name} 成为代表被导入命名空间的特定的脚本局部名。因此 {name} 必须由字母、
数位和 "_" 组成,就像 internal-variables 那样。{longfilename} 表达式的计算结
果必须为文件名。例如:
import "thatscript.vim.v2" as that
E1060 E1258 E1259 E1260
这样你就可用 "that.item" 等等。可以自由选择名字 "that"。请用可以识别导入脚本的
名字。避免命令名、命令修饰符和内建函数名,因为你选的名字会使那些被隐藏。最好不
要用大写字母开头的名字,因为那样也可能会隐藏全局用户命令和函数。此外,也不能和
脚本其它项目,如函数或变量名,重名。
如果不想要带句号的名字,可为函数提供本地引用:
var LongFunc = that.LongFuncName
对常量这样也可以:
const MAXLEN = that.MAX_LEN_OF_NAME
对变量不可以,因为值会被复制一次,而修改变量会改变备份,而不是原先的变量。所
以,只能用带句号的全名。
:import 不能在函数内使用。导入项目应该出现在脚本级别,且只能导入一次。
import 之后的脚本名可以是:
- 相对路径,以 "." 或 ".." 开始。这会找到相对于脚本文件自身所在位置的文件。可
用于把大型插件分割为几个文件。
- 绝对路径,Unix 上以 "/" 开始,或 MS-Windows 上以 "D:/" 开始。很少用到。
- 既非相对也不是绝对的路径。会在 'runtimepath' 项目的 "import" 子目录中寻找。
名字通常较长且唯一,以避免载入错误的文件。
- 注意 不使用 "after/import"。
如果名字不以 ".vim" 结尾,必须使用 "as name" 的形式。
Vim9 脚本文件一旦导入,结果会被缓冲,下次导入相同脚本时,会使用缓冲而不会再次
读取文件。
不能导入同一脚本两次,即使用两个不同的 "as" 名字也不行。
使用导入名时,句号和项目名必须在同一行,中间不能断行:
echo that.
name # 出错!
echo that
.name # 出错!
import-map
Vim9 脚本从另一个脚本导入函数时,用 <SID> 前缀可在映射里引用导入的函数:
noremap <silent> ,a :call <SID>name.Function()<CR>
映射在定义时, "<SID>name." 会被替换为 <SNR> 和导入脚本的脚本 ID。
更简单的方案是用 <ScriptCmd> :
noremap ,a <ScriptCmd>name.Function()<CR>
注意 这只适用于函数,不适用于变量。
import-legacy legacy-import
:import 也可用于老式 Vim 脚本。即使未给出 "s:" 前缀,导入命名空间仍然是脚本
局部的。例如:
import "myfile.vim"
call s:myfile.MyFunc()
使用 "as name" 形式:
import "otherfile.vim9script" as that
call s:that.OtherFunc()
不过,不能直接解析命名空间本身:
import "that.vim"
echo s:that
" ERROR: E1060: Expected dot after name: s:that
这也影响老式映射上下文里 <SID> 的使用。因为 <SID> 只是函数的合法前缀而 不
是命名空间的,你不能用之在局部于脚本的命名空间里局限函数的作用域。因此,不要用
<SID> 函数前缀,而应用 <ScriptCmd> 。例如:
noremap ,a <ScriptCmd>:call s:that.OtherFunc()<CR>
:import-cycle
import 命令在见到时就会执行。如果脚本 A 导入脚本 B,而 B 已经 (直接或间接地)
导入了 A,会跳过前者。A 在 "import B" 之后的项目此时尚处于未处理和未定义状态。
所以,循环导入可以存在且不会直接报错,但 A 在 "import B" 之后的项目可能会因为
未定义而出错。这不适用于自动载入导入,见下一小节。
在自动载入脚本中导入
vim9-autoload import-autoload
要有最佳的启动速度,应该尽量延迟脚本的载入直到实际需要为止。建议使用自动载入机
制:
E1264
1. 在插件中定义指向自动载入脚本导入的项目的用户命令、函数和/或映射。
import autoload 'for/search.vim'
command -nargs=1 SearchForStuff search.Stuff(<f-args>)
应放在 .../plugin/anyname.vim。 "anyname.vim" 可自由选择其名字。现在就
可用 "SearchForStff" 命令了。
:import 的 "autoload" 参数意味着直到其中项目被实际用到,脚本暂缓载
入。这类脚本可在 'runtimepath' 的 "autoload" 目录下找到,而不是
"import" 目录。此处,也可用相对或绝对名字,见下。
2. 主要代码放在自动载入脚本。
vim9script
export def Stuff(arg: string): void
...
放在 .../autoload/for/search.vim 里。
"search.vim" 脚本放在 "/autoload/for/" 目录下的效果是 "for#search#" 会
加在每个导出的项目之前。前缀由文件名获取,就像老式自动载入脚本里你会手
动做的一样。因而,上例中的导出函数可由 "for#search#Stuff" 获取,但通常
会用 `import autoload` 而不是直接用前缀 (在函数的编译过程中时如果遇到
此函数,后者有载入自动载入脚本的副作用)。
自动载入脚本中,可以根据需要分割功能和导入其它文件。这样可以在插件间分
享代码。
在 'runtimepath' 里的所有项目里搜索自动载入脚本颇耗时。如果插件知道脚本的位置
的话,使用相对路径是常见的。这样可以避免搜索,会快很多。另一个优点是脚本名不需
要是唯一的。也可用绝对路径。例如:
import autoload '../lib/implement.vim'
import autoload MyScriptsDir .. '/lib/implement.vim'
使用导入的自动载入脚本的映射在定义时,可用特殊键 <ScriptCmd> 。这使映射中的命
令可以使用映射定义所在脚本的上下文。
编译 :def 函数时,如果遇到自动载入脚本中的函数,不载入该脚本,直到 :def 函
数被调用时才载入。这意味着只有在运行时才看到错误,因为参数和返回类型此时尚未
知。但如果直接用带 '#' 字符的名字,那么 确实会 载入自动载入脚本。
小心不要意外触发自动载入脚本的载入。例如,设置选项时如果用到函数名,要用字符串
而非函数引用:
import autoload 'qftf.vim'
&quickfixtextfunc = 'qftf.Func' # _不载入_自动载入脚本
&quickfixtextfunc = qftf.Func # _载入_自动载入脚本
另一方面,需要报告错误的时候,应早点载入脚本。
为测试用, test_override() 函数使 `import autoload` 立即载入脚本,这样可以马
上检查项目和类型,而无须等待实际使用它们的时候再检查:
test_override('autoload', 1)
之后要复位的话:
test_override('autoload', 0)
或者:
test_override('ALL', 0)
8. 理据 vim9-rationale
:def 命令
插件作者一直要求有更快的 Vim 脚本。调查发现,继续保持原有的函数调用语义会使性
能提高近乎不可能,因为涉及的函数调用、局部函数作用域的设置以及行的执行引起的负
担。这里需要处理很多细节,比如错误信息和例外。创建用于 a: 和 l: 作用域的字典、
a:000 列表和若干其它部分增加了太多不可避免的负担。
因此定义新风格函数的 :def 方法应运而生,它接受使用不同语义的函数。多数功能不
变,但有些部分有变化。经过考虑,这种定义函数的新方法是区别老式风格代码和 Vim9
脚本代码的最佳方案。
使用 "def" 定义函数来源于 Python。其它语言使用 "function",这和老式的 Vim 脚本
使用的有冲突。
类型检查
应在编译时尽可能地把 Vim 代码行编译为指令。延迟到运行时会使执行变慢,也意味着
错误只能在后期才能发现。例如,如果遇到 "+" 字符时编译成通用的加法指令,在运行
时,此指令必须检查参数类型并决定要执行的是哪种加法。如果类型是字典要抛出错误。
如果类型已知为数值型,就可用 "数值相加" 指令,这会快很多。编译时可报错,而运行
时就无需错误处理,因为两个数值相加几乎不会出错。
备注: 稍为离题一下,唯一例外是整数溢出,如果结果超出了最大的整数值的话。例
如,加到 64-位带符号整数使结果大于 2^63:
vim9script
echo 9223372036854775807 + 1 # -9223372036854775808
echo 2->pow(63)->float2nr() + 1 # -9223372036854775808
类型语法,包括使用 <type> 用作复合类型,类似于 Java,因为容易理解,也广泛使
用。类型名是 Vim 之前所用的加上新增的 "void" 和 "bool" 等类型。
去除臃肿和怪异行为
一旦决定 :def 函数可以和老式函数有不同的语法,我们就有自由去新增改进,使了解
常用编程语言的用户对代码能更熟悉。换而言之: 删除只有 Vim 才采取的怪异行为。
我们也可以去除臃肿,这里主要是指使 Vim 脚本和陈旧的 Vi 命令后向兼容的那些部
分。
例如:
- 调用函数不再需要 :call ,而计算表达式不再需要 :eval 。
- 续行不再需要前导反斜杠,可以自动判断表达式何处终止。
不过,这也意味有些部分需要改变:
- 注释改用 # 而不是 " 开头,以避免和字符串混淆。这也很好,若干流行的语言也是如
此。
- Ex 命令范围需要有冒号前导,以避免和表达式混淆 (单引号可以是字符串或位置标
记,"/" 可能是除法或搜索命令,等等)。
目标是尽量减少差异。一个好的标准是如果不小心用了旧语法,很有可能你会得到错误信
息。
来自流行语言的语法和语义
脚本作者抱怨 Vim 脚本语法和他们习惯使用的有出乎意外的差异。为了减少抱怨,使用
流行的语言作为范例。与此同时,我们不想放弃老式的 Vim 脚本为人熟知的部分。
有很多方面我们跟随 TypeScript。这是新近的语言,已得到广泛流行,且和 Vim 脚本有
相似性。它也有静态类型 (总是有已知值类型的变量) 和动态类型 (在运行时可决定有不
同类型的变量) 的混合。既然老式 Vim 脚本是动态类型的,许多现有功能 (尤其是内建
函数) 依赖于这一点,而静态类型允许更快地执行,我们需要在 Vim9 脚本中支持两者的
混合。
我们无意完全照搬 TypeScript 语法和语义。只想借用可用于 Vim 的部分,使 Vim 用户
可以开心地接受。TypeScript 是个复杂的语言,有它自己的历史,优点和缺点。关于缺
点部分,可阅读此书: "JavaScript: The Good Parts"。或找找此文章 "TypeScript:
the good parts" 并阅读 "Things to avoid" 一节。
熟悉其它语言 (Java、Python 等等) 的人士可能会不喜欢或不理解 TypeScript 的其它
一些部分。我们也试图避免那些部分。
避免的 TypeScript 特定项目:
- 重载 "+" 同时用于加法和字符串连接。这有违老式的 Vim 脚本,也经常引发错误。为
此原因,我们继续使用 ".." 用于字符串连接。Lua 也如此使用 ".."。这也方便把更
多值转换为字符串。
- TypeScript 可用形如 "99 || 'yes'" 的表达式作为条件,但不能把该值赋给布尔型。
这不统一也很讨厌。Vim 识别带 && 或 || 的表达式,并可以把结果用作布尔型。新增
了 falsy-operator 支持使用缺省值的机制。
- TypeScript 把空串当作假值,而空列表或字典当作真值。这不统一。Vim 中空列表和
字典也都当作假值。
- TypeScript 有若干 "只读" 类型,其用途有限,因为类型转换可抹除其不可更改的性
质。Vim 则对值进行锁定,这更灵活,但只在运行时进行检查。
- TypeScript 有复杂的 "import" 语句,这和 Vim 导入机制不匹配。可用更简单的机制
代替,以满足导入脚本只执行一次的要求。
声明
老式 Vim 脚本中,每个赋值都要用 :let 语句,而 Vim9 中只需用之作声明。既有此
不同,最好采用另一个命令: :var 。它在很多语言中都有用到。语义或有些许不同,但
都很容易把它识别为声明。
用 :const 来定义常量很常见,但语义有差别。有些语言只使变量本身不可变,而其它
语言则使其值不可变。考虑到 "final" 在 Java 里使变量不可变已为人熟知,我们决定
采用它来实现该语义。而 :const 可用作使两者都不可变。这也用于老式 Vim 脚本
里,含义近乎相同。
最后,我们采用的和 Dart 十分类似:
:var name # 可变的变量和值
:final name # 不可变的变量,可变的值
:const name # 不可变的变量和值
因为老式和 Vim9 脚本会混合使用,全局变量也会共享,所以类型检查最好可选。另外,
类型推断机制会在很多场合下不再需要对类型直接指定。TypeScript 语法最适合为声明
加入类型:
var name: string # 指定字符串类型
...
name = 'John'
const greeting = 'hello' # 推断为字符串类型
这是我们如何在声明在放入类型:
var mylist: list<string>
final mylist: list<string> = ['foo']
def Func(arg1: number, arg2: string): bool
考虑过两种其它方案:
1. 把类型放在名字前,类似于 Dart:
var list<string> mylist
final list<string> mylist = ['foo']
def Func(number arg1, string arg2) bool
2. 为类型放在变量名后,但不用冒号,类似于 Go:
var mylist list<string>
final mylist list<string> = ['foo']
def Func(arg1 number, arg2 string) bool
第一种对用过 C 或 Java 的用户很熟悉。而第二种和第一种比起来没有任何好处,我们
先排除第二种。
因为使用了类型推断机制,如果可以从值中推断,类型可以省略。这意味着在 var 后
我们不知道是跟着类型还是名字。这使解析复杂化,不仅对 Vim,对人而言也是如此。另
外,这样也没法使用和类型名重名的变量名,用 `var string string` 太混淆了。
我们最终选择了用冒号分隔名字和类型的语法。它需要引入标点符号,但实际更易分辨声
明的不同部分。
表达式
表达式计算已经和其它语言采用的方式很接近。有些细节有出入,有改进的空间。例如,
布尔条件可以接受字符串,先把它转换为数值,并检查该值是否非零。这不符一般期望,
经常引发错误,因为不以数值开头的文本会转换为零,也就被当作假值。如此,字符串用
作条件时就经常会不报错就而被当作假值,这常产生混淆。
Vim9 的类型检查更严格以防出错。需要使用条件时,例如用 :if 命令或 || 操作符
的时候,只接受类似于布尔的值:
真: true 、 v:true 、 1 、`0 < 9`
假: false 、 v:false 、 0 、`0 > 9`
注意 数值零为假,而数值一为真。这比绝大多数语言要宽一些。这是因为许多内建函数
会返回这些值,而改变这一点得不偿失。用了一段时间后,现在效果看来不错。
如用你有任何类型的值并希望把它当作布尔型来使用,使用 !! 操作符
(见 expr-! ):
vim9script
# 下面的值都为 true:
echo [!!'text', !![1], !!{'x': 1}, !!1, !!1.1]
# 下面的值都为 false:
echo [!!'', !![], !!{}, !!0, !!0.0]
JavaScript 这样的语言中,我们有这样方便的构造:
GetName() || 'unknown'
不过,这和在使用条件的地方只接受布尔型有冲突。为此,引入 "??" 操作符:
GetName() ?? 'unknown'
这里,你在显式地表达自己的意愿,按值本身去使用,而不是用作布尔值。这叫作
falsy-operator 。
导入和导出
老式 Vim 脚本的一个问题是所有函数和变量缺省都是全局的。可以使它们局部于脚本,
但因而就不能为其它脚本所用。这就违背了软件包只能有选择性地导出项目,其它保持局
部的概念。
Vim9 脚本里支持和 JavaScript 导入导出非常相似的机制。这是已有的 :source 命令
的变种,且工作方式符合人们期待:
- 和所有的缺省都是全局相反,所有的都是局部于脚本的,有些被导出。
- 导入脚本时显式列出要导入的符号,避免以后新增功能时的名字冲突和可能的失败。
- 此机制允许编写大而长又有清晰 API 的脚本: 导出函数、变量和类。
- 通过使用相对路径,同一包里导入的载入会更快,无需搜索许多目录。
- 导入一旦使用,其中项目会被缓冲而避免了再次载入。
- Vim 特定使事物局部于脚本的 "s:" 用法可以不需要了。
(从 Vim9 或老式脚本里) 执行 Vim9 脚本时,只能使用全局定义的项目,而不是导出的
项目。考虑过以下备选方案:
- 所有导出项目成为局部于脚本的项目。这样没法控制什么项目有定义,有可能很快就会
有问题。
- 使用导出项目成为全局项目。缺点是这样就不能避免全局空间的名字冲突。
- 完全禁止 Vim9 脚本的执行,而必须使用 :import 。这样就很难用脚本进行测试,或
在命令行上执行脚本进行实验。
注意 也 可以 在老式 Vim 脚本中使用 :import ,见上。
尽早编译函数
函数在实际调用时或使用 :defcompile 时才进行编译。为什么不尽早编译函数,以便
尽快报告语法和类型错误呢?
函数不能在遭遇时立刻编译,因为可能有之后定义的函数的前向引用。考虑定义函数 A、
B 和 C,其中 A 调用 B,B 调用 C,而 C 又调用 A。这里不可能通过对函数重新排列来
避免正向引用。
一个替代方案是先扫描整个文件以定位项目并判断其类型,这样就能找到正向引用,然后
再执行脚本并编译函数。这意味着脚本要解析两次,这会减慢速度,且脚本级别的某些条
件,如检查某特性是否支持等等,会难于使用。有过这方面的尝试,但结果是不能很好地
工作。
也可以在脚本最后编译所有函数。这样做的缺点是如果函数从未被调用,仍然要承受其编
译的开销。因为启动速度至关重要,绝大多数情况下最好延后处理,并在那时才报告语法
和类型错误。如果确实希望早报告错误,譬如测试期间,在脚本结束处的 :defcompile
命令可作救济。
为什么不用已有的嵌入式语言?
Vim 支持 Perl、Python、Lua、Tcl 和一些其它语言的接口。但由于种种原因,这些从未
广泛使用过。Vim 9 设计时作了一个决定,降低这些接口的优先级,并集中在 Vim 脚本
上。
不过,插件作者可能对其它语言更加熟悉,想用已有的库或要提高性能。我们鼓励脚本作
者使用任何语言编程并作为外部进程运行,使用作业和通道通信。我们可以想办法使之更
加便捷。
使用外部工具也有其不足。一种替代方案是把工具转换为 Vim 脚本。要尽量减少翻译的
工作量,并且同时保持代码快速,需要支持工具使用的构造。因为 Vim9 脚本现在支持了
类、对象、界面和枚举,这种做法现在更加可行。
vim:tw=78:ts=8:noet:ft=help:norl: