备注: 在本 vim9.txt 帮助文档中,以 vim9script 开头的 Vim9 脚本代码块 (以及以
vim9cmd 开始的单行语句) 会启用 Vim9 脚本语法高亮。这些代码还可直接执行,直观
查看运行输出。具体来说,用 :'<,'>source (见 :source-range ) 来执行,也就是
先可视选中代码行,按 V 进入行可视模式,然后输入 :so 。例如,可直接运行以下
Vim9 脚本:
vim9script
echowindow "欢迎来到 Vim9 的脚本世界!"
还有部分代码仅用于讲解概念,不适合加载运行。这类代码使用通用代码语法高亮,如下
例:
def ThisFunction() # 脚本局部函数
def g:ThatFunction() # 全局函数
export def Function() # 用于 import 和 import autoload
1. 什么是 Vim9 脚本? Vim9-script
Vim 脚本随着时间的推移日益庞大,同时还需要保持向后兼容。这意味着,过去做出的一
些错误设计决定往往已无法更改,而与 Vi 的兼容性也限制了解决问题的方式。此外,
Vim 脚本的执行速度较慢,因为每条命令在每次执行时都需要先进行解析。
Vim9 脚本的主要目标是大幅提高性能。为此,Vim 会将命令编译为可高效执行的指令。
执行速度有望提高数十倍甚至上百倍。
次要目标是尽量避免 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
脚本上下文) 之间的异同。两者外观相似,但区别不止在于语法,还有
语义和执行方式。
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})
和老式 Vim 脚本一样,Vim9 脚本中的脚本局部变量可用前缀 "s:",
但 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: 不能将 :legacy 用于此命令: endif
legacy endif
enddef
F_1189()
E1234
:legacy 不能单独使用;必须后跟其他命令。
2. 和老式 Vim 脚本的差异 vim9-differences
总览
E1146
下文简要汇总老式 Vim 脚本与 Vim9 脚本 (含 :def 函数) 的核心差异:
- 注释以 # 开头,而非 ":
vim9script
# 注释使用井号 (#),而非双引号 (")
- 用作续行符的反斜杠 (\) 多可省略:
vim9script
echo $"用户主目录是 {$HOME}\n" ..
$"$VIMRUNTIME 环境变量是 {$VIMRUNTIME}"
- 很多位置必须添加空格,提升可读性。参见 vim9-white-space 。
E1126
- 使用 :var 声明变量,赋值不用 :let (且禁止使用):
vim9script
var count = 0
count += 3 # 3
echo count
let E1126 = count # E1126: 不能在 Vim9 脚本中使用 :let
- 常量可通过 :final 与 :const 声明:
vim9script
final matches = ['Peter'] # 后续可向该列表追加元素
const NAMES = ['Paul', 'Mary'] # NAMES 常量不可修改
matches->extend(NAMES)
echo matches # ['Peter', 'Paul', 'Mary']
- 变量与函数缺省属于脚本局部作用域 (参见 vim9-scopes )。
- 函数声明需要给出参数类型与返回值类型:
vim9script
def Cubed(n: float): string # float 类型入参, 返回 string 类型
return $"\n{n} 立方是 {n->pow(3)}"
enddef
echo Cubed(input("输入浮点数,计算立方: ")->str2float())
- 调用函数无需 :call 。 :call 虽已废弃,但不报错:
vim9script
popup_notification("调用内置函数_无需_:call", {})
call popup_notification("调用内置函数_仍可通过_:call", {})
vim9-invalid-Ex-commands
- 下列 Ex 命令不可使用:
:Print (及其简写 :Pr )
:append (及其简写 :a )
:change (及其简写 :c )
:dp
:insert (及其简写 :i )
:k
:mode (及其简写 :mod )
:open (及其简写 :o )
:sce :scg :sci :scI :scl :scn :scp
:sg :sgc :sge :sgi :sgI :sgl :sgn :sgp :sgr
:sic :sie :siI :sin :sip :sir
:sI :sIc :sIe :sIg :sIi :sIl :sIn :sIp :sIr
:src :srg :sri :srI :srl :srn :srp
:t
:xit (及其简写 :x 和 :xi )
备注: 部分命令虽不报错,但 Vim9 脚本中其行为有所不同。例如 :dl 为
:dlist 的简写,而在老式脚本中则是 :delete 的简写。还有 :sc 、
:si 、 :sr 在老式脚本中是替代命令的各种简写形式,而在 Vim9 脚本里则
分别为 :scriptnames 、 :simalt 、 :srewind 的简写。
vim9-no-shorten E1065
- 大量命令禁止简写,使用其简写形式会报 E1065,具体来说,以下命令必须写全名:
- 流程控制: :break 、 :catch 、 :continue 、 :else 、 :elseif 、 :endfor 、
:endif 、 :endtry 、 :endwhile 、 :finally 、 :finish 、 :return 、
:throw 、 :while
- 声明关键字: :abstract 、 :class 、 :const 、 :def 、 :endclass 、
:enddef 、 :endenum 、 :endinterface 、 :enum 、 :export 、 :final 、
:import 、 :interface 、 :public 、 :static 、 :this 、 :type 、 :var
- :final 不能用作 :finally 的简写。因为 Vim9 中 :final 是关键字;单独使
用 :final 会列出全部变量,等同于在老式脚本中单独使用 :let 。
- 不支持花括号扩展变量名 curly-braces-names ,如 my_{&background}_message 。
- 命令前的范围必须加冒号前缀,示例:
vim9script
# 下行会匹配并显示包含 'colon' 的行
:-3,+2g/colon
# 下行会触发报错 E1050: 范围前需要有冒号
-3,+2g/colon
- 不支持用 "@" 直接执行寄存器;需加冒号前缀或使用 :execute :
vim9script
@a = 'echo "有效"'
:@a # 有效
execute @a # 有效
@a # E1207: 表达式无副作用: @a
- 除非特殊声明,默认使用最高版本 scriptversion 。
- 定义表达式映射时,在定义所在的脚本上下文中求值。
- 字符串索引按字符计数,而非字节。详情与示例参见 vim9-string-index 。
- 其他出乎意料的差异,参见 vim9-gotchas 。
# 开头的注释
vim9-comments
老式 Vim 脚本的注释以双引号 (") 开头。Vim9 脚本注释则以井号 (#) 开头。
vim9script
# 此处为注释
'Vim9 script uses #, not "'->popup_notification({}) # 另一处注释
原因是双引号同时用作字符串起始符。在很多场景,尤其是带换行的表达式中间,字符串
与注释后均可跟随任意文本,难以区分。为消除歧义,现在仅识别 # 注释,这一点和外
壳和 Python 脚本一致。
在 Vi 中, :# 是带行号列出文本的命令;Vim9 脚本改用 :number 。例如下面单行脚
本列出本段内容。
vim9cmd :-4,-2number
为提升可读性,命令与注释起始的 # 之间必须至少有一个空格或制表符。开启 syntax
后,语法高亮也有助于标识非法注释。示例:
vim9script
const OKAY: bool = true # 合法注释 (# 前有空格)
echo OKAY # true
try
const NO: bool = false# 这 是 非 法 注 释!
catch
echo v:exception # E121: 未定义的变量: false#
endtry
E1170
不能用 #{ 开启注释,它会被识别为老式字典常量,引发歧义并报错,示例:
vim9script
#{ E1170: 不能用 #{ 开始注释
用于创建折叠的折叠标记(即 #{{{ ) 不受此限制,仍然可用。
读取脚本时,Vim 在读到 vim9script 指令前,无法判断这是 Vim9 脚本。因此,在
该指令之前,注释必须沿用使用双引号的老式语法:
" vim9script 前沿用老式 Vim 脚本注释
vim9script
# Vim9 脚本注释
这种写法观感不佳 (语法高亮也不会把开头的老式注释命令用注释高亮组高亮)。推荐将
vim9script 放在首行:
vim9script
# Vim9 脚本注释
老式 Vim 脚本中,# 还用于代表轮换文件名。Vim9 脚本必须改用 %%。原本代表全部参
数的 ## 则必须改用 %%%。参见 c_%% 与 c_%%%# 。
Vim9 函数
vim9-functions
使用 :def 定义的函数会被编译,执行速度提升数倍,通常可达 10~100 倍。
许多错误会在编译阶段、函数运行前就检出。语法规则严格,保证代码易于阅读理解。
满足以下任一条件时触发编译:
- 函数第一次被调用
- 脚本中函数定义之后遇到 :defcompile 命令
- 使用 :disassemble 查看该函数
- 已编译函数调用此函数,或将其作为函数引用 (用于校验参数与返回值类型)
E1099
运行时可能出现 E1099 未知错误。如果可以稳定复现,可提交至
https://github.com/vim/vim/issues,这一般属于罕见未处理的异常。
E1091
编译失败后,再次调用不会重试编译,直接报错: "E1091: 函数未编译: {name}"。示例:
vim9script
def g:Broken(): string
echo 'a' .. []
enddef
g:Broken() # E1105: 无法将 list 转换为字符串
编译失败。之后调用 "g:Broken" 会触发 E1091:
vim9cmd g:Broken() # E1091: 函数未编译: Broken
E1191
调用编译失败的函数也可能返回: "E1191: 调用编译失败的函数"。示例:
vim9script
def Set_hlsearch()
&hlsearch = 9 # 注意: 'hlsearch' 是布尔类型,所以此处会报 E1012
enddef
def D_Set_hlsearch()
Set_hlsearch()
enddef
# 使用 'silent!',即便出现 E1012 也不会终止执行
silent! Set_hlsearch()
# 调用编译失败的函数会触发 E1191:
D_Set_hlsearch() # E1191: 调用编译失败的函数
编译阶段执行不存在的用户命令,会在编译阶段或执行阶段失败。因此下面代码报错:
vim9cmd YeahNah() # E117: 未知的函数: YeahNah
不过,函数内部封装的用户命令,在函数尚未编译时不会报错。例如下面脚本可以正常加
载,尽管两个函数内部调用的命令都不存在:
vim9script
def g:Late1()
DefinedLate1
enddef
def g:Late2()
execute('DefinedLate2')
enddef
如果在 DefinedLate1、DefinedLate2 定义前调用上述函数,编译失败。因此,加载完上
述脚本后,执行下面代码时,会分别报错 E476 或 E492 :
vim9cmd g:Late1() # E476: 无效的命令:DefinedLate1
vim9cmd g:Late2() # E492: 不是编辑器的命令: DefinedLate2
虽然两处由于不同原因而调用失败,关键点是命令必须在执行时存在。下面脚本可以正常
运行,因为命令在执行前已经定义 (注意: 加载此脚本前,需要重新加载包含两个全局函
数的那个脚本):
vim9script
command DefinedLate1 @a = "One"
command DefinedLate2 @b = "Two"
g:Late1()
g:Late2()
echo (@a, @b) # ('One', 'Two')
:def 函数没有 :function 若干选项 ("abort"、"range"、"closure" 或 "dict")。
:def 函数特性有:
- 如果出错,缺省终止执行,除非被 :try 块捕获,或通过 :silent! 调用 (例见
E1191 一节中的首个脚本),
- 不能传入范围,
- 原生支持闭包 (参见 vim9-closure ),以及
- 不能用作字典函数 (参见后续 vim9-no-dict-function 一节)。
vim9-no-dict-function
:def 函数不支持 "dict" 选项,也不支持 "dict" (字典) 函数。先看一段可正常运行
的老式 Vim 脚本示例:
" 老式 Vim 脚本字典函数
let dic = {'ln': [0, 1, 2, 3]}
function! dic.Len() dict
return len(self.ln)
endfunction
echo dic.Len() | " 4
E1182
在 Vim9 脚本中,编写同类代码会触发 E1182:
vim9script
var dic: dict<list<number>> = {'ln': [0, 1, 2, 3]}
def dic.len(): number # E1182: 无法在 Vim9 脚本中定义字典函数…
return len(self.ln)
enddef
Vim9 脚本不再使用字典函数,可选用以下几种方案:
- 使用 Vim9 类 (参见 Vim9-class ):
vim9script
class MyClass
var ln: list<number>
def Len(): number
return this.ln->len()
enddef
endclass
var obj = MyClass.new([0, 1, 2, 3])
echo obj.Len() # 4
- 显式传入字典:
vim9script
def DicLen(self: dict<any>, arg: string): number
return self[arg]->len()
enddef
var da: dict<any> = {func: DicLen, item: [0, 1, 2, 3]}
echo da.func(da, 'item') # 4
- 调用老式 Vim 脚本的字典函数 ( Dictionary-function ):
vim9script
function LegDicLen() dict
return self.item->len()
endfunc
def CallLegDicLen(): number
var da: dict<any> = {func: LegDicLen, item: [0, 1, 2, 3]}
return da.func()
enddef
echo CallLegDicLen() # 4
参数类型与返回值类型必须事先声明且与实际类型匹配;不匹配会产生各类错误。参见
fast-functions 与 type-checking 。
vim9-any-type
可以使用 "any" 类型,类型检查推迟至运行期,行为类似老式函数,也支持类型动态变
更。示例:
vim9script
var avar: any
echo avar->typename() # number
avar = 'A string'
echo avar->typename() # string
avar = [['A', 'list'], ['of', 'lists']]
echo avar->typename() # list<list<string>>
警告: "any" 类型缺省为 number 类型,缺省值 0。如果未留意这一点,要求特
定类型的运算可能产生意外结果或报错。另外,和显式声明为 number 的变量不
同,直接将 "any" 变量赋值其他类型会在运行时重新推断类型。示例:
vim9script
var n1: any
echo n1->typename() # number
n1 ..= 'zero'
echo n1 # 0zero (即缺省值 0 拼接字符串 "zero")
echo n1->typename() # string
n1 = true
echo n1->typename() # bool
var n2: any
n2->extend(['item']) # E712: extend() 的参数必须是列表或字典
参数直接通过名称访问,不再带 "a:" 前缀,和其他常规语言一致。不再存在 "a:" 字典
或 a:000 列表。下面示例除展示 "a:" 用法外,也演示老式脚本允许的极致压缩空格
写法:
function! MyFirst(s,d,n,...)
return a:s.a:d[a:n].a:000[1]
endfunction
let MyDict={1:'one',2:'two'}
echo MyFirst('MyDict 对应键 2 的值为 ',
\ MyDict,2,v:null,'!',9999)
对应的 Vim9 脚本写法:
vim9script
def MyFirst(s: string, d: dict<string>, n: number,
...l: list<any>): string
return s .. d[n] .. l[1]
enddef
var MyDict: dict<string> = {1: 'one', 2: 'two'}
echo MyFirst('MyDict 对应键 2 的值为 ',
MyDict, 2, null, '!', 9999)
注意: 这段 Vim9 脚本中,如果在以下位置省略空格,会报错:
- 逗号后
- '键: 值' 或 '变量: 类型' 中的冒号之后
- '..' 前后
- '=' 前后
vim9-variable-arguments
上例演示了可变参数 ("...l: list<any>"),可变参数必须放在参数列表末尾。Vim9 脚
本的可变参数需要指定参数名与 list<type>,写法类似 TypeScript。下例遍历一组数值
可变参数:
vim9script
def MyProduct(...ln: list<number>): number
var prod: number = 1
for num in ln
prod *= num
endfor
return prod
enddef
echo MyProduct(10, 10) # 100
echo MyProduct(5, 4, 5) # 100
可变参数未声明、或传参不正确都会触发报错:
E1055
- 可变参数后缺少参数名:
vim9script
def F1055(n: number, ...): void # E1055: ... 后面缺少名字
enddef
E1160
- 为可变参数设置缺省值:
vim9script
def F1160(...l = []): void # E1160: 不能为可变参数使用默认值
enddef
E1180
- 可变参数未声明为 list<type>:
vim9script
def F1180(...l: string): void # E1180: 可变参数类型必须是一个列表…
enddef
如果函数参数可选 (有缺省值),传递 v:none 作为参数会使用其缺省值。用于在使用
缺省值的参数之后需要显式指定其他参数值的场合。示例:
vim9script
def F(pi: float = 3.14, ra: float = 1.0): float
return pi * ra->pow(2)
enddef
echo F(v:none, 2.0) # 12.56 ('pi' 使用缺省值 3.14)
E1106
如果传入参数过多,会触发 E176 或 E1106。E1106 示例:
vim9script
var ln: list<number> = [1, 2, 4, 8]
foreach(ln, (val) => {
echo val
}) # E1106: 1 个参数太多 (该匿名函数仅声明一个参数)
备注: foreach() 会向回调传入两个参数: 处理列表时分别是列表索引与值。
即使只用到其中一个,Vim9 匿名函数也必须声明这两个参数 (严格来说,参数
数量必须匹配)。因此 "(val)" 应写成 "(_, val)",下文会说明。
vim9-ignored-argument
下划线 ("_") 可用作占位参数,代表该参数应被忽略。常在回调中,因为需要补齐参数
数量,但不需要使用该参数时使用。例如 map() 处理列表时,会传入列表索引与值两
个参数。下例先忽略索引,再忽略值:
vim9script
final nl: list<number> = [1, 2, 4, 8]
map(nl, (_, val) => val * 2) # '_' 忽略索引
echo nl # [2, 4, 8, 16]
map(nl, (idx, _) => idx * 2) # '_' 忽略值
echo nl # [0, 2, 4, 6]
"_" 占位符可多次使用,无需标注类型。
vim9script
var count: number
def In20s(_, _, year: number): void
count += year >= 2020 ? 1 : 0
enddef
var data = [['Bo', 'A', 2026], ['Mo', 'B', 2018], ['Jo', 'A', 2022]]
for staff in data
In20s->call(staff)
endfor
echo $"2020 年代共有 {count} 位员工."
备注: 这里使用的是 call() 函数,不要和 :call 命令混淆。
E1181
部分场景禁止使用下划线 "_",否则会报错 E1181。示例:
vim9script
def F1181(_): string
return _ # E1181: 不能在这里使用下划线
enddef
echo F1181("No")
函数与变量默认属于脚本局部作用域
vim9-scopes
在 Vim9 脚本的脚本顶层,通过 :function 或 :def 定义函数时,函数缺省局部于
脚本 (等同于老式 Vim 脚本加前缀 "s:")。要定义全局函数或变量,必须使用 "g:" 前
缀。需要被其他脚本导入 (含自动加载脚本) 的函数,必须用 :export 导出,才能在
别处使用。
def ThisFunction() # 脚本局部函数
def g:ThatFunction() # 全局函数
export def Function() # 可用 import 和 import autoload 导入的函数
E1075
不再支持老式脚本的 "s:" 前缀。在脚本顶层 :def 函数使用会触发 E1268 ,在嵌套
:def 函数内使用会触发 E1075。示例:
vim9script
def F1075(): void
def s:Inner() # E1075: 不支持命令空间:s:Inner()
enddef
enddef
F1075()
在 :def 函数内部,通过 :function 或 :def 定义嵌套函数且未指定命名空间
时,嵌套函数局部于定义所在的代码块。不能以字符串形式传给 function() 获取引
用。必须直接传递函数引用本身:
vim9script
def Outer(): void
def Inner(): string
return '成功调用 Inner()'
enddef
var Okay = function(Inner)
echo Okay() # 成功调用 Inner()
try
var Bad = function("Inner")
catch
echo v:exception # Vim:E700: 未知的函数: Inner
endtry
enddef
Outer()
备注: 传入字符串会失败的原因是 "Inner" 对应的是带有内部自动生成名称的
函数引用,可用 funcref(Inner) 查看该内部名。
不能在函数内部定义脚本局部函数。但可以先在脚本顶层声明脚本局部函数引用,再在函
数内把嵌套函数赋值给它:
vim9script
var ScriptLocalFuncref: func
def Outer(): void
def Inner(): string
return "来自 ScriptLocalFuncref 的问候!"
enddef
ScriptLocalFuncref = Inner
enddef
Outer()
echo ScriptLocalFuncref()
引用不带 "g:" 或 "s:" 前缀的函数时,Vim 在以下作用域内依次搜索: 函数作用域,代
码块作用域,脚本作用域。
导入函数通过 :import 指定的前缀访问。导出与导入参见 vim9-import 。
Vim9 脚本中,脚本局部函数引用名首字母必须大写。因此,即使在老式脚本作用域需要
"s:" 前缀时,也必须写成 "s:大写函数引用名" 形式,避免与内置函数产生歧义。以下
示例对老式 Vim 脚本与 Vim9 脚本进行对比:
1. 首先,老式 Vim 脚本中可用 "s:" 定义函数,以避免覆盖内置函数 (此例中存在
cos() 内置函数):
function! s:cos()
echo 's:cos() 可用'
endfunction
call s:cos() " s:cos() 可用
2. 其次,在 Vim9 脚本中,老式函数必须遵循三条规则:
1. 函数名首字母必须大写 (否则会报错 E1267 ),
2. 在 Vim9 脚本局部作用域内,不能通过 "s:" 定义 ( E1268 ),以及
3. 在老式函数作用域内,基于上述原因,必须使用 "s:" 加上首字母大写 ( E117 )
vim9script
execute ('function cos()') # E1267: 函数名必须以大写字母开头…
vim9script
execute ('function s:cos()') # E1268: 在 Vim9 脚本中不能使用 s:…
vim9script
function Cos()
echo '一切正常: 在 Cos() 内执行。'
endfunction
function Call_Cos()
call s:Cos() " 需要 's:' 前缀
call Cos() " E117: 未知的函数: Cos
endfunction
Call_Cos()
vim9-s-namespace E1268
在 Vim9 脚本中的 :def 函数内部,禁止使用 "s:"。对比两段脚本: 首先,在老式脚
本中的 :def 中可用 "s:" (可选),其次在 Vim9 脚本中则会触发 E1268:
" 老式 Vim 脚本 - :def 中可用 :s,但可选
let s:leg = '正常'
def Ok_def_s(): void
# 注意 leg 与 s:leg 都合法
echo leg .. ', ' .. s:leg # 正常,正常
enddef
call Ok_def_s()
vim9script
var nine: string
def F1268(): void
echo s:nine # E1268: 在 Vim9 脚本中不能使用 s::s:nine
enddef
F1268()
Vim9 脚本局部作用域不再支持 "s:" 前缀。不带前缀的函数和变量都局部于脚本,即使
调用老式函数也要遵循该规则:
vim9script
function ScriptLevel(str)
echo a:str
endfunction
ScriptLevel('正常')
# 下行会触发 E1268: 在 Vim9 脚本中不能使用 s:
s:ScriptLevel('不可用')
在老式函数内部,访问脚本局部变量必须加 "s:":
vim9script
var local: string = '脚本局部 :var'
function Legacy_requires_s()
echo s:local
" 不加 s:,直接写 local 会报 E121
echo local
endfunction
Legacy_requires_s()
在所有情况下,函数必须先定义,后使用。无论是直接调用 (包括由 :defcompile 触
发编译时),还是在编译调用该函数的代码、推断其返回类型时,都必须先定义该函数。
因此,不带命名空间的函数和变量缺省都在当前脚本内查找其定义或导入语句。全局变量
和函数则可在任意位置定义 (想确定具体位置并不容易,可通过 :verbose 查看变量最
后一次被设置的位置)。
删除 Vim9 脚本中的函数
E1084
Vim9 脚本内,脚本局部函数 (无论是通过 :function 还是 :def ) 仅在加载脚本时
一次性定义,脚本自身无法删除或覆盖:
vim9script
def MyDef(): void
enddef
delfunction MyDef # E1084: 无法删除 Vim9 函数 MyDef
备注: 重新加载脚本可以覆盖脚本局部函数。参见 vim9-reload 。
带 "g:" 前缀的全局函数几乎可以随时定义与删除。但在 Vim9 脚本中删除全局函数时,
function() 与 funcref() 存在行为差异: 当全局函数被删除并重定义后,通过
function() 获取的引用会动态指向新函数;而 funcref() 获取的引用会保持指向旧
函数,除非重新给变量赋值。这是和老式 Vim 脚本重要区别: 老式脚本删除函数时,函
数引用也会一并失效。示例:
vim9script
def g:F(): string
return 'one'
enddef
var Function = function(g:F)
var Funcref = funcref(g:F)
echo (Function(), Funcref()) # ('one', 'one')
delfunction g:F
def g:F(): string
return 'two'
enddef
echo (Function(), Funcref()) # ('two', 'one')
Funcref = funcref(g:F) # 重新声明 Funcref
echo (Function(), Funcref()) # ('two', 'two')
备注: funcref() 这种持久引用行为可被视为设计特性,但如果不了解可能会
产生意外行为。
编译函数时,如果调用尚未定义的函数,不会触发 FuncUndefined 自动命令。如有需
要,可使用自动加载函数,或是调用老式脚本函数来触发 FuncUndefined 。
重新加载 Vim9 脚本缺省清除函数与变量
vim9-reload vim9-noclear
老式 Vim 脚本多次加载时,不会清理旧内容。执行命令只会覆盖已有变量、函数,新建
对象,旧的内容会残留。
Vim9 脚本多次加载时,缺省删除全部已有的脚本局部函数与变量,相当于全新环境。插
件开发时便于测试新版本;修改名称后不用担心旧定义残留。
自动加载脚本导出的函数与变量存放在带自动加载前缀的全局命名空间,重新加载此类脚
本时,同样会清空已有内容。但自动加载脚本中的类与枚举属于例外: 无法用此方式重定
义,会报错 E1041 ,因为旧实例仍然引用旧定义。想要加载修改后的自动加载类或枚
举,需要重启 Vim。其他普通脚本中,类和枚举则会像其余内容一样被正常清空,从而可
以重定义。
要保留脚本局部函数与变量,使用 "noclear" 参数。示例脚本可多次加载,每次都会往
MyList 追加一个 9:
vim9script noclear
var MyList: list<number> = !exists('MyList') ? [9] : MyList->add(9)
echo MyList
适合搭配 :finish 在重新加载时提前退出的脚本。例如,缓冲区局部选项可绑定函
数,该函数只需定义一次:
vim9script noclear
# 首次加载输出 '位于 SomeFunc() 中'。
# 后续加载则弹出提示 'SomeFunc 已存在'。
setlocal completefunc=SomeFunc
if exists('*SomeFunc')
popup_notification('SomeFunc() 已定义', {time: 4000})
finish
endif
def SomeFunc()
popup_notification('位于 SomeFunc() 中', {time: 4000})
enddef
SomeFunc()
E1149
脚本局部变量与函数会被清空,这会带来一个重要影响: 对于传统写法依靠
"g:loaded_{plugin_name}" 判断是否加载、然后 finish 的模式,必须牢记变量不会
持久保留。例如,加载下面 Vim9 脚本,首次会显示 "正常…",再次加载就触发 E1149
错误:
vim9script
:+7,+15source
echo g:GetName()
# 第二次及之后加载触发:
# E1149: 在函数 GetName 重新加载后,脚本变量无效
" 不要直接执行以下脚本: 要完成演示,执行上面的脚本
vim9script
if exists('g:loaded_E1149')
finish
endif
g:loaded_E1149 = true
var name: string = '正常 (仅在首次加载时声明!)'
def g:GetName(): string
return name
enddef
备注: 想复现测试,执行 `:unlet g:loaded_E1149` 删除全局标记变量。
E1150
启用 noclear ( vim9-noclear ) 时,变量类型不会被重置。如果尝试更改变量类型,
可能会触发 E1150。例如,加载以下第一个脚本会输出 "string"。然后加载第二个脚本
会报错:
vim9script
var my_var: string = "string"
def g:Get_myvar(): void
echo my_var
enddef
g:Get_myvar()
vim9script noclear
my_var = false # 将 my_var 变更为布尔型
echo my_var # false
echo my_var->typename() # bool
g:Get_myvar() # E1150: 脚本变量类型改变
E1190
使用 "noclear" 模式重载时,已编译函数调用关系保留。如果被调用函数签名变更,会
出现参数不匹配。例如,以下脚本首次加载会输出 "9"。但再次加载时,Echo 函数新增
参数,导致调用参数数量不足,从而触发 E1190:
vim9script noclear
if !exists('g:loaded_E1190')
def Echo(n: number): void
echo n
enddef
def CallEcho(n: number): void
Echo(n)
enddef
else
# 重定义 Echo(),增加第二个参数
def Echo(n: number, x: number)
echo n * x
enddef
endif
CallEcho(9) # 首次运行输出 9;再次加载: E1190: 1 个参数太少
g:loaded_E1190 = true
用 :var、:final 和 :const 声明变量
vim9-declaration :var
局部变量必须使用 :var 声明。局部常量需要使用 :final 或 :const 声明。本节
中将它们统称为 "变量"。 :var 、 :final 、 :const 不允许简写,参见
vim9-no-shorten 。
变量的作用域可以是脚本、函数或者代码块。下例展示这三种作用域:
vim9script
var s: string = '脚本'
def Func(): void
var f: string = '函数'
{
var b: string = '代码块'
echo $"可见: {s}、{f}和{b}"
}
echo $"可见: {s}和{f}" # b: 在 {...} 块外部不可见
enddef
Func()
echo $"可见: 仅{s}" # f 和 b: 在:def 函数外部不可见
备注: 判断变量是否存在存在一些限制,参见 vim9-exists() 。
vim9-block
变量仅在其被定义的代码块内可见,包含嵌套的子块。代码块结束后变量就无法访问,尝
试使用会报 E121。例如,要有意从后续代码 "隔离" 变量,可以利用代码块,使该变量
仅在块内生效:
vim9script
{
var inblock: string = 'inblock 在块外不可见!'
}
echo inblock # E121: 未定义的变量: inblock
要使变量 "inblock" 在块外部访问,需要提前声明:
vim9script
var inblock: string
{
inblock = '事先定义时, inblock 在块外可见'
}
echo inblock
代码块在用户命令中非常有用 (另见 command-block )。下例中常量 SAVE 保存无名寄
存器的值,之后用来恢复无名寄存器。SAVE 在用户命令外部不可见:
vim9script
# 创建 YankHelpGrep 命令 (对可视选区执行 :helpgrep)
command -register YankHelpGrep {
const SAVE = @"
normal! y
execute $"helpgrep {getreg('<register>')}"
@" = SAVE
copen
}
# 可视模式下,将 YH 映射到 YankHelpGrep
xnoremap YH <ScriptCmd>YankHelpGrep "<CR>
代码块对自动命令同样有用 (参见 :autocmd-block )。
虽然代码块写法简洁好用,但很多场景下使用 :def 函数会更合适,可读性通常更好。
E1025 E1128
代码块缺少左大括号会触发 E1025 或 E1128。示例:
vim9script
def F1025()
}
enddef
defcompile F1025 # E1025: 在块作用域外使用 }
和
vim9cmd } # E1128: } 没有匹配的 {
变量声明与初始化写法错误对应的报错:
E1017
vim9script
var F1017: func = (): void => {
var a: any
var a = 'error' # E1017: 变量已被声明:a
}
E1020
vim9script
var x += 4 # E1020: 不能对新变量使用操作符:x += 4
E1022
vim9script
var x # E1022: 需要类型或初始化
E1034
vim9script
var this = 'reserved' # E1034: 不能使用保留名字 this
E1054
vim9script
var x: bool
var F1054: func = (): void => {
var x = 'no' # E1054: 变量已在脚本中声明:x
}
E1087
vim9script
var F1087: func = (): void => {
var x.y = 0 # E1087: 声明变量时不能使用索引
}
E1124
vim9script
function F1124()
var x = 'E1124: ":var" 不能在老式 Vim 脚本中使用'
endfunction
F1124()
E1079
不能在命令行模式或 Ex 模式下声明变量。示例:
vim9script
feedkeys(":vim9cmd var x = 0\<CR>")
# E1079: 不能在命令行上声明变量
只声明类型但不指定初始值时,变量缺省值的规则是:
- bool: false
- number、any: 0
- float: 0.0
- 其余可无初始化声明的类型: 空值
示例:
vim9script
var a: any | var b: bool | var n: number | var f: float
echo [a, b, f, n] # [0, false, 0.0, 0]
var s: string | var F: func | var l: list<any> | var d: dict<any> |
\ var j: job | var c: channel | var z: blob | var t: tuple<any>
echo empty(s) && empty(F) && empty(l) && empty(d) && empty(j) &&
\ empty(c) && empty(z) && empty(t) # true
备注: "any" 类型缺省值为 0,使用时需要留意。相关影响参见
vim9-any-type 。
未初始化的容器类型可以追加元素。示例:
vim9script
var myDict: dict<list<number>>
var myList: list<number>
myList->add(9)
myDict['version'] = myList
echo myDict # {'version': [9]}
将变量初始化为 null 值,与使用 "null_<type>" 初始化变量存在区别。特别是
null_dict 、 null_list 和 null_blob ,尝试对这类变量执行添加或扩展操作时会
报错:
- null_dict :
E1103
vim9script
var nd = null_dict
def F1103(): void
nd['a'] = 'fail' # E1103: 字典未设置
enddef
F1103()
E1133
vim9script
var ed: dict<string> # 空字典可以执行扩展
ed->extend({'a': 'okay'})
echo ed # {'a': 'okay'}
var nd: dict<string> = null_dict
nd->extend({'a': 'fail'}) # E1133: 无法扩充 null 字典
- null_list :
E1147
vim9script
var nl = null_list
def F1147(): void
nl[0] = 'fail' # E1147: 列表未设置
enddef
F1147()
E1130
vim9script
var el: list<string> # 空列表可以添加元素
el->add('okay')
echo el # ['okay']
var nl: list<string> = null_list
nl->add('fail') # E1130: 不能加到 null 列表
E1134
vim9script
var el: list<string> # 空列表可以执行扩展
el->extend(['okay'])
echo el # ['okay']
var nl: list<string> = null_list
nl->extend(['fail']) # E1134: 不能扩充 null 列表
- null_blob :
E1581
vim9script
var eb: blob # 空 blob 可以执行扩展
eb->extend(0zF09F988A)
echo eb->blob2str() # ['😊']
var nb: blob = null_blob
nb->extend(0zF09F988A) # E1581: 不能扩充 null Blob
E1131
vim9script
var eb: blob # 空 blob 可以添加数据
eb->add(0xF0)->add(0x9F)->add(0x98)->add(0x8A)
echo eb->blob2str() # ['😊']
var nb: blob = null_blob
nb->add(0xF0) # E1131: 不能加到 null Blob
备注: 以下两种类型不会触发类似错误:
- null_tuple (元组不可变,本身就不允许添加或扩展)
- null_string (属于 null-anomalies 之一),示例:
vim9script
var ns: string = null_string
ns ..= '没问题!'
echo ns # 没问题!
Vim9 脚本不允许使用 :let ,尝试使用会报 E1226 。
已有变量直接赋值即可,无需命令。全局、窗口、标签页、缓冲区变量以及 Vim 内置变
量同理,无需正式声明。仍然可以用 :unlet 删除,示例:
vim9script
g:v = "全局"
b:v = "缓冲区局部"
t:v = "标签页局部"
w:v = "窗口局部"
echo [g:v, b:v, w:v, t:v]
unlet g:v | unlet b:v | unlet w:v | unlet t:v
# 此时所有 'v' 变量均已删除,因此会输出 [0, 0, 0, 0]
echo [exists('g:v'), exists('b:v'), exists('w:v'), exists('t:v')]
使用 :var 声明以下对象会报错:
- 全局、缓冲区、标签页、窗口变量
- 选项,或
- 寄存器。
示例:
E1016
vim9script
var t:err = 'err' # E1016: 不能声明 标签页 变量:t:err
E1052
vim9script
var &ts = 8 # E1052: 不能声明选项:&ts = 8
E1066
vim9script
var @a = 'no' # E1066: 不能声明寄存器:@a = 'no'
E1178
函数局部变量不能使用 :lockvar ,应改用 :const 或 :final 。
vim9script
def F1178(): void
var x: any
lockvar x
enddef
F1178() # E1178: 无法锁定或解锁局部变量
脚本局部变量虽然支持 :lockvar ,但通常应优先使用 :const 或 :final ,只有需
要动态切换锁定/解锁状态时才使用 lockvar/unlockvar,示例:
vim9script
var n: number = 8
lockvar n
try
n = 9
catch
echo v:exception # E741: 值已锁定: n
finally
unlockvar n
endtry
n = 9
echo n # 9
vim9-exists()
exists() (以及 exists_compiled() ) 无法检测已编译函数内的局部参数或局部变
量,调用永远返回 0。但 exists() 可在未编译的 Vim9 脚本中正常使用;在已编译函
数内部,检测定义在未编译作用域的变量时也可正常工作。示例:
vim9script
var script: any
var MyCompiled: func = (arg: any): void => {
var compiled: any
echo exists('arg') # 0 (已编译函数,局部参数)
echo exists('compiled') # 0 (已编译函数,局部变量)
echo exists('script') # 1 (已编译函数,脚本局部变量)
}
MyCompiled(true)
echo exists('script') # 1 (脚本局部作用域变量)
同时在 Vim9 脚本中, exists() 可以检测老式函数里的参数与变量,包括用 :var
声明的变量。示例:
vim9script
function Legacy(arg = 'yes')
let local = 'yes'
vim9cmd var vlocal: string = 'yes'
return [exists('a:arg'), exists('local'), exists('vlocal')]
endfunction
echo Legacy() # [1, 1, 1]
vim9-no-shadowing
同一脚本文件内,变量、函数、函数参数不能遮蔽已经定义或导入的变量或函数。但变量
可以遮蔽 Ex 命令,遇到这种情况需要重命名变量。下面是尝试遮蔽标识符时报错的示
例:
E1006
vim9script
def F1006(n: number): void
var n: number = 10 # E1006: n 被作为参数使用
enddef
F1006(9)
E1041
vim9script
def X(): void
enddef
var X: number # E1041: 重定义脚本项目:"X"
E1167
vim9script
var ll: list<number> = [2, 3, 1]
def F1167(): void
var x: number = 1
echo ll->sort((x, y) => x - y)
enddef
F1167() # E1167: 参数名遮掩现有变量:x
E1168
vim9script
var ll: list<number> = [2, 3, 1]
var x: number
echo ll->sort((x, y) => x - y) # E1168: 参数已经在脚本中声明:in…
E1213
尝试重定义导入脚本的名称会触发 E1213 (另见 :import )。以下示例脚本先创建临时
文件,以 "Imp" 名导入,并输出其变量 y;之后再定义同名 Imp 触发 E1213:
vim9script
const TMP: string = tempname()
var lines: list<string> = ['vim9script', 'export var y = "成功!"']
lines->writefile(TMP)
import TMP as Imp
echo Imp.y # 成功!
var Imp: string # E1213: 重新定义导入的项目 "Imp"
全局变量与全局函数必须加 "g:" 前缀,即使在脚本顶层也是如此。因为 Vim9 脚本作用
域中不带前缀的变量或函数均缺省为脚本局部。示例 (注意执行时暂时不会有任何效果,
需要配合加载后续脚本):
vim9script
var scriptvar: string = 'scriptvar 为脚本局部变量'
def Sfunc(): string
return 'Sfunc() 为脚本局部函数'
enddef
g:globalvar = 'g:globalvar 为全局变量'
def g:Gfunc(): string
return 'g:Gfunc() 为全局函数'
enddef
现在加载以下脚本。前两行会输出全局变量与函数内容;不带前缀的变量和函数
会分别报 E121 (未定义变量) 与 E117 (未知函数)。
echo g:globalvar
echo g:Gfunc()
echo scriptvar
echo Sfunc()
autoload 函数不需要加 "g:" 前缀。
vim9-function-defined-later
Vim9 脚本中,可以在全局函数定义之前调用它,前提是调用写在 exists() 条件判断
内部。示例:
vim9script
def Later(arg: number): void
if exists('g:ExLater')
g:ExLater(arg)
endif
enddef
Later(1)
def g:ExLater(arg: number): void
popup_notification($'调用 g:ExLater() - {arg}', {})
enddef
Later(2)
首次加载脚本时,"Later(1)" 不会执行任何操作,因为 exists() 判定
"g:ExLater(arg)" 尚不存在,从而跳过该函数调用。再次加载脚本时,"g:ExLater()"
已经存在,会弹出两条提示: "调用 g:ExLater () - 1" 和 "调用 g:ExLater () - 2"。
可用 exists_compiled() 避免报错,但即便后续定义了该函数,也不会执行调用。下
例中,首次加载脚本,两次 "LaterCompiled()" 都不会弹出提示;第二次加载则两条提
示都会弹出:
vim9script
def LaterCompiled(arg: number): void
if exists_compiled('g:ExCompLater')
g:ExCompLater(arg)
endif
enddef
LaterCompiled(1)
def g:ExCompLater(arg: number): void
popup_notification($'调用 g:ExCompLater() - {arg}', {})
enddef
LaterCompiled(2)
`&opt = value` 写法现在代表为选项 "opt" 赋值,单独的 "&" 不再用来重复
:substitute 替代命令。为此,必须在 "&" 前面加 ":",写成 ":&",才代表重复替代
命令。以下示例脚本会新建分割窗口,打开可编辑缓冲区,追加文本 "princess",执行
两次替代 "s",最终得到 "prince":
vim9script
:sp | enew
append(0, 'princess')
:1substitute/s//
:&
备注: 如果脚本里直接写 "&" 而非 ":&",会触发 E112 。
< vim9-unpack-ignore
解包赋值 (或称为解构) 时,下划线可用于忽略列表项,和函数参数忽略写法类似:
vim9script
var theList: list<string> = ['A', 'M', 'Z']
var [first, _, last] = theList # 忽略第二个列表项
echo $"{first} 到 {last}"
要忽略所有其余列表项,可用 "; _":
vim9script
def GetNameCountry(): list<string>
return ['Kamoga', 'Kibaale', '乌干达', '9', 'M']
enddef
# 解包,忽略第二项以及第三项之后全部内容:
var [name, _, country; _] = GetNameCountry()
echo $"{name} 生活在 {country}"
解包表示法除了可以使用一次声明多个变量外,每个变量还可以显式声明类型,也可以根
据其值自动推断类型:
vim9script
var one: tuple<...list<any>> = (1, 'I', 'one', 'tahi', '١', '๑')
var [d: number, _, _, m; _] = one
echo $'毛利语中的数位 {d} 是 "{m}"。'
该写法仅适用于已有值的列表。每行声明一个变量通常可读性更好。
未显式声明类型的变量,其类型由所赋的值决定。";" 后的变量接收其余项目: 对于列
表,其类型为同成员类型的列表,而对于元组,其类型为剩余各项的类型组成的元组:
vim9script
var [v1; v2] = [8, 9, 10] # v2 类型为 list<number>
var [v3; v4] = (null, 'a', true) # v4 类型为 tuple<string, bool>
echo " Variable has type\n -------- --------"
echo $"{v1->printf('%12S')} {v1->typename()}"
echo $"{v2->printf('%12S')} {v2->typename()}"
echo $"{v3->printf('%12S')} {v3->typename()}"
echo $"{v4->printf('%12S')} {v4->typename()}"
原值类型为 "any" 时,无法获知内部各项类型,因此所有解包得到的变量都会是 "any"
类型。
E1163
解包赋值时如果类型不匹配,会触发 E1163。示例: 内置函数 getpos() 返回四个数值
(bufnum, lnum, col, off),因此脚本里的 "col" 类型不匹配:
vim9script
def F1163(): void
var lin: number
var col: bool
[lin, col] = getpos('w0')[1 : 2] # E1163: 变量 2: 类型不匹配…
enddef
F1163()
E1080
在 :def 函数内,如果多次使用 ";",会报 E1080,相当于脚本级别 E452 的函数域
版本:
vim9script
def F1080(): void
var [b; l; c] = getpos('w0')[0 : 2] # E1080: 无效的赋值
enddef
F1080()
常量
vim9-const vim9-final
常量如何工作因语言而异。有些语言会将无法再次赋值的变量当作常量,JavaScript 就
是一例。其他语言则会同时保证其值也不可变,当常量绑定列表时,列表内容不能修改。
Vim9 脚本两种模式都支持。
E1021 E1307
可用 :const 使变量及其值为常量。可用于复合结构以确保其不会被修改。下面脚本演
示两点:
- 常量必须提供初始值,否则报 E1021,以及
- 尝试修改常量会失败,报 E1307。
vim9script
try
const MISSING_VAL: any
catch
echo v:exception # E1021: const 需要值
endtry
def F1307(): void
const L: list<number> = [7, 8]
L->add(9) # E1307: 参数 1: 尝试修改常量 list<number>
enddef
F1307()
在脚本局部作用域尝试修改常量,也可能触发 E46 和 E741 。
注: 帮助文档示例习惯使用全大写命名常量,这只是惯例,并非强制要求。
:final E1125
可用 :final 使变量为常量,而值仍可修改,这一概念在 Java 中广为人知。下例展示
final 声明的变量可修改其值;同时展示不带初始值的 final 声明变量会报 E1125:
vim9script
final ln: list<number> = [7]
ln[1] = 9 # 向列表新增列表项 (:final 允许)
ln->add(10) # 另一种添加列表项写法
ln->extend([11, 12]) # 使用列表扩展原列表
ln[0] = 8 # 修改内部列表项同样合法
echo ln # [8, 9, 10, 11, 12]
final f1125: list<any> # E1125: final 需要值
常量设定只作用于值本身,而非其引用对象。下例中,除 "females" 列表内部列表项
外,其余项目全部被标为常量/被锁定:
vim9script
final females: list<string> = ['Martha']
const NAMES: list<list<string>> = [['Peter', 'Paul'], females]
NAMES[1][0] = 'Mary'
echo NAMES->flattennew() # ['Peter', 'Paul', 'Mary']
NAMES[1] = ["Mary"] # E741: 值已锁定
省略 :call 与 :eval
vim9-omitting-:call
调用函数时可以不加 :call 。如下例所示,使用 :call 语法仍合法,但不推荐。
vim9script
popup_create("调用函数时无需 :call", {time: 3000})
call popup_create(":call 仍可用,但并非必要", {time: 6000})
vim9-omitting-:eval
只要表达式无歧义,方法调用可以省略 :eval 。每个 -> 后面必须紧跟函数名,中间
不能换行,否则触发 E260 。同理,函数调用的左括号 "(" 前面不能换行,否则触发
E107 。以下是脚本局部方法示例: 在老式 Vim 脚本中必须写 :eval ,Vim9 脚本则不
需要:
vim9script
def Pop(arg: any): void
arg->popup_notification({time: 7000})
enddef
b:list = []
b:list->add('1')->add('2')->Pop() # 1 和 2 (在不同行上)
{a: 1, b: 2}->string()->Pop() # {'a': 1, 'b': 2}
(8, 9)->typename()->Pop() # tuple<number, number>
'单引号 ''字符串'''->Pop() # 单引号 '字符串'
"双引号 \"字符串\""->Pop() # 双引号 "字符串"
(85 >> 1 == 42)->string()->Pop() # true
# 下面代码触发 E260: -> 后面缺少名称
b:list->add('3')->
Pop()
少数场景下函数名与 Ex 命令存在歧义,这时需要在前面加 ":" 来执行 Ex 命令。例
如,既有 :substitute 命令,又有 substitute() 函数,而简写 :glob 命令与
glob() 函数等也是如此。不带 ":" 时,Vim 会将 "(" 识别为函数参数列表的左括
号,而非 pattern-delimiter 。因此,需要执行命令并使用 "(" 当作定界符时,必须
加上冒号前缀。示例:
vim9script
# 等价于 ":glob/delimiter)" (输出匹配行)
:glob(delimiter)
# 直接调用 "glob()" 函数 (会报 E121/E116)
glob(delimiter)
以 "!" 开头的表达式会被解析为外壳命令,而非条件取反。以下这行会被当作外壳命令
执行:
vim9cmd !(900 == v:version)
要用 "!" 做逻辑取反,需将表达式包裹在括号内:
vim9cmd (!(900 == v:version))->string()->popup_notification({})
注意 变量必须先定义再使用,而函数则可在定义之前调用,参见
vim9-function-defined-later 。该特性用于支持函数之间循环依赖。代价是性能略
低,运行时需要按名称查找函数。同时,函数名拼写错误仅在函数被调用时才能发现。
省略 function()
用户自定义函数可在表达式中直接作为函数引用使用,不必写 function() ,前提是该
函数已经定义。此时会校验参数类型与返回值类型。下例展示 function() 为可选写
法:
vim9script
def IsASCII(c: string): string
return $"{c} {char2nr(c) > 127 ? '不是' : '是'} ASCII 字符"
enddef
const ASCII: func(string): string = IsASCII
const F_ASCII: func(string): string = function(IsASCII)
popup_notification(['K'->ASCII(), '§'->F_ASCII()], {time: 7000})
在脚本局部作用域 (如上例),直接赋值和使用 function() 获取函数引用,都要求引
用时函数已经完成定义。
在 :def 函数内部,直接赋值与 function() 都支持前向引用,因为它们在运行时才
求值。示例:
vim9script
# IsASCII 前向引用
def ASCII(arg: string): string
var Forward_IsASCII: func(string): string = IsASCII
return Forward_IsASCII(arg)
enddef
# function(IsASCII) 前向引用
def Func_ASCII(arg: string): string
var Forward_funcIsASCII: func(string): string = function(IsASCII)
return Forward_funcIsASCII(arg)
enddef
# 以下两个函数所需的前向引用函数定义
def IsASCII(c: string): string
return $"{c} {char2nr(c) > 127 ? '不是' : '是'} ASCII 字符"
enddef
popup_notification(['K'->ASCII(), '§'->Func_ASCII()], {time: 7000})
匿名函数使用 => 而非 ->
vim9-lambda
老式 Vim 脚本里,"->" 既用于方法调用链式写法,也用于匿名函数,容易产生混淆。同
时解析器遇到 "{" 时,需要判断这是匿名函数还是字典的起始,在 Vim9 脚本中因为支
持参数类型标注,解析会更加复杂。
为规避这些问题,Vim9 脚本采用类似 JavaScript 的全新匿名函数语法,最简形式为:
(args) => expr1
该语法生成 func 类型 ( vim9-func-type ),参数默认为 any 类型,返回值类型也是
any。示例:
vim9script
# Vim9 匿名函数作为 filter() 的第二个参数
echo [8, -2, 9]->filter((_, val) => val > 0) # [8, 9]
# Vim9 匿名函数作为 func 类型
var Increment = (n) => n + 1
echo Increment(8) # 9
echo Increment->typename() # func(any): any
Vim9 脚本匿名函数可为参数和/或返回值添加严格类型标注。沿用以上的过滤示例,下例
全部输出 [8, 9]:
vim9script
echo [8, -2, 9]->filter((_, val: number) => val > 0)
echo [8, -2, 9]->filter((_, val): bool => val > 0)
echo [8, -2, 9]->filter((_, val: number): bool => val > 0)
用作 func 类型并赋值给 Funcref 变量时,一共有 14 种合法写法,示例如下:
vim9script
const I01 = (n) => n + 1
const I02 = (n): number => n + 1
const I03 = (n: number) => n + 1
const I04 = (n: number): number => n + 1
const I05: func = (n) => n + 1
const I06: func = (n: number) => n + 1
const I07: func = (n): number => n + 1
const I08: func = (n: number): number => n + 1
const I09: func: number = (n: number) => n + 1
const I10: func: number = (n): number => n + 1
const I11: func: number = (n: number): number => n + 1
const I12: func(number): number = (n): number => n + 1
const I13: func(number): number = (n: number) => n + 1
const I14: func(number): number = (n: number): number => n + 1
echo 0->I01()->I02()->I03()->I04()->I05()->I06()->I07()->I08()
->I09()->I10()->I11()->I12()->I13()->I14() # 14
备注:
1. 返回类型取变量声明或匿名函数返回类型中更特定的类型。只要任意一处写
了 ": {type}",就会作为匿名函数的返回类型。变量声明不能有类型冲突,
必须匹配匿名函数显式标注或已推断的返回类型,或为 any,或省略。匿名
函数未声明返回类型时,会从表达式自动推断 (例如 I03 推断为 number)。
2. 参数类型由匿名函数的参数声明决定,不受变量类型声明 (即使有) 影响。
因此 I02、I07、I10、I12 的 typename() 均为 "func (any): number"。
应当尽量避免这类写法: 表面看参数接受 "any",实际并非如此,极易引发
类型不匹配。
3. 以下是非法匿名函数示例,触发 E1012 。失效原因: (a 和 b) 变量声明要
求返回 number,但匿名函数返回类型是 "any",或 (c) "func({type}) ="
这种写法本身永远非法。
vim9cmd var a: func: number = (n) => n + 1
# E1012: 类型不匹配;预期 func(...): number 但得到 func(any): any
vim9cmd var b: func(number): number = (n) => n + 1
# E1012: 类型不匹配;预期 func(number): number 但得到 func(any): any
vim9cmd var c: func(number) = (n) => n + 1
# E1012: 类型不匹配;预期 func(number) 但得到 func(any): any
前面示例中的 "Increment" 匿名函数,给变量声明 "func" 类型,同时标注匿名函数参
数与返回类型 (对应上面 I08):
vim9script
var Increment: func = (n: number): number => n + 1
echo Increment(0) # 1
echo Increment->typename() # func(number): number
备注: 显式声明 func 类型看似冗余,但可以一眼看出变量是函数类型。同时出于上述原
因,建议显式标注匿名函数参数与返回类型。运行性能也更好,不再需要 "any" 类型校
验 (可用 :disassemble 验证)。本帮助文档大量示例都采用这种写法。
E1157
虽然匿名函数的返回类型无需强制声明,但只要写了冒号,就必须指定返回类型:
vim9script
var Minimal = (n) => n / 2
var Missing = (n): => n / 2 # E1157: 缺少返回类型
匿名函数的参数部分,直到 "=>" (含) 为止,中间不能换行 (方便 Vim 区分带括号表达
式和匿名函数参数)。以下写法合法:
vim9script
var list: list<number> = [1, -2, 2, -3, -1, 3]
filter(list, (_, val): bool =>
val > 0)
echowindow list
以下写法不合法,会报 E121 与 E116 :
vim9script
filter(list, (ind, val): bool
=> val > 0)
以下写法也不合法,会报 E121 与 E116 :
vim9script
filter(list, (ind,
val): bool => val > 0)
以下写法同样不合法,会报 E1157 :
vim9script
filter([1, 2], (ind, val):
bool => val > 0)
但可用反斜杠拼接行,然后进行解析。下例同时演示了匿名函数标注类型的 "val" 参数:
vim9script
var list: list<number> = [1, -2, 2, -3, -1, 3]
filter(list,
\ (_,
\ val: number):
\ bool
\ => val > 0)
echowindow list
E1172
匿名函数不支持参数缺省值:
vim9script
var F_1172 = (n = 1) => n + 1 # E1172: 不能在 lambda 中使用默认值
vim9-lambda-arguments
老式 Vim 脚本调用匿名函数时可以传入任意多余参数,不会有警告。Vim9 脚本则要求参
数的数量必须匹配。要接收任意数量或额外参数,可用 "..._" 开启可变参数,参见
vim9-variable-arguments 。示例:
vim9script
b:fruits = ['apple', 'apricot', 'avocado']
def Complete(..._): list<string>
const K: string = getcmdline()[: getcmdpos() - 2]->matchstr('\S*$')
return b:fruits->filter((_, val) => val =~ K)
enddef
var fruit = input('> ', '', $"customlist,{Complete->string()}")
# (例如,键入 p<Tab> 会显示 apple 和 apricot 候选)
echowindow $"选中了 {fruit}"
备注: 不写 "..._" 函数会报错。 input() 的补全回调参数 {completion} 包
含三项 (ArgLead, CmdLine, CursorPos)。本例不需要使用这些参数构造输出列
表,所以使用 "..._" 更好;但也可用 "...l: list<any>"。参见
:command-completion-custom 。
inline-function
lambda 可以在 {} 代码块内写多条语句。下例读取三个缓冲区局部选项:
vim9script
var Get_bv: func = (...args: list<string>): dict<any> => {
var opts: dict<any>
for arg in args
opts[arg] = getbufvar(bufname(), $'&{arg}')
endfor
return opts
}
echowindow Get_bv('modifiable', 'tabstop', 'textwidth')
这种写法也适用于 Channel 、 Job 或 timer 。以下是定时器示例:
vim9script
echowindow "回调调用中..."
var count: number
var timer: number = timer_start(700, (_): void => {
count += 1
echowindow $'回调调用了 {count}'
}, {repeat: 3})
备注: 本例 timer_start() 要求第二个参数是回调函数 {callback}。定时器
触发时 Vim 会把定时器 ID 作为必需参数传给回调。匿名函数里,"(_)" 语法
接收该 ID,无需命名也无需引用,参见 vim9-ignored-argument 。
代码块的结束 "}" 必须在行首位置 (忽略前置空格或制表符),后面可跟其他字符。"{"
后面不能写其他命令,但可以写注释。示例:
vim9script
var numbers: list<number> = [1, 2, 3, 4, 5]
var Square: func = (ln: list<number>): list<number> => ln
->mapnew((_, val) => { # 代码块的 { 后接受注释
return val * val
}) # 代码块的 } 必须开启新行 (前置空白除外)
echowindow Square(numbers)
E1171
内联函数代码块如果缺少闭合 "}" 会触发 E1171:
vim9script
var F_1171 = (): bool => {
return false # E1171: 内联函数后缺少 }
command-block
代码块也可用于定义用户命令,块内部使用 Vim9 脚本语法。下例使用嵌入文档
(heredoc) (参见 :let-heredoc ):
vim9script
command MyHeredoc {
var someVar: list<string> =<< trim eval END
生命、宇宙以及一切
{6 * 7}
END
echowindow someVar
}
MyHeredoc
备注: "=<<" 后面必须写 "eval",这样 "{6 * 7}" 才会被求值,而不是当作
literal-string 。尽管命令块内是 Vim9 脚本,但这一点和
vim9-omitting-:eval 不同。
如果语句包含字典,字典的闭合 "}" 不能写在行首,否则会被解析器识别为代码块的结
束标记。下例无法运行 (最后一行报 E1128 ),因为倒数第二个 "}" 被识别为命令块结
束:
command FailingNewCommand {
g:mydict = {
'k1': 'v1',
'k2': 'v2'
}
}
解决办法是将字典的 "}" 放在最后一个键值对的同一行:
vim9script
command WorkingNewCommand {
g:mydict = {
'k1': 'v1',
'k2': 'v2' }
g:mydict->keys()->popup_notification({time: 4000})
}
WorkingNewCommand
理据: "}" 不能跟在命令后面,否则需要解析全部命令才能定位块结束。为保持一致,
"{" 后面不能紧跟命令。因此 "() => { command }" 这种写法不适用;同理,结束命令
块的 "}" 前面必须换行 (允许前置空格或制表符)。
E1026
代码块缺少闭合 "}" 触发 E1026 ("缺少 }"):
vim9script
command C1026 {
vim9-curly
为防止字典起始 "{" 被识别成语句块,可把字典包裹在括号内:
vim9script
var AgeDict: func = (arg: number): dict<number> => ({'age': arg})
echo 42->AgeDict()
同理,字典套括号可避免和命令块起始混淆。例如,如果字典左大括号后接换行,用括号
包裹就可防止被解析成命令块:
vim9script
command -nargs=+ PopDict {
var [key, val] = split(<q-args>)
({
[key]: val})
->string()->popup_notification({time: 5000})
}
PopDict one tahi
备注: 即使在括号内部,字典的 "}" 也不能单独占一行。另外字典的键必须放
在方括号内,才会被当作表达式求值,参见 vim9-literal-dict 。
自动续行
vim9-line-continuation
很多场景下,表达式明显会延续到下一行。这类情况在 Vim9 脚本中无需在行首添加反斜
杠,而老式 Vim 脚本里,列表、字典、元组、函数调用跨多行时,必须写反斜杠 (参见
line-continuation )。下例演示这四种结构:
vim9script
var mylist: list<number> = [
1,
2,
]
var mydict: dict<number> = {
1: 1,
2: 2,
}
var mytuple: tuple<number, number> = (
1,
2,
)
var MyDef: func = (...l: list<any>): list<any> => l
echo MyDef(mylist,
mydict,
mytuple) # [[1, 2], {'1': 1, '2': 2}, (1, 2)]
对于不在列表、字典、元组内部的表达式二元与三元运算符,换行可以放在运算符之前或
者之后。示例:
vim9script
var text: string = 'V'
.. 'i'
.. 'm'
var version: number = 9 *
100
+ 2
var latest: string = v:version == 902
? '最新版本' :
'过时版本'
echo (text, version, latest) # ('Vim', 902, 'current')
使用 "->" 的方法调用允许在 "->" 之前换行。用于成员访问的 "." 同理。以下交互式
示例中,输入配速 (分:秒),返回 5 公里跑/走的总秒数以及分秒格式:
vim9script
def Run5(ms: string): dict<any>
const M: number = ms
->split('[:.]')[0]
->str2nr()
const S: number = ms
->split('[:.]')[1]
->str2nr()
var secs: number = (M * 5 * 60) + S * 5
var m_s: string = (secs / 60) .. ':' .. printf('%02d', (secs % 60))
return {seconds: secs, minutes_seconds: m_s}
enddef
var result: dict<any> = input('配速 (分:秒) / km: ')
->Run5()
echo ' 对应 5 公里完成时间 (总秒数,分:秒):'
echo (result
.seconds,
result
.minutes_seconds)
对于参数为命令序列的命令,行首的 | 字符 (参见 :bar ) 代表续行:
vim9script
command! ShowFileLine if !empty(bufname())
| echo $'File: {bufname()}'
| echo $'Line: {line(".")}'
| endif
ShowFileLine
备注: 因此嵌入文档的首行缺省不能以竖线开头:
vim9script
# E488: 多余的尾部字符: | 这样不行
var lines =<< trim END
| 这样不行
END
解决办法是,开头留空行,或者不使用嵌入文档。也可以临时在 'cpoptions'
中包含 "C" 标志位:
vim9script
const CPO: string = &cpoptions
execute CPO->match('C') == -1 ? 'set cpoptions+=C' : ''
var lines =<< trim END
| 这样就可以
END
echo lines[0]
&cpoptions = $'{CPO}'
如果嵌入文档写在函数内部,'cpoptions' 必须在 :def 之前设置,在
:enddef 之后恢复。
E1097
不完整的续行会报 E1097:
vim9script
def F1097(): void
var x =
enddef
defcompile # E1097: 行不完整
某些场景仍然需要反斜杠续行,例如拆分很长的 Ex 命令,这类续行里注释可以用 '#\ '
开头 (类似老式 Vim 脚本的 '"\ '):
vim9script
syntax region IncSearch
\ oneline
#\ 加载此脚本会将 IncSearch 高亮组
#\ 应用于本缓冲区的 "Ex command" 文本。
\ start='Ex '
\ end='command'
使用无反斜杠自动续行,且某一行以竖线开头时,同样要用这种注释写法。示例:
vim9script
command! BufferWords echo $'缓冲区名: {bufname()}'
#\ 本缓冲区的单词数统计
| echo $'单词数: {wordcount().words}'
BufferWords
E1050
为让行首运算符能够被正确识别,范围前面必须加冒号。下例定义 "start" 和 "print"
(注意这里会遮蔽 :print 命令):
vim9script
var print: number = 1
# 下面两行等价于单行写法
# var start: number = 1 + print
var start: number = 1
+ print
echo start
而下面代码会给 "start" 赋值,同时显示脚本的第二行:
vim9script
var start: number = 1
:+ print
如果省略冒号,会触发 E1050。和上例不同,此处 "print" 不是变量:
vim9script
var start: number
+ print
# E1050: 范围前需要有冒号:+ print
范围之后必须跟 Ex 命令。不带冒号可以不用 :call 调用函数,但范围后面必须写
:call :
vim9script
const PRINT_W_DIVIDERS: func = (): void => {
echohl Statement
:print
echo "_"->repeat(70)
echohl None
}
# 无范围时,调用函数无需 `call`;加载脚本时,仅显示包含 'vim9script'
# 这一行,前面加分隔线
PRINT_W_DIVIDERS()
# 带范围时,必须写 :call;加载脚本时,显示从 'const...' 到闭合 '}' 之
# 间的行,用分隔线隔开
:+,+6call PRINT_W_DIVIDERS()
但 +cmd 参数不需要冒号。示例:
vim9script
# 新开窗口分割帮助缓冲区,对 'MATCHME' 应用 IncSearch 高亮组
split +/MATCHME
match IncSearch _MATCHME_
函数参数也可以跨多行书写。示例:
vim9script
def SubBlankHyphen(
text: string,
separator: string = ' '
): string
return text->substitute('[[:blank:]]', separator, 'g')
enddef
echo '多行参数示例'->SubBlankHyphen()
由于续行无法简单识别,命令解析规则更加严格。老式 Vim 脚本中,一处错误可能导致
续行被意外解析。以下是一段可正常运行的脚本:
vim9script
def Msg(..._): void
popup_notification("作业成功结束!", {time: 1500})
enddef
var myjob = job_start([&shell, &shellcmdflag, 'date'], {
exit_cb: Msg})
但如果代码存在错误,例如下面脚本,Vim9 脚本会直接报错:
vim9script
var myjob = job_start([&shell, INVALID_LIST_ITEM_HERE, 'date'], {
exit_cb: Msg})
# 报错 E121、E116,脚本停止执行
下面对应的老式 Vim 脚本中, :let 命令会执行失败,但解析会继续,把 "exit" 当作
:exit 命令,并带参数 "_cb: Msg})",这会尝试用该字符串作为文件名保存文件,然
后退出:
" *** 不 要 执 行 本老式 Vim 脚本 ***
" 一旦执行,会写入文件 '_cb: Msg})' 然后退出!
"
let myjob = job_start([&shell, INVALID_ITEM_HERE, 'date'], {
exit_cb: Msg})
E1144
为避免出现上文那种意外行为,Vim9 脚本要求大部分命令名与参数之间必须有空白字符
(备注: 带定界符的命令,例如 :global 与 :substitute ,属于例外)。示例:
vim9script
exit_abc
# E1144: 命令 "exit" 后面没有空格:exit_abc
但当一条命令的参数本身是另一条命令时,自动续行无法稳定生效。例如
"windo echo {expr}" 中,表达式内部换行会导致该命令只会作用于当前窗口,不会作用
于其余窗口:
vim9script
windo echo 'hi'
->toupper() # 'HI' (当前窗口) and 'hi' (其他窗口)
所以这类场景仍然必须使用 "\" 续行符:
vim9script
windo echo 'hi'
\ ->toupper() # 所有窗口都输出 'HI'
备注: 此类不一致性可能会产生严重后果,例如,假定命令是
"windo execute ':global/something/'",而续行写为 ".. d"。此时,匹配
"something" 的行只会在当前窗口被删除;其余窗口只会对匹配行执行缺省的
:print (即 :p ) 命令。
其他续行相关注意事项:
- :enddef 不能放在续行行首,它会直接终止当前 :def 函数。参见 E1057 。
- 赋值语句左侧不允许换行。针对列表解包 ( :let-unpack ),以下写法合法:
vim9script
const UNPACK: func = (a: string, b: string): list<string> => [a, b]
var [v1, v2] =
UNPACK('all', 'good')
echo $'{v1} {v2}'
而以下写法无效,会报 E475 :
var [v1,
v2] = UNPACK('all', 'good')
- :echo 、 :execute 等命令的参数中间不允许换行。以下写法合法:
vim9script
echo [1,
2] [3,
4]
而以下写法无效,不会输出 "[3, 4]" 部分:
vim9script
echo [1, 2]
[3, 4]
- 部分场景 Vim 难以解析命令,尤其是命令作为另一条命令的参数时,例如 :windo 、
:command 、 :autocmd 等。这类场景必须使用反斜杠续行。示例:
command! Foo call Bar('x', {
\ 'key': 'value',
\ })
autocmd BufWritePre * call Bar('x', {
\ 'key': 'value',
\ })
同时参考上文的 "windo echo" 示例与注释。
空白字符
vim9-white-space
Vim9 脚本强制规范空白字符的使用。例如变量赋值的 "=" 前后必须有空白:
vim9script
var num = 234
以下场景必须使用空白字符:
E1004
- 变量声明中 "=" 的前后:
vim9script
var num=234 # E1004: '=' 的前后需要空白
# 下面写法同样报 E1004:
var num= 234
var num =234
- 大部分运算符两侧。以下首例合法,第二例报 E1004:
vim9cmd echo 'Yes' .. '!' # Yes!
vim9cmd echo 'F'..'ail' # E1004: '.." 的前后需要空白
- 子列表 (切片) 中的 ":" 两侧,但出现在首尾位置时例外:
vim9script
var mylist: list<number> = [7, 8, 9]
echo mylist[:] # [7, 8, 9]
echo mylist[1 : 2] # [8, 9]
echo mylist[: 1] # [7, 8]
echo mylist[2 :] # [9]
echo mylist[1:2] # E1004: ':' 的前后需要空白
E1069
- 变量名加冒号 ":" 后接类型声明时,":" 后要有空白:
vim9script
var okay_list: list<any> = ['可','用']
var fail_list:list<any> = ['不', '行'] # E1069: ':' 后要求有空白
- 注释起始的 '#' 前要有空白。缺少空白会触发 E121 或 E488 等错误:
vim9cmd echo 'No'# E121: 未定义的变量: #
vim9cmd var f: number = 99# E488: 多余的尾部字符:#
以下场景不允许使用空白字符:
E1068
- 字典键值对、列表项的逗号前不能有空白 (元组不受此限制)。示例:
vim9cmd echo (1 , 2) # (1, 2) [注: Vim 会自动规范化元组]
vim9cmd echo {1: 1 , 2: 2} # E1068: ',' 前不允许有空白
vim9cmd echo [1 , 2] # E1068: ',' 前不允许有空白
- 函数参数的逗号前不能有空白:
vim9script
def F1068(arg: string , arg2: bool): void
# E1068: ',' 前不允许有空白: , arg2: bool): void
enddef
- 泛型函数声明中的 "<" 或 "(" 前不能有空白:
vim9script
def F1068<T> (): T # E1068: '(' 前不允许有空白: (): T
enddef
F1068()
E1074
- 导入成员的 '.' 后不能有空白。以下示例会临时生成 Vim9 脚本并导入。该脚本导出
的 "okay" 变量可正常输出,但第二个 "Imp" 的 '.' 后的空格会触发 E1074:
vim9script
var tmp: string = $'{tempname()}.vim'->substitute('\\', '/', 'g')
var temp_lines: list<string> = ['vim9script',
'export var okay: bool = true', 'export var err: bool']
temp_lines->writefile(tmp)
import tmp as Imp
echo Imp.okay # true
echo Imp. err # E1074: 点后不允许有空白
- 函数名与左括号 "(" 之间不能有空白,但在括号内,任意参数前和最后参数之后都允
许空白:
vim9script
def MyN(...arg: list<number>): void
echo arg
enddef
MyN(1) # [1]
MyN( 2) # [2]
MyN(3 ) # [3]
MyN( 4, 5, 6 ) # [4, 5, 6]
MyN (7, 8, 9) # E492: 不是编辑器的命令
备注: 以下写法同样不合法,报 E492 :
MyN
\ (7, 8, 9)
MyN
(7, 8, 9)
E1202
- 访问对象属性、方法、枚举成员的 '.' 后不能有空白。枚举方法示例:
vim9script
enum Metal
Au((79, 'Gold')),
Hg((80, 'Mercury'))
var data: tuple<number, string>
def Get_name(): string
return this.data[1]
enddef
endenum
echo Metal.Au.Get_name() # Gold
echo Metal.Hg. Get_name() # E1202:'.' 后不允许有空格
E1205
- :set 命令中,选项名和后续的 "&"、"!"、"<"、"="、"+="、"-="、"^=" 之间不能
加空白。示例:
vim9script
# 合法写法
set tabstop=8
echo &tabstop
# 报错 E1205: 选项和之间不允许有空格: =8
set tabstop =8
- :set 命令中,选项名和后续的 ":" 之间不能加空白,会报 E518 。示例:
vim9script
# 合法写法
set tabstop:8
echo &tabstop
# 报错 E518: 未知的选项: :8
set tabstop :8
没有花括号扩展
vim9-no-curly-braces-expansion
Vim9 脚本作用域内,不能使用花括号构造动态变量名 ( curly-braces-names )。下例展
示了在老式 Vim 脚本 :function 中,可用花括号扩展构造脚本级变量名,但在
:def 函数中会报错:
vim9script
var [O, O3] = ['oxygen', 'ozone']
function Al(arg, n = null_string) abort
return s:{a:arg}{a:n}
endfunction
def Al9(arg: string, n: any = null_string): string
return {arg}{n}
enddef
echo Al('O', 3) # ozone
echo Al9('O', 3) # E720: Dictionary 中缺少冒号: }{n}
同理, curly-braces-function-names 仅能在老式 Vim 脚本作用域使用:
vim9script
var low_line: string = '_'
function F_curly()
echo '花括号扩展函数名: 老式 Vim 脚本作用域中可用'
endfunction
legacy call s:F{s:low_line}curly()
F{low_line}curly() # E1144: 命令 "F" 后面没有空格
不总是忽略命令修饰符,可能报错
部分场景下,为不支持命令修饰符的命令加上命令修饰符会报错。但也有一些修饰符不会
报错 (和老式 Vim 脚本行为一致)。例如以下脚本中,无意义的 :vertical 、
:keepmarks 、 :hide 修饰符会被忽略:
vim9script
vertical if 1 == 1
keepmarks echo '启动中...'
try
echo ERROR
catch
hide echo '已捕获!'
endtry
endif
E1176
但如果将修饰符用于部分控制流命令,会报 E1176。在 :endif 前加 :silent 的示
例:
vim9script
if 1
echo "Hi!"
silent endif
# E1176: 错放的命令修饰符
备注: 如果仅执行本脚本的第 2 至 4 行,不会报 E1176,因为此时脚本运行在
老式 Vim 脚本上下文。
同理,为 :try 、 :endtry 、 :for 、 :endfor 、 :while 、 :endwhile 、
:catch 、 :finally 添加修饰符可能触发 E1176,但取决于所用的冗余修饰符。例如
为 :for 加 :silent 会被忽略;为 :endfor 加 :keepmarks 则报错 (备注:
:silent 行为是刻意设计,Vim 编译时未加入 +eval 特性时,此行为可用于抑制错
误消息)。
E1082
此外,如果修饰符后面没有跟随命令,会报 E1082:
vim9script
silent
# E1082: 命令修饰符没有命令
字典常量
vim9-literal-dict
旧版 Vim 使用 {} 语法书写字典常量:
let dict = {'key': value}
后来因为纯文本键名非常常用,在保持向后兼容前提下引入常量字典写法:
let dict = #{key: value}
但此 #{} 语法和其他语言差异很大。实际使用中常量键远多于表达式键,考虑到
JavaScript 也使用这种语法,采用 {} 形式表示字典常量被认为更实用。在 Vim9 脚本
中,{} 使用常量键:
var dict = {key: value}
示例:
vim9script
var dict = {key: 9}
echo dict # {'key': 9}
备注: Vim 会自动规范化键名,自动添加单引号。字典键类型永远是字符串。
常量键支持字母、数位、下划线和短横线。如果需要使用其他字符,或者要用表达式,可
选以下三种方式:
- 使用单引号或双引号字符串,或
- 使用 extend() 配合常量键语法,或
- 将表达式键放在 [] 内 (类似 JavaScript 的计算属性)。
下例同时演示三种写法:
vim9script
var dict: dict<bool>
dict["key\twith\ttabs"] = true # 双引号字符串
dict->extend({[40.9 + 1.1]: true}) # 通过表达式求值生成键
dict['¡non–ASCII! w/ spaces'] = true # 单引号字符串
for k in dict->keys()
echo [k, dict[k]]
endfor
键的类型可以是字符串、数值、布尔、浮点数,但所有键最终都会以字符串形式保存。使
用其他类型访问会报错:
vim9script
var dict: dict<bool>
dict[9] = true # 数值键
dict[true] = true # 布尔健
dict[9.2] = true # 浮点键
echo dict # {'true': true, '9.2': true, '9': true}
dict[(9, 2)] = false # E1522: 将元组作为字符串使用
不写 [] 时,键被视为常量,所以会保留前导零;而写在 [] 里的表达式会先求值,再转
为字符串,求值时会去掉前导零。示例:
vim9script
var dict = {09: '09 为常量 "09"', [09]: '[09] 会求值为 "9"'}
echo dict['09'] # '09 为常量 "09"'
echo dict['9'] # '[09] 会求值为 "9"'
E1139
如果 [] 内的键非法,会触发 E1139 等错误,例如:
vim9script
var E1139 = {[0😢9]: 2} # E1139: 字典键后缺少匹配的括号
E1014
非法键触发 E1014:
vim9script
# 尝试使用未转义控制字符 U+0007 (响铃符)
var E1014: dict<string> = {: 'E1014'} # E1014: 无效的键:^G
无论是给出字典常量还是用在下标赋值中,浮点数键必须写在 [] 内部。除这两种上下文
外,浮点数中的 '.' 都会被识别为无效的 Vim9 脚本字典常量键:
vim9script
var dict: dict<string> = {[.09]: '正常'}
dict[0.10] = '正常'
echo dict # {'0.09': '正常', '0.1': '正常'}
try
dict->extend({0.11: '失败!'})
catch
echo v:exception # E720: Dictionary 中缺少冒号: .11: 'fail!'
endtry
E1127
如果 '.' 后面省略名称,报 E1127:
vim9script
def F1127(): void
var d = {x: 0}
echo d.
enddef
F1127() # E1127: 点后面缺少名称
不支持 :xit、:t、:k、:Print、:append、:change、:insert、:open
部分命令容易和局部变量名混淆,但它们都有等价替代命令:
禁止使用 替代形式
:k :mark
:Print :print
:t :copy
:xit :exit
备注: :x 等简写形式同样不允许。
E1100
下例脚本中 `mark x` 可正常运行,而 `k x` 会报 E1100:
vim9script
:+2
k x
# E1100: Vim9 脚本不支持此命令(缺少 :var?): k x
还有部分命令 (包括简写,如 :a 、 :o 、 :ch ) 在 Vim9 脚本里完全不可用,会触发
E1100:
:append
:change
:insert
:open
另见 vim9-invalid-Ex-commands 。
比较运算符
vim9-comparators
字符串比较运算符不会考虑 'ignorecase' 选项。因此 =~ 和 =~# 行为一致,比较
会区分大小写:
vim9script
var ic: bool = &ignorecase
set ignorecase
# 两条都输出 'false',因为不会使用 'ignorecase'
echo 'a' =~# 'A'
echo 'a' =~ 'A'
# 老式脚本中,区分大小写的 '=~#' 输出 0。不区分大小写的 '=~' 输出 1
legacy echo 'a' =~# 'A'
legacy echo 'a' =~ 'A'
# 恢复脚本运行前 'ignorecase' 的旧值
&ignorecase = ic
"is" 运算符 ( expr-is ) 用于字符串比较时,返回 false (仅当待比较字符串是显式
null 或未初始化时除外)。对应的 "isnot" ( expr-isnot ) 则返回 true。这是因为,
老式 Vim 脚本作用域比较字符串内容,而 Vim9 脚本作用域比较对象身份。Vim9 中字
符串使用时会先复制,因此两个字符串不指向同一实例;未来如果改为引用计数机制而非
复制,此行为可能会有变化。示例:
vim9script
var str: string = ''
echo str is str # false
legacy echo s:str is s:str | # 1
var x: string
var y: string = null_string
echo x is y # true
对于布尔、数值、浮点型: 老式 Vim 脚本中 "is" 和 "isnot" 的行为与字符串比较类
似,进行内容比较。而在 Vim9 脚本中,除数值与浮点数互比外,其他使用 "is" 的比较
都会报 E1037 或 E1072 :
vim9cmd var [boo: bool, num: number, flo: float] = [true, 9, 9.2]
legacy echo s:boo is v:true | " 1
legacy echo s:num is 9 | " 1
legacy echo s:flo is 9.2 | " 1
legacy echo s:boo is 9 | " 0
legacy echo s:boo is 9.2 | " 0
legacy echo s:num is 9.2 | " 0
vim9cmd echo boo is true # E1037: 不能对 bool 使用 "is"
vim9cmd echo num is 9 # E1037: 不能对 number 使用 "is"
vim9cmd echo flo is 9.2 # E1037: 不能对 float 使用 "is"
vim9cmd echo boo is 9 # E1072: 不能比较 bool 和 number
vim9cmd echo boo is 9.2 # E1072: 不能比较 bool 和 float
vim9cmd echo num is 9.2 # false
同理,"is" 和 "isnot" 不能用于对作业和通道类型进行比较。下例展示了老式脚本中作
业可用 "is" 进行比较,而 Vim9 脚本会报 E1072 :
vim9script
var job1: job
var job2: job = job1
legacy echo s:job1 is s:job2 | # 1
echo job1 is job2 # E1072: 不能比较 job 和 job
对于列表、字典、元组、blob 这类容器类型,使用 "is" 和 "isnot" 的行为在 Vim9 脚
本和老式脚本一致: 比较实例对象身份,而非比较内容。下面用 Vim9 列表举例,附带老
式脚本对比:
vim9script
var l1: list<any>
var l2: list<any> = l1
echo l1 is l2 # true (同一实例)
legacy echo s:l1 is s:l2 | # 1
echo l1 is [] # false (不同实例)
legacy echo s:l1 is [] | # 0
echo l1 is null_list # false (不同实例)
对于 Funcref 变量类型,比较行为和容器变量大体相同,但未初始化的 Funcref 与
Vim9 的 null_function 比较时,行为特殊:
vim9script
var F1: func
var F2: func = F1
echo F1 is F2 # true (相同未初始化实例)
echo F1 is null_function # true
Vim9 脚本中,类对象比较规则类似:
vim9script
class C
endclass
# 未初始化类对象
var O1: C
var O2: C
echo O1 is O2 # true (相同未初始化类对象)
echo O1 is null_object # true
# 已初始化类对象
var O4: C = C.new()
var O5: C = O4
var O6: C = C.new()
echo O4 is O5 # true (相同已初始化类对象)
echo O5 is O6 # false (不同类对象)
备注: 类本身不能参与比较:
vim9script
class C
endclass
echo C is null_class # E1405: 类 "C" 不能作为值使用
枚举对象比较和类对象不同。每个枚举值都是单例,指向同一个实例。即使修改枚举值内
的可变实例成员,改动会作用到所有引用该枚举值的变量:
vim9script
enum Switch
On(['active', true]),
Off(['inactive', false])
final state: list<any>
endenum
var on1: Switch = Switch.On
var on2: Switch = Switch.On
on2.state[0] = 'engaged'
# 修改会同时影响 on1 与 on2,二者指向同一个枚举值
echo on1 is on2 # true
# 未初始化枚举变量 (不存在 null_enumvalue)
var uninitialized: Switch
echo uninitialized is null_object # true
总结 "is" 与 "isnot" 运算符规则:
- Vim9 脚本相比老式 Vim 脚本有更严格限制:
- 字符串比较行为不同,
- 列表、字典、元组、blob 行为相同,
- 不允许布尔、数字、浮点数、作业、通道之间使用 is 比较 (数值和浮点数互
比除外),
- 函数引用行为大体相同 (但 Vim9 脚本的 null_function 行为特殊)。
- Vim9 脚本独有:
- 类对象比较逻辑类似函数引用,
- 枚举对象使用 "is" 和 "isnot" 比较时,判断是否为同一个枚举单例。
出错后终止执行
老式 Vim 脚本遇到错误时,会继续执行错误行之后的代码,可能连续爆出大量错误,需
要按 CTRL-C 中止。示例脚本会产生两条 E121 错误:
" 老式 Vim 脚本
let x = does_not_exist
let y = does_not_exist_too
而 Vim9 脚本会在首个错误处停止执行:
vim9script
var x = does_not_exist # E121: 未定义的变量: does_not_exist
# 执行终止,下一行不会运行
var y = does_not_exist_too
for 循环
循环变量不能预先声明:
vim9script
var it: list<number>
for it in [1, 2, 3] # E1041: 重定义脚本项目:"it"
但可以使用带前缀的变量,如缓冲区局部变量:
vim9script
b:i = []
for b:i in [1, 2, 3]
echo b:i
endfor
E1254
:def 函数内的循环变量不能使用 s: 脚本变量:
vim9script
def F()
for s:n in range(9) # E1254: 不能在 for 循环中使用脚本变量
endfor
enddef
defcompile
老式 Vim 脚本有一些技巧,可以在遍历列表时删除当前项或前一项。在 Vim9 脚本非编
译作用域中,同样的技巧依然可用。但在 Vim9 脚本编译作用域中,删除项目会导致下一
项被跳过 ("迭代器失效"),行为和 Python 和 Ruby 一致。以下脚本演示非编译与编译
作用域的差异:
vim9script
echo '在 Vim9 脚本非编译作用域中删除列表项:'
# 输出 10,20,30,40,列表结果为 []
var list_one: list<number> = [10, 20, 30, 40]
for n in list_one
echo n
list_one->remove(index(list_one, n))
endfor
echo list_one
echo '在 Vim9 脚本编译作用域中删除列表项:'
# 输出 10,30,列表结果为 [20, 40]
# 原因: 删除 10 之后,20 移动到下标 0,但迭代器前进到下标 1,此时下标
# 1 的值变成 30!
var list_two: list<number> = [10, 20, 30, 40]
var Remove_items: func = (): void => {
for n in list_two
echo n
list_two->remove(index(list_two, n))
endfor
echo list_two
}
Remove_items()
该例说明遍历列表时尽量不要修改原列表,先复制一份通常更安全。
遍历嵌套列表时,可以修改内层列表。循环变量隐含 :final 声明: 变量本身不能重新
赋值,但它指向的内容可以修改。示例:
vim9script
var lst: list<list<number>> = [[1, 2], [3, 4], [5, 6]]
for subl in lst
subl[0] = 9
endfor
echo lst # [[9, 2], [9, 4], [9, 6]]
for subl in lst
subl = [9, subl[1]] # E46: 不能改变只读变量 "subl"
endfor
E1306
:for 与 :while 的循环嵌套总深度不能超过 10 层。
条件与表达式
Vim9 脚本在布尔上下文的类型检查比老式 Vim 脚本更严格。大部分运算符要求操作数为
布尔值、0/1 或是 Special 专用类型。假值运算符 ( ?? ) 与逻辑非 ( expr-! ) 采
用真值判定规则。
– 严格布尔表达式
vim9-boolean
Vim9 脚本中, :if 、三元运算符 ternary 、 :while 、 || ( expr-barbar ) 以及
&& ( expr-&& ) 这些条件结构要求严格布尔类型。老式 Vim 脚本不同: 任意非零数值
都视为真,字符串与特殊类型会隐式转为数值。以下老式脚本示例中所有条件表达式全部
合法:
" 老式 Vim 脚本
" 以下求值结果为 1:
echo 1 ? 1 : 0
echo 0 || 1
echo 1 || v:false
echo 1 && v:true
echo v:null || 1
echo 99 ? 1 : 0
echo "99" ? 1 : 0
" 以下求值结果为 0:
echo 0 || v:none
echo 0 && 1
echo "text" ? 1 : 0
Vim9 脚本的条件表达式 ( ?? 与 ! 除外) 类型检查 type-checking 更严格:
- 数值仅允许 0 (假) 或 1 (真),其余数值触发 E1023 ,
- 字符串不允许使用,触发 E1135 。
vim9cmd echo 1 ? 1 : 0 # 1
vim9cmd echo 0 || 1 # true
vim9cmd echo 1 || false # true
vim9cmd echo 1 && true # true
vim9cmd echo null || 1 # true
vim9cmd echo 0 || v:none # false
vim9cmd echo 0 && v:true # false
vim9cmd echo 9 ? 1 : 0 # E1023: 将整数作布尔值使用:9
vim9cmd echo "9" ? 1 : 0 # E1135: 将字符串作布尔值使用:"9"
vim9cmd echo "x" ? 1 : 0 # E1135: 将字符串作布尔值使用:"x"
– 准假值运算符
vim9-falsy
绝大多数类型使用 ?? (准假值运算符 falsy-operator ) 不会报错。所有类型值分
为准真值与准假值,各类判定准假值规则如下:
类型 准假值条件
Number 零
String 空字符串
Funcref null
List 空列表
Dictionary 空字典
Float 0.0
Boolean false (包含特殊值 Special v:false )
None 永远 ( v:null 和 v:none )
Job null
Channel null
Blob 空 blob
Class 不适用 (参见 E1405 )
Object null
Typealias 不适用 (参见 E1403 )
Enum 不适用 (参见 E1421 )
EnumValue null
Tuple 空元组
void 永远
示例脚本展示除报错类型以外各类的准假值判定:
vim9script
echo "Number: \t0\t\t" 0 ?? 'is falsy'
echo "String: \t''\t\t" '' ?? 'is falsy'
echo "Funcref:\tnull_function\t" null_function ?? 'is falsy'
echo "List: \t[]\t\t" [] ?? 'is falsy'
echo "Dictionary:\t{}\t\t" {} ?? 'is falsy'
echo "Float: \t0.0\t\t" 0.0 ?? 'is falsy'
echo "Boolean:\tfalse\t\t" false ?? 'is falsy'
echo " \tv:false\t\t" v:false ?? 'is falsy'
echo "None: \tv:none\t\t" v:none ?? 'is falsy'
echo " \tv:null\t\t" v:null ?? 'is falsy'
echo "Job: \tnull_job\t" null_job ?? 'is falsy'
echo "Channel:\tnull_channel\t" null_channel ?? 'is falsy'
echo "Blob: \t0z\t\t" 0z ?? 'is falsy'
echo "Object: \tnull_object\t" null_object ?? 'is falsy'
enum Enum
endenum
var null_enumval: Enum # 注: 不存在内置 "null_enumvalue"
echo "EnumValue:\t'null_enumval'\t" null_enumval ?? 'is falsy'
echo "Tuple: \t()\t\t" () ?? 'is falsy'
echo "void: \t\t\t" test_void() ?? 'is falsy'
备注: Vim9 脚本的准假值判定和 Python 很接近 (例如 Python 中 "if []:" 为假,
Vim9 脚本中 "if []" 同样为假)。JavaScript 行为也类似,但空对象 / 空数组在 JS
中很特别地被判定为真,这一点有所差异。
– 类型转换与异常
vim9-!
逻辑非运算符 "!" ( expr-! ) 用于取反,大部分类型使用不会报错 (以下类型例外:
class 报 E1405 、enum 报 E1421 、typealias 报 E1403 ),运算结果永远是布尔值。
下例对所有准假值取反,全部得到 "true":
vim9script
echo [!0, !'', !null_function, ![], !{}, !0.0, !false,
!v:false, !v:none, !v:null, !null_job, !null_channel, !0z,
!null_object, !(), !test_void()]
vim9-!!
同理,使用 "!!" 可将任意值转为布尔值,下例全部求值为 "false":
vim9script
echo [!!0, !!'', !!null_function, !![], !!{}, !!0.0, !!false,
!!v:false, !!v:none, !!v:null, !!null_job, !!null_channel, !!0z,
!!null_object, !!(), !!test_void()]
备注: 这是 Vim9 少数比老式 Vim 脚本宽松的地方。如下例所示,老式脚本对函数引
用、列表、字典、blob、作业、元组类型使用 "!" 或 "!!" 会报错:
" 老式 Vim 脚本: !! 错误示例
let F = {x -> x}
let j = job_start([&shell, &shellcmdflag, 'date'], {})
echo !!F | " E703: 将 Funcref 作整数使用
echo !![] | " E745: 将列表作整数使用
echo !!{} | " E728: 将字典作整数使用
echo !!0z | " E974: 将 Blob 作整数使用
echo !!j | " E910: 将任务作整数使用
echo !!() | " E1520: 将元组作为数字使用
使用 ".." 做字符串拼接时,数值、浮点数、布尔、 Special 类型会自动转为字符串:
vim9script
var mystr = 8 .. ', ' .. 9.2 .. ', ' .. true .. ' 和 ' .. v:none
echo [mystr, mystr->typename()]
备注: 1. true ( v:true 同理) 会转为字符串 "true",而老式 Vim 脚本会
把 v:true 转为 "v:true"。
2. Vim9 脚本中, .. 拼接浮点数行为稳定。老式脚本存在缺陷,小数
点在首个 "." 或 "." 后会被误识别为拼接符 "."。如下老式脚本中
9.2 会丢失小数点:
let s:my8str = 8 .. ', ' .. 9.2 .. ', ' .. v:true .. ' 和 ' .. v:none
echo s:my8str | " 8, 92, v:true 和 v:none
基本类型 ( v:t_number 、 v:t_float 、 v:t_bool ) 以及 Special 类型可以直接用
"==" 比较。其余类型必须使用 string() 。示例:
vim9script
echo '这是列表: ' .. [1, 2, 3]->string()
# 直接拼接会报 E730: 将列表作字符串使用
echo '这会出错! ' .. [1, 2, 3]
同理,使用 "==" 比较非基本类型也需要调用 string() ,否则触发 E1072 :
vim9script
var l: list<any> = [9, '9.2']
echo string(l) == "[9, '9.2']" # true
echo l == "[9, '9.2']" # E1072: 不能比较 list 和 string
警告: 短路求值可能会隐藏布尔表达式中的错误。OR 表达式只要判定为 true 就不再
执行后续操作数。执行提前结束,无效代码不会运行,运行时可能出现的错误,包括
Vim9 脚本的类型相关错误,可能会被掩盖。例如,类本身不允许参与比较,但下例中第
一个三元表达式中 "t" 已经求值为 true ,不会触发报错;第二个表达式则会报错:
vim9script
var t: bool = true
echo t || null_class ? true : false # true
echo null_class || t ? true : false # E1405: 类 "" 不能作为值使用
预定义值
false true null null_blob null_channel
null_class null_dict null_function null_job
null_list null_object null_partial null_string
Vim9 脚本内置预定义常量,用来表示真、假以及各类空状态。下表列出这些预定义值,
附带其 type() 、 typename() 、 string() 的表示形式:
预定义值 type() typename() string()
null_string v:t_string string ''
null_function v:t_func func (...): unknown function ()
null_partial v:t_func func (...): unknown function ('')
null_list v:t_list list<any> []
null_dict v:t_dict dict<any> {}
true v:t_bool bool true
false v:t_bool bool false
null v:t_none special null
null_job v:t_job job no process
null_channel v:t_channel channel channel fail
null_blob v:t_blob blob 0z
null_class v:t_class class<Unknown> class [unknown]
null_object v:t_object object<any> object of [unknown]
null_tuple v:t_tuple tuple<any> ()
预定义值 true 和 v:true 等价; false 和 v:false 等价; null 和
v:null 等价。
"null_<type>" 仅在部分场景下等同于空值。参见 null-details 。
以下类型没有对应的 "null_<type>` 预定义常量:
- Number: 使用 0 代替 (但 0 不等于 null )
- Float: 使用 0.0 代替 (和数值 0 一样,不等于 null )
- Typealias: 不能作为值使用,会报 E1403
- Enum: 不能作为值使用,会报 E1421
- EnumValue: 可赋值为 null_object ,见 vim9-comparators 中的 "Switch" 示例
"null_<type>" 适合用来重置脚本局部变量,因为脚本局部变量不能用 :unlet 删除。
示例:
vim9script
var myvar: string = "not null_string"
try
unlet myvar
catch
echo v:exception # E1081: 无法 unlet myvar
finally
myvar = null_string
echo $"'myvar' 为 {myvar->string()}"
endtry
它们也可用作函数参数缺省值:
vim9script
def CheckMyList(l: list<number> = null_list): string
if l == null
return '未传入列表'
elseif l->empty()
return '传入空列表'
else
return $"已传入列表 {l->string()}"
endif
enddef
echo CheckMyList()
echo CheckMyList([])
echo CheckMyList([8, 9])
备注: 示例中将列表 "l" 与 null 比较,而非 null_list 。这样可以区分
缺省值 null_list 和空列表 []。更多 null 判断细节参见 null-compare 和
null-anomalies 。
null 可以和任意值比较,不会触发类型错误。但 null 与数值、浮点数、布尔值比
较结果永远是 false 。老式 Vim 脚本行为不同: v:null 和 0 或 0.0 比较返回 1。
示例:
vim9script
echo [0 == null, 0.0 == null] # [false, false]
legacy echo [0 == v:null, 0.0 == v:null] | # [1, 1]
vim9-false-true
布尔值转为字符串时,输出 false 和 true 。Vim9 脚本中, v:false 等价于
false , v:true 等价于 true 。
vim9script
echo $'{v:none} 没有 "none" 等价形式,但'
echo $'"v:true" 在字符串中的的字符串形式为 "{v:true}",而'
echo 'Q: "v:false" 和 "false" 是否可以互换?'
echo $'A: {v:false == false}'
备注: v:none 没有对应的 "none" 常量,因为其他语言没有对应的概念。
vim9-string-index
使用 [idx] 对字符串索引,或是 [idx : idx] 切片,都会采用字符索引而非字节索引,
组合字符被视为基本字符的一部分。示例:
vim9script
echo 'très'[2] # è (U+00E8)
legacy echo 'très'[2] | # <c3> (非法字节 0xc3)
负索引从末尾向前计数,"[-1]" 代表最后一个字符。
vim9script
echo 'fenêtre'[-4 : -1] # être
legacy echo 'fenêtre'[-5 : -1] | # être (因为 ê 为两字节,所以为 -5)
内置函数 slice() 在老式 Vim 脚本与 Vim9 脚本中统一使用字符索引,是稳妥选择:
vim9script
echo 'fenêtre'->slice(-4) # être
legacy echo 'fenêtre'->slice(-4) | # être
使用 strcharpart() 可将组合字符拆分开单独计数。示例字符由 "a" (U+0061) 加上
组合长音符号 (U+0304) 构成:
vim9script
echo 'Ngā mihi'->strcharpart(2, 1) # a (不含组合长音符号)
echo 'Ngā mihi'[2] # ā (含组合长音符号)
如果索引越界,返回空串。
vim9script
echo ['In range'[0 : 1]] # ['In']
echo ['Out of range'[19 : ]] # ['']
E1148
尝试通过索引赋值、修改字符串会报错。示例:
vim9script
def F1148(): void
b:s = 'Fails'
b:s[4] = '!' # E1148: 不能索引 string
enddef
F1148()
备注: 根据作用域与变量前缀不同,可能会报其他错误。例如本例变量如果为
:var 声明而非缓冲区变量,会触发 E1141 ,如果在脚本作用域而非 :def
内,则触发 E689 。
老式 Vim 脚本在字符索引中使用 ++var ( :++ ) 或 --var ( :-- ) 会直接忽略自
增 / 自减;Vim9 脚本中,这种写法非法:
vim9script
var int: number = 1
var str: string = 'hear'
legacy echo s:str[++s:int : ] | # ear
echo str[++int : ] # E15: 无效的表达式: "++int : ]
以 0 开头的数值不再视作八进制,只有 "0o" 开头才是八进制,例如 "0o744"。参见
scriptversion-4 。示例:
vim9script
echo 017 # 17 (推断为十进制)
legacy echo 017 | # 15 (推断为八进制)
echo 0o17 # 15 (显式指定八进制)
需要留意的坑
vim9-gotchas
Vim9 脚本设计目标是更贴近现代编程语言,同时尽量兼容老式 Vim Ex 命令,因此做了
一些取舍。下面汇总容易出现非预期行为的场景。
Ex 命令范围通常需要加冒号前缀:
- 老式 Vim 脚本中,行首 "%" 代表全部行 ( :% ) ;Vim9 脚本中 "%" 代表取模运算符
( expr-% ),否则触发 E1050 。示例:
" 老式 Vim 脚本
let g = 97
echo 1000
% g / 9.7
" 1000 (随后在消息窗口输出所有包含 '9.7' 的行)
而 Vim9 脚本:
vim9script
var g = 97
echo 1000
% g / 9.7
# 3.092784 (等价于 1000 % 97 / 9.7)
echo 1000
:%g/9.7
# 1000 (随后在消息窗口输出所有包含 '9.7' 的行)
% g / 9.7
# E1050: 范围前需要有冒号: % g / 9.7
- 老式 Vim 脚本中,开头单引号代表跳转到位置标记 ( ' ) ;Vim9 脚本中,单引号为
字符串起始符,否则触发 E115 :
" 老式 Vim 脚本
:normal! mt
"在注释行 " 老式 Vim 脚本 上设置位置标记 t
't
" 跳转到位置标记 t (加载脚本后,位于注释行)
而 Vim9 脚本:
vim9script
:normal! mt
# 在 vim9script 命令这一行设置位置标记 t
't'->popup_notification({time: 4000}) # Vim9 链式方法调用
:'t
# 跳转到位置标记 t (加载脚本后,位于 vim9script 行)
# 单独写 't 会报 E115: 缺少单引号
- 老式 Vim 脚本里,如果脚本是通过加载执行的,"->" 会将范围之前的上一行向右缩进
'shiftwidth' 个空格;Vim9 脚本中 "->" 是方法链的续行:
vim9script
const mine: string = 'MINE'
->toupper()
echo mine # MINE
# 在可修改缓冲区中,单独一行写 :->,当本脚本被加载时,会把 vim9script
# 的上一行向右缩进 'shiftwidth' 个空格。
Vim9 脚本中部分 Ex 命令容易和赋值语法混淆:
" 老式 Vim 脚本
let let = 8
echo let | " 8
:g:let = 8
" 输出匹配 'let = 8' 的行到消息
而 Vim9 脚本:
vim9script
g:let = 9
echo g:let # 9
:g:let = 9 # E1241: 不支持的分隔符: :let = 9
为了区分 :global 或 :substitute 命令与表达式或赋值语句,当命令简写为单个字
符时,不能使用 '-'、'.'、':' 作为定界符。以 :g 为例:
vim9cmd g-pattern-cmd # E1241: 不支持的分隔符: -pattern-cm…
vim9cmd g:pattern:cmd # E1069: ':' 后要求有空白: :cmd…
vim9cmd g.pattern.cmd # E121: 未定义的变量: g
同时命令和定界符之间不能有空格,这点和老式 Vim 脚本不同。示例:
g /[-:.]pattern/p | " 可以正常执行,显示上面三条 vim9cmd 相关行
而 Vim9 脚本:
vim9cmd g /[-:.]pattern/p # E1242: 分隔符前不允许有空格
:def 定义的函数会整体编译,无论代码路径是否会执行,编译阶段就会捕获全部语法
与类型错误。老式 :function 没有静态类型检查,类型问题只会在代码实际运行时暴
露。下面老式 Vim 脚本的 :funciton 会返回 "yes" (除非是极罕见的情况,在 z/OS
UNIX 环境加载):
vim9script
func Maybe()
if !has('ebcdic')
return 'yes'
endif
return [] + 'yes'
endfunc
echo Maybe() # yes
等价 Vim9 脚本会报 E1051 :
vim9script
def Maybe(): any
if !has('ebcdic')
return 'yes'
endif
return [] + 'yes' # E1051: + 的参数类型不正确
enddef
echo Maybe()
作为一种规避方案,可以将不支持的代码放在求值结果恒为假的常量表达式条件分支内,
编译器会跳过这部分代码的编译。示例:
vim9script
def Maybe(): any
if has('ebcdic')
return [] + 'yes'
endif
return 'yes'
enddef
echo Maybe() # yes
另一种办法是拆分成两个函数:
vim9script
if has('ebcdic')
# 非 EBCDIC 系统下完全不会编译此函数
def MaybeInner(): any
return [] + 'yes'
enddef
endif
func Maybe()
if has('ebcdic')
return MaybeInner()
else
return 'yes'
endif
endfunc
echo Maybe() # yes
还有一种方案 (另一使用场景) 是使用 exists_compiled() 。内置函数 luaeval()
仅在可用时才会被引用:
vim9script
def Version(): string
if !exists_compiled('*luaeval')
return $"Version 使用 v:version: {v:version}"
else
return $"Version 通过 Lua: {luaeval('900 + 2')}"
endif
enddef
echo Version()
:def 函数或 Vim9 匿名函数中, :return 之后存在不可达代码会报 E1095 ,老式
函数则直接忽略不可达代码:
vim9script
function Legacy()
return v:true
let X = "虽然不可达,但会被忽略。"
endfunction
echo Legacy() # true
var Vim9: func = (): bool => {
return true
const X = false # E1095: :return 语句之后存在无法执行的代码
}
vim9-user-command
函数编译带来的另一个副作用是用户命令在编译阶段检查是否存在。如果命令定义在函数
之后,会报错。下面这段可正常运行:
vim9script
command -nargs=1 MyCommand echomsg <q-args>
def Works()
MyCommand '可用'
enddef
Works()
下面这段会触发 E476 : 编译时找不到 MyCommandFails:
vim9script
def Fails()
command -nargs=1 MyCommandFails echomsg <q-args>
MyCommandFails '不可用'
enddef
Fails()
规避方法是用 :execute 间接调用命令:
vim9script
def Works_Using_Execute()
command -nargs=1 MyCommandUsingExecute echomsg <q-args>
execute "MyCommandUsingExecute '可用'"
enddef
Works_Using_Execute()
条件语句里出现未知命令不会跳过检查。条件为假时报 E171 ;条件为真时报 E476 。
示例:
vim9script
def F171()
if has('ebcdic') | Nah | endif
enddef
F171() # E171: 缺少 :endif
vim9script
def F476()
if !has('ebcdic') | Nah | endif
enddef
F476() # E476: 无效的命令: Nah | endif
其他差异
- 除非模式内显式覆盖,正则模式缺省使用 'magic' 规则。也就是说,即使开启全局选
项 'nomagic',会被忽略。
- 忽略 'edcompatible' 选项值。
- 忽略 'gdefault' 选项值。
:++ :--
- 新增 ++ 和 -- 命令,分别执行自增、自减操作,支持数值和浮点类型。示例:
vim9script
var flo: float = 41.0
++flo
echo flo # 42.0
- 表达式内部尚不支持 ++var 或 --var 写法,如果使用,会报 E15 。示例:
vim9script
var num: number = 43
# The following line gives E15: 无效的表达式: "--num"
echo --num
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}
部分 (也就是,只有一个冒号非法)。": " 的前面也不能有空
格。示例:
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 脚本的 :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
以下维基页面可能有所帮助。它由 Vim9 脚本的一位早期试用者编写:
https://github.com/lacygoill/wiki/blob/master/vim/vim9.md
vim9-s:var
引用脚本局部变量时,由上下文决定 "s:" 前缀为必需、可选、或触发 E1268 。上下文
判断要素是,脚本版本是 Vim9 脚本还是老式 Vim 脚本,以及引用位置是脚本顶层、
:def 函数内部,还是 :function 函数内部。共三条规则:
1. 在 :function 内,"s:" 为必需,不受脚本类型或函数外层上下文 (例如嵌套在其
他函数中) 影响。同理,在老式 Vim 脚本的脚本顶层作用域内,"s:" 也为必需。
2. 在 Vim9 脚本中, :def 函数内部使用 "s:" 都会报 E1268 ,不受函数外层上下文
影响。Vim9 脚本顶层作用域内使用 "s:" 同样触发 E1268。
3. 在老式 Vim 脚本的 :def 函数内,"s:" 可选,不受函数外层上下文影响。 :def
函数不能新建脚本局部变量,已编译函数内如果为尚不存在的脚本局部变量赋值,会
报 E1089 。
下面三段脚本演示这三条规则:
" 1. 在 :function 内和老式脚本的脚本顶层作用域内,"s:" 为必需。
let s:MyVar = v:true
echo s:MyVar | " v:true
" echo MyVar (会报 E121: 未定义的变量: MyVar)
vim9cmd echo MyVar # true
function! MyFunc()
echo s:MyVar | " v:true
" echo MyVar (会报 E121: 未定义的变量: MyVar)
vim9cmd echo MyVar # true
endfunction
call MyFunc()
vim9script
# 2. Vim9 脚本中,在任意 :def 函数内或脚本顶层使用 "s:" 都会报 E1268
var MyVar: bool = true
echo MyVar # true
# echo s:MyVar (会报 E1268: 在 Vim9 脚本中不能使用 s:)
legacy echo s:MyVar | # v:true
def MyFunc()
echo MyVar # true
# echo s:MyVar (会报 E1268: 在 Vim9 脚本中不能使用 s:)
legacy echo s:MyVar | # v:true
enddef
MyFunc()
>vim
" 3. 老式 Vim 脚本的 :def 函数内,"s:" 可写可不写
let s:MyVar = v:true
function! Outer()
def! MyFunc()
echo MyVar # true
echo s:MyVar # true
enddef
call MyFunc()
endfunction
call Outer()
<
运行时求值的 exists() 不能用来有条件地跳过未声明变量;而在编译时求值的
exists_compiled() 可以。示例:
vim9script
def MyDef()
if exists_compiled('MyVar') # 编译时求值
echo $"MyVar = {MyVar}" # MyVar = 1
endif
if exists_compiled('MyVar2') # 编译时求值
echo $"MyVar2 = {MyVar2}" # 不可达
else
echo "MyVar2 在编译时不存在"
endif
if exists('MyVar') # 运行时求值
echo $"MyVar = {MyVar}" # MyVar = 1
endif
if exists('MyVar2') # 运行时求值
# 下行会报 E1001: 找不到变量: MyVar2
# echo MyVar2
else
echo "MyVar2 在运行时不存在"
endif
enddef
var MyVar: number = 1 # 在 MyDef() 编译前完成声明
MyDef()
var MyVar2: number = 2 # 在 MyDef() 编译后完成声明
< 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): string => 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'
vim9-closure
循环里定义的闭包,取决于变量声明所在位置,可以共享该变量的引用,也可为每个闭包
使用变量的不同备份。具体来说,变量在循环之外声明时,所有的闭包会引用相同的共享
变量。下例展示效果,"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, _): number => 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, _): number => 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, _): number => 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 脚本版本:
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:]][[:upper:]]'
&& getline(v:lnum + 1)->empty() ? '>1' : '1'
enddef
set foldexpr=MyFoldFunc()
set foldmethod=expr
normal! 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_1008: list
vim9cmd var l_1009: list<number
vim9cmd var l_1010: list<invalidtype>
vim9cmd var l_1012: 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
类型元素的元组
示例:
vim9script
var t1: tuple<number> = (20,)
var t2: tuple<number, string> = (30, 'vim')
var t3: tuple<number, float, bool> = (40, 1.1, true)
var t4: tuple<...list<string>> = ('a', 'b', 'c')
var t5: tuple<number, ...list<string>> = (3, 'a', 'b', 'c', 'd')
variadic-tuple E1539
可变元组有零或多个相同类型的元素,其类型声明必须以列表类型结尾,否则报 E1539。
示例:
vim9script
var t6: tuple<...list<number>> = (1, 2, 3)
var t7: tuple<...list<string>> = ('a', 'b', 'c')
var t8: tuple<...list<bool>> = ()
var tE: tuple<...any> = () # E1539: 可变元组必须以列表类型结尾:any
vim9-func-declaration
vim9-partial-declaration
vim9-func-type
函数 (包括偏函) 可用以下多种形式声明:
func
func: void
func: {type}
func()[: void]
func(): {type}
func({type})[: void]
func({type}): {type}
func(?{type})[: void]
func(?{type}): {type}
func(...list<{type}>)[: void]
func(...list<{type}>): {type}
func({type}, ...list<{type}>)[: void]
func({type}, ...list<{type}>): {type}
返回类型为 "void" 时,函数没有返回值。下面列出各类写法的说明与示例,所有示例都
会输出 '9'。
func
- 任意类型函数引用
- 返回值类型不限,也可无返回值
vim9script
const I = (n) => n + 1
const F: func = I->function()
echo F(8)
func: void
- 参数的数量与类型不限
- 无返回值
vim9script
def I(n: number): void
echo n + 1
enddef
const F: func: void = I->function()
F(8)
func: {type}
- 参数的数量与类型不限
- 返回指定类型的值
vim9script
const I = (n): number => n + 1
const F: func: number = I->function()
echo F(8)
func()[: void]
- 无参数
- 无返回值
vim9script
var n: number = 8
def I(): void
echo n + 1
enddef
const F: func(): void = I->function()
F()
func(): {type}
- 无参数
- 返回指定类型的值
vim9script
var n: number = 8
def I(): void
echo n + 1
enddef
const F: func(): void = I->function()
F()
func({type})[: void]
- 带类型标注参数
- 无返回值
vim9script
def I(n: number): void
echo n + 1
enddef
const F: func(number): void = I->function()
F(8)
func({type}): {type}
- 带类型标注参数
- 返回指定类型的值
vim9script
const I = (n: number): number => n + 1
const F: func(number): number = I->function()
echo F(8)
func(?{type})[: void]
- 可选类型参数
- 无返回值
vim9script
def I(n: number = 8): void
echo n + 1
enddef
const F: func(?number): void = I->function()
F()
func(?{type}): {type}
- 可选类型参数
- 返回指定类型的值
vim9script
def I(n: number = 8): number
return n + 1
enddef
const F: func(?number): number = I->function()
echo F()
func(...list<{type}>)[: void]
- 接收可变数量参数的类型化列表
- 无返回值
vim9script
def L(...l: list<number>): void
echo l->reduce((x, y) => x + y)
enddef
const F: func(...list<number>): void = L->function()
F(8, 0, 0, 1)
func(...list<{type}>): {type}
- 接收可变数量参数的类型化列表
- 返回指定类型的值
vim9script
def L(...l: list<number>): number
return l->reduce((x, y) => x + y)
enddef
const F: func(...list<number>): number = L->function()
echo F(8, 0, 0, 1)
func({type}, ...list<{type}>)[: void]
- 必选类型参数
- 接收可变数量参数的类型化列表
- 无返回值
vim9script
def D(a: float, ...l: list<number>): void
echo (a + l->reduce((x, y) => x + y))->float2nr()
enddef
const F: func(float, ...list<number>): void = D->function()
F(8.0, 0, 0, 1)
func({type}, ...list<{type}>): {type}
- 必选类型参数
- 接收可变数量参数的类型化列表
- 返回指定类型的值
vim9script
def D(a: float, ...l: list<number>): number
return (a + l->reduce((x, y) => x + y))->float2nr()
enddef
const F: func(float, ...list<number>): number = D->function()
echo F(8.0, 0, 0, 1)
函数引用也可为偏函 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)} (π 精确到两位小数)"
E1005
函数引用的参数类型最多只能使用 19 个 (比 :def 或匿名函数允许的上限少一个,参
见 E740 )。参数类型达到 20 个或以上的会触发 E1005,示例如下:
vim9script
var X: func
X = (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) => 0
var F: func(any, any, any, any, any, any, any, any, any, any, any,
\ any, any, any, any, any, any, any, any, any): any = X->function()
# E1005: 参数类型过多
E1007
函数引用中,必选参数不能放在可选参数之后:
vim9script
def I(n: number, f: float): void
echo n + f
enddef
const F: func(?number, float) = I->function()
# E1007: 必须参数位于可选参数之后
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 )。保存枚举值的变量在运行时类型为 enumvalue
( 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,枚举值 (EnumValue) 类型值,而 Quad 返回 15,枚举 (Enum) 类型值)。
变量类型和类型转换
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"
# E1012: 类型不匹配;预期 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 # E1012: 类型不匹配;预期 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 E1395
类型不完整 (如对象所属类未知) 会报错 E1363 或 E1395。示例:
vim9script
var E1363 = null_class.member # E1363: 不完整的类型
vim9script
def F1395(): void
echo null_class.member
enddef
F1395() # E1395: 使用了一个空类
另一个和 null 对象相关的错误是 E1360 :
vim9script
var obj = null_object
var E1360 = obj.MyMethod() # E1360: 使用空对象
类型推断
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: 类型不匹配,
# 预期 list<bool> 但得到 list<number>
要使列表接受多种类型,可显式使用 <any>,也可在初始化时使用空列表 (或两者兼有,
如 `list<any> = []`)。示例:
vim9script
final la: list<any> = []
echo la->extend(['二', 1])
final le = []
echo le->extend(la)
同理,要做字典接受多种类型:
vim9script
final da: dict<any> = {}
echo da->extend({2: 2, 1: '一'})
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":
echo 123 == '123'
但如果不小心多了一个空格,就会回显 "0":
echo 123 == ' 123'
E1206
Vim9 脚本中类型检查更加严格。只要所用值匹配期待类型,绝大多数地方照常工作。例
如,无论在老式 Vim 脚本和 Vim9 脚本中,期待字典但试图用其他类型都会报错:
echo [8, 9]->keys()
vim9cmd echo [8, 9]->keys() # E1206: 参数 1 需要字典
E1023 E1024 E1029
E1030 E1174 E1175
E1210 E1212
不过在有些情况下,Vim9 脚本会报错,而老式脚本里不会,这会导致后向不兼容。下例
展示了各种不兼容的情况。先给出老式 Vim 脚本中不报错时的行为。然后再展示相同的
命令在 Vim9 脚本里触发的错误。
- 期待布尔值但使用了数值 (0 或 1 除外):
echo v:version ? v:true : v:false
vim9cmd echo v:version ? true : false # E1023: 将整数作布尔值使用
- 期待字符串时使用了数值:
echo filter([1, 2], 0)
vim9cmd echo filter([1, 2], 0) # E1024: 将整数作字符串使用
- 将数值赋给环境变量、寄存器或字符串类型的 v: 变量
let $FOO = 123
vim9cmd $FOO = 123 # E1012: 类型不匹配;预期 string 但得到 number
- 期待数值时不使用数值:
" 此例中,老式 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
- 期待数值时使用字符串:
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 需要非空字符串
- 参数期待数值时不使用数值:
echo gettabinfo('a')
vim9cmd echo gettabinfo('a') # E1210: 参数 1 需要整数
- 参数期待布尔值时不使用布尔值:
echo char2nr('¡', 2)
vim9cmd echo char2nr('¡', 2) # E1212: 参数 2 需要布尔值
- 赋值期待数值时不使用数值 ( E521 ):
let &laststatus='2'
vim9cmd &laststatus = '2'
- 赋值需要字符串时不使用字符串 ( E928 ):
let &langmenu = 42
vim9cmd &langmenu = 42 # E928: 需要字符串
- 使用 'is' 比较 Special 特殊类型时,常会失败 ( E1037 , E1072 ):
" 回显 1,因为两次比较在老式 Vim 脚本中均返回 true
echo v:null is v:null && v:none is v:none
" 类似地,回显 0,因为所有比较在老式 Vim 脚本中均返回 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 v:null is 8 # false
echo true is v:null # false
- 期待布尔值时使用字符串 ( E1135 ):
echo '42' ? v:true : v:false
vim9cmd echo '42' ? true : false # E1135: 将字符串作布尔值使用: "42"
- 期待数值时使用布尔值 ( E1138 ):
let &laststatus=v:true
vim9cmd &laststatus = true
一个后果是,项目类型已显式声明或由推断得到时,传递给 map() 的列表或字典不能
更改其项目类型。下例中,老式 Vim 脚本里不会报错:
" 老式 Vim 脚本会将 [0, 1] 列表修改为 ['item 0', 'item 1']
let s:mylist = [0, 1]
call map(s:mylist, {i -> $"item {i}"})
echo s:mylist
而在 Vim9 脚本中会报 E1012 :
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() # E1013 (现为编译时报错)
enddef
FuncSNfail()
函数引用未指定参数时,不进行参数类型检查。下例中 FlexArgs 第一次调用时接受字符
串参数,第二次调用时接受列表参数:
vim9script
var FlexArgs: func: string
FlexArgs = (s: string): string => $"倒计时时间 {s}..."
echo FlexArgs("所有人")
FlexArgs = (...values: list<string>): string => join(values, ', ')
echo FlexArgs('3', '2', '1', '出发!')
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: 参数 1 需要字符串、列表 ...
vim9 9->add(9) # E1226: 参数 1 需要列表或 blob
vim9 9->remove(9) # E1228: 参数 1 需要列表、字典或 ...
vim9 getcharstr('9') # E1235: 参数 1 需要布尔值或整数
vim9 9->blob2list() # E1238: 参数 1 需要 blob
vim9 9->filter(9) # E1251: 参数 1 需要列表、元组、B...
vim9 9->reverse() # E1253: 参数 1 需要字符串、列表 ...
vim9 9->call(9) # E1256: 参数 1 需要字符串或函数
vim9 null_dict->winrestview() # E1297: 参数 1 需要非空字典
vim9 {}->prop_add_list(null_list) # E1298: 参数 2 需要非空列表
vim9 {}->repeat(9) # E1301: 参数 1 需要字符串、数字 ...
vim9 9->index(9) # E1528: 参数 1 需要列表、元组或 ...
vim9 9->join() # E1529: 参数 1 需要列表或元组
vim9 9->max() # E1530: 参数 1 需要列表、元组或字典
vim9 9->get(9) # E1531: get() 的参数必须是列表、...
vim9 9->tuple2list() # E1534: 参数 1 需要元组
保留作将来使用: 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 (即 `== 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]
备注: (1) empty() 说明了作业、通道和对象类型何时为空。(2) class 、
enum 和 typealias 类型不能作为值使用,因此不能与 null 比较 (否则
分别会报 E1405 、 E1421 和 E1403 )。
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 的
复杂性。
容器变量和专用变量使用不同的初始化语法。对于容器类型:
- 未初始化的容器变量缺省为空 ( empty() 判定为真),但非 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 容器与具有特定类型的 空 容器之
间的混合/桥梁形式,兼具两者的特性。下一节将详细介绍比较结果。
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> 和 空 容器三者之间并不满足传递
关系。下例简单回顾一下:
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
5. 泛型函数 generic-functions
泛型函数允许使用相同的函数定义但带不同的类型参数,同时保留对参数和返回值的类型
检查。可以兼顾类型安全性与代码重用性。
声明
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 示例 #1: {Zip<number, string>(n, s)}"
echo $"Zip 示例 #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: 类型变量名必须以大写字母开头:n...
E1558 E1560
函数必须声明并使用为泛型函数或常规函数 - 二者不可混用。以下 Vim9 脚本显示这些
错误:
vim9script
My1558<number>()
# E1558: 未知的泛型函数:My1558
vim9script
def My1560(): void
enddef
My1560<string>()
# E1560: 不是泛型函数:My1560
E1561
类型参数名不能和其他标识符冲突:
vim9script
def My1561<D, E, D>(): D
enddef
# E1561: 重复的类型变量名:D
vim9script
enum E
Yes, No
endenum
def My1041<E>(): E
enddef
# E1041: 重定义脚本项目:"E"
调用泛型函数
generic-function-call
调用泛型函数时,在函数名和参数列表之间的 "<" 和 ">" 内指定具体类型 (实参):
MyFunc<number, string, list<number>>()
注意: 本节有多个可直接加载执行的示例,如 generic-function-example 。
E1555 E1556 E1557 E1559
传递给函数的类型实参必须和声明中类型参数的数量匹配。不允许空类型列表。
示例:
vim9script
def My1555<>(): void
enddef # E1555: 为泛型函数 … 指定了空类型列表
vim9script
def My1556<T>(): void
enddef
My1556<bool, bool>() # E1556: 为泛型函数 … 指定了太多类型
vim9script
def My1557<T, U>(): void
enddef
My1557<bool>() # E1557: 为泛型函数 … 指定的类型不足
vim9script
def My1559<T>(): T
enddef
My1559() # E1559: 泛型函数 … 缺少类型参数
泛型函数的具体类型可使用任意 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" 仅存在于该脚本作用域。相比之下,在老式 Vim 脚本中,"let myvar" 会
创建全局变量 g:myvar ,在其他脚本与函数中均可访问。
E1101
文件级别变量相当于老式脚本里的 "s:" 局部变量,但省略 "s:" 前缀。而且这类变量不
能被删除。
E1304
Vim9 脚本中仍然可用全局 ( g: )、缓冲区 ( b: )、窗口 ( w: ) 和标签页 ( t: ) 命
名空间。用法和老式 Vim 脚本一致。Vim9 脚本中,带命名空间前缀的变量:
- 无需声明
- 无固定类型,且
- 可被删除。
如果声明使用带命名空间前缀的变量,触发 E1016 。如果指定类型,则触发 E1304 :
vim9cmd var b:fails: bool # E1016: 不能声明 缓冲区 变量:b:fails
vim9cmd b:fails: bool = false # E1304: 不能对此变量使用类型:b:fails
vim9script 的一个附带效果是将 'cpoptions' 选项设为 Vim 缺省值,等价执行:
:set cpo&vim
其中一个效果之是启用 line-continuation 自动续行。
脚本执行结束后,会恢复原先的 'cpoptions' 值,脚本内对该选项的标志位增删会合并
回原值,确保相同效果得到保留,但标志位顺序可能发生变化。在启动加载的 vimrc
文件中,不会执行此重置逻辑。
vim9-mix
有一种方法能在单个脚本文件中同时兼容老式 Vim 语法与 Vim9 语法:
" 这里写_老式 Vim 脚本_注释
if !has('vim9script')
" _老式 Vim 脚本_ 注释和命令写在这里
finish
endif
vim9script
# 从此行开始,全部为_Vim9 脚本_命令
echowindow $"has('vim9script') == {has('vim9script')}"
这样写的脚本,在支持 Vim9 的环境启用 Vim9 语法;在没有 Vim9 的 Vim 版本中,
vim9script 命令不会执行,避免报错。
注意: Vim 正式版 9 发布前 Vim9 语法有改动。使用新版语法 (例如用 "import from"
而非 "import") 的脚本在旧版本会报错。更稳妥的版本判断方式是使用 v:version >=
900 (因为 "has('vim9script')" 在 Vim 8.2 补丁 3965 中就返回 v:true )。有时需
要限定更高补丁版本。例如要使用元组功能 (Vim 9.1 补丁 1232 引入),判断条件应写
成:
if !has('patch-9.1.1232')
echowindow $"Fail: Vim 没有补丁 9.1.1232"
finish
endif
vim9script
echowindow $"Pass: 版本号 {v:versionlong}。继续中 ..."
无论使用哪种 vim-mix 判断逻辑,仅存在两种生效路径:
1. "if" 语句计算为假,跳过直到 endif 部分为止的代码, vim9script 成为实际
执行的首条命令。
2. "if" 语句计算为真,执行直到 endif 部分为止的代码,而 finish 在到达
vim9script 前终止脚本。
导出
:export
可以这样导出项目:
export var myvar ...
export const MYCONST ...
export final myvar ...
export def MyDef() ...
export function MyFunc() ...
export class MyClass ...
export abstract class MyAbstractClass ...
export interface MyInterface ...
export enum MyEnum ...
export type MyType ...
E1043
如上提示,变量、常量、函数、类 (包含抽象类)、接口、枚举、类型都可以导出。尝试
导出其他内容会触发 E1043:
vim9cmd export echo 0 # E1043: :export 后无效的命令
E1042
export 仅能在 Vim9 脚本顶层作用域使用。例如,导出 :function 是合法的,但在
此类函数的内部执行导出会触发 E1042:
vim9script
export function F1042()
export var b: bool
endfunction
F1042() # E1042: 导出只能在 vim9script 中使用
E1044
导出命令非法参数会触发 E1044:
vim9script
export def /a1044(): void
enddef # E1044: 导出无效的参数: …
导入
:import
导出的成员可以在其他脚本中导入。导入语法有两种形式。简单形式是:
import {filename}
{filename} 是表达式,求值结果必须为文件名字符串。此形式中的文件名应以 ".vim"
结尾,".vim" 之前的部分将作为导入命名空间的脚本局部名。示例:
import "myscript.vim"
这样 "myscript.vim" 中所有导出成员就可以通过 "myscript.item" 访问。
E1094
仅允许在脚本顶层作用域执行导入。在其他位置 (例如 :function 或 :def 函数内
部) 导入会触发 E1094:
vim9script
def F1094(): void
import 'nah.vim' # E1094: 只能在脚本中导入
enddef
F1094()
E1053 E1071
如果文件不存在或者文件名非法,报错:
vim9cmd import 'e1053.vim' # E1053: 无法导入 "e1053.vim"
vim9cmd import false # E1071: 对 :import 无效的字符串: false
:import-as
当文件名很长、存在歧义 (例如在不同目录下的同名文件)、或希望自定义命名结构时,
可以使用第二种形式:
import {longfilename} as {name}
该种长形式中,{name} 是导入命名空间的自定义脚本局部名。{name} 只能由字母、数位
和下划线构成且不能以数位开头,规则同 internal-variables 。{longfilename} 为表
达式,求值结果必须指向一个存在的文件。
下面脚本演示 :import-as 用法:
- 定义常量 "TMP",保存待导入 Vim9 临时脚本 {longfilename}
- 向 TMP 写入两行 Vim9 脚本
- 通过 "as" 将导入脚本命名为 "that"
- 回显导入脚本路径 {longfilename},并在另一行上回显导出变量 "that.myvar" 的值
vim9script
const TMP: string = tempname()
['vim9script', 'export var myvar: string = "YES!"']->writefile(TMP)
import TMP as that
echo $"'that' ({TMP}) 中的导出变量 'myvar' 为:\n" .. that.myvar
示例展示 "that.myvar" 这类写法。"that" 名可自行选取,用于指代导入脚本。建议避
开命令名、命令修饰符和内置函数名,避免它们被 {name} 遮蔽;尽量以小写字母开头,
防止遮蔽全局用户命令与函数。{name} 在当前脚本内不能另作他用,如函数或变量名
(否则触发 E1213 ),也不能重新赋值 (否则触发 E1258 )。
如果不想每次都写 "." 访问,可为函数创建局部引用:
var LongDef = that.LongDefName
常量同理:
const MAXLEN = that.MAXIMUM_LENGTH_OF_NAME
这种方式不适用于变量,因为值会被复制一次,后续修改副本变量时,原变量不会同步更
新。因此,变量必须使用带 . 的完整名访问。
导入成员设计为脚本顶层成员,只会被导入一次。不能在 :function 或 :def 内部
调用 :import ,参见 E1094 。
:import 后的脚本文件名有三种类型:
- 相对路径,以 "." 或 ".." 开头。查找会相对于当前脚本文件位置,适合将大型插件
拆分为多个文件。
- 绝对路径,Unix 上以 "/" 开头,Windows 上以 "D:/" 这类格式开头,很少使用。
- 非相对、非绝对路径: 在 'runtimepath' 项目下的 "import" 子目录中查找。一般使
用较长且唯一的名字,避免加载错误文件。
注意不会查找 "after/import" 目录。
Vim9 脚本一旦被导入,会被缓存,不会重复读取。
导入操作会触发多种语法及其他错误,示例如下:
E1047
- "as" 后的 {name} 非法:
vim9script
import $'{$VIMRUNTIME}/autoload/ccomplete.vim' as i-cc
# E1047: 导入中语法错误: i-cc
E1048
- 访问导入脚本中不存在的成员:
vim9script
import $'{$VIMRUNTIME}/autoload/ccomplete.vim' as i_cc
echo i_cc.absent_item # E1048: 脚本中找不到项目: absent_item
E1049
- 访问导入脚本中已声明、但未导出的成员:
vim9script
import $'{$VIMRUNTIME}/autoload/ccomplete.vim' as i_cc
echo i_cc.prepended # E1049: 脚本中未导出项目: prepended
E1060
使用导入命名空间时, . 与成员名必须连续, . 前后都不能有空格或换行:
- . 之后不能有空格 (参见 E1074 )
- . 之前也不能有空格,示例:
vim9script
import $'{$VIMRUNTIME}/autoload/ccomplete.vim' as i_cc
echo i_cc .GetPath() # E1060: 在名字后面预期点号: i_cc .Get...
E1088
- 导入脚本尝试导入自身:
vim9script
const TMP: string = $'{tempname()}.vim'->substitute('\\', '/', 'g')
var lines: list<string> = ['vim9script', $'import "{TMP}" as e1088']
lines->writefile(TMP)
import TMP # E1088: 脚本不能导入自身
E1236
- 直接调用导入名本身:
vim9script
import $'{$VIMRUNTIME}/autoload/ccomplete.vim' as cc
cc() # E1236: 不能使用 cc 本身,它是导入的
E1257
- 导入文件后缀不是 ".vim",且未使用 "as {name}":
vim9script
const TMP: string = tempname()
['vim9script']->writefile(TMP)
import TMP # E1257: 导入的脚本必须使用 "as" 或以 .vim结尾: …
E1258
- 导入名后没有 '.':
vim9script
import $'{$VIMRUNTIME}/autoload/ccomplete.vim' as i_cc
def F1258(): void
i_cc =
enddef
defcompile # E1258: 导入名后没有 '.': i_cc =
E1259
- 导入名后缺失成员名:
vim9script
import $'{$VIMRUNTIME}/autoload/ccomplete.vim' as i_cc
def F1259(): void
i_cc.8 = 0
enddef
defcompile # E1259: 导入名后缺少名称: i_cc.8 = 0
E1260
- 尝试对导入成员执行 :unlet :
vim9script
const TMP: string = tempname()
['vim9script', 'export var b: bool']->writefile(TMP)
import TMP as e_unlet
unlet e_unlet.b # E1260: 无法 unlet 导入项: b
E1261
- 导入文件名为 ".vim",且未使用 "as"。脚本会在系统临时目录生成名为 ".vim" 的临
时文件,然后尝试导入:
vim9script
const TMPDIR: string = tempname()
->substitute('\\', '/', 'g') # 将 Windows 反斜杠替换为 /
->substitute('/[^/]\+$', '/', '') # 移除文件名
['vim9script']->writefile($'{TMPDIR}.vim')
echo $"临时导入文件名为: {TMPDIR}.vim"
import $'{TMPDIR}.vim' # E1261: 不使用 "as" 无法导入 .vim
E1262
- 多次导入同一个脚本 (包括使用不同 "as" 别名) :
vim9script
import $'{$VIMRUNTIME}/autoload/ccomplete.vim' as i_cc1
import $'{$VIMRUNTIME}/autoload/ccomplete.vim' as i_cc2
# E1262: 不能导入相同的脚本两次: .../ccomplete.vim
import-map
Vim9 脚本从另一个脚本导入函数时,可用 <SID> 前缀在映射里引用导入函数:
noremap <silent> ,a :call <SID>name.Function()<CR>
映射定义时,"<SID>name." 会被替换为 <SNR> 加上导入脚本的脚本 ID。
更简单的方案是用 <ScriptCmd> :
noremap ,a <ScriptCmd>name.Function()<CR>
注意 这只适用于函数,不适用于变量。
以下完整示例演示 <ScriptCmd> 的用法,创建并写入临时脚本,导入该脚本,再将导出
函数 "exported.MyDef()" 映射到 <leader>a:
vim9script
const TMP: string = tempname()
['vim9script',
'export def MyDef(): void',
' popup_notification("调用了 exported.MyDef()", {time: 4000})',
'enddef']->writefile(TMP)
import TMP as exported
nnoremap <leader>a <ScriptCmd>exported.MyDef()<CR>
# 按下 <leader>a 就会调用 exported.MyDef(),弹出通知
import-legacy legacy-import :imp
import (含简写形式 :imp ) 同样可在老式 Vim 脚本内使用。即便不写 "s:" 前缀,
导入的命名空间依旧属于脚本局部。示例:
import "myfile.vim"
call s:myfile.MyFunc()
使用 "as name" 形式时,不能单独解析命名空间 (否则会报 E1060 )。下例演示了如何
正常调用 :import 搭配 as ,以及后续报错:
let s:tmp = $'{tempname()}.vim'->substitute('\\', '/', 'g')
call writefile(['vim9script',
\ 'export function L()',
\ ' return "Vim9 脚本导出的 :function 函数"',
\ 'endfunction'], s:tmp)
imp s:tmp as iLegacy
echo s:iLegacy.L() |" 回显 'Vim9 脚本导出的 :function 函数'
echo s:iLegacy |" E1060: 在名字后面预期点号: s:iLegacy
上述限制同样影响老式映射上下文里 <SID> 的使用。 <SID> 仅可作为函数前缀,_不
能 _用于命名空间本身,因此无法用它限定脚本局部命名空间内的函数。此时应改用
<ScriptCmd> ,而不是在函数名前加上 <SID> 。
示例:
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 独有的、反直觉的语法。
同时清理冗余语法,大多是为兼容古老 Vi 命令而遗留的设计。
示例:
- 调用函数不再需要 :call ,表达式求值也不再需要 :eval 。
- 续行不再需要前导反斜杠,可以自动判断表达式何处终止。
但这也带来部分改动:
- 注释改用 # 而非 " 开头,避免和字符串混淆,该写法也被众多主流语言采用。
- Ex 命令范围需要加冒号前缀,防止和表达式产生歧义 (例如,单引号既可以是字符串
也可以是位置标记,"/" 既可以做除法也可以是搜索命令等)
目标是尽量减少差异。一个良好判断标准是,如果不小心写了旧语法,大概率直接报错。
借鉴主流语言的语法和语义
脚本编写者常抱怨 Vim 脚本语法和习惯使用的语言差异过大。为减少这类问题,参考了
主流语言作为语法修订范例。同时,也不会舍弃原有 Vim 脚本中为人熟知的部分。
Vim9 大量参考 TypeScript。它是一门新兴且流行的语言,和 Vim 脚本有不少相似点。
同时兼具静态类型 (变量类型确定) 与动态类型 (变量类型运行时可变)。老式 Vim 脚
本是动态类型,大量已有功能 (尤其是内置函数) 依赖动态类型;而静态类型可以大幅提
升执行速度,所以 Vim9 脚本需要混合两种类型系统。
Vim9 无意完全复刻 TypeScript 语法与语义,只选取适合 Vim 用户体验良好的部分。
TypeScript 本身比较复杂,有其历史渊源,各有优缺点。想了解其缺点可阅读
《JavaScript: The Good Parts》,或文章《TypeScript: the good parts》中 "应当规
避的特性" 一节。
熟悉 Java、Python 等语言的开发者,同样会觉得 TypeScript 存在难以理解或不喜欢的
设计。Vim9 会尽量避开这些设计。
具体来说,Vim9 特意不采用 TypeScript 的以下设计:
- TypeSript 重载 "+",同时用于加法和字符串拼接: 这和老式 Vim 脚本冲突,极易引
发错误。因此 Vim9 沿用 ".." 做字符串拼接,Lua 也使用同样写法。此写法也支持更
多类型自动转字符串。
- TypeScript 可用 '99 || "yes"' 这类表达式作为条件,但不能将求值结果赋值给布尔
变量,行为不一致,容易造成困扰。Vim9 识别 && 或 || 表达式,并可将结果作为布
尔值使用。新增 falsy-operator 用于设置表达式缺省值,参见 vim9-falsy 。
- TypeScript 中空串视为假值,但空列表、空字典为真值,逻辑不一致。Vim9 中空列表
和空字典都视为假值。
- TypeScript 的各类 "只读" 类型实用性有限,类型转换就能打破不可变限制。Vim9 直
接对值锁定,灵活性更高,但仅在运行时做检查。
- TypeScript "import" 语句的语法复杂,和 Vim 导入机制不匹配。Vim9 采用更简洁的
导入机制,导入脚本只会加载一次。
声明
老式 Vim 脚本全部使用 :let 赋值,而 Vim9 采用变量声明机制。二者语法不同,因
此选用新命令 :var 。很多编程语言都使用这个关键字,语义稍有不同,但非常容易识
别为变量声明。
:const 用于常量是通用做法,但不同语言间语义有差异。部分语言仅锁定变量引用不
可重赋值,另一些同时锁定变量引用与值均不可变。考虑到 "final" 在 Java 里使变量
引用不可变已为人熟知,Vim9 采用它来实现该语义。而 :const 则用来同时锁定变量
引用与值均不可变。这套设计在老式 Vim 脚本中已有雏形,含义基本一致。
最终效果非常接近 Dart:
:var name # 变量引用与值均可修改
:final name # 变量不可重新赋值,值可修改
:const name # 变量引用与值均不可变
由于老式脚本与 Vim9 脚本会混合使用、全局变量可共享,因此类型检查最好可选。同时
类型推断可以省去很多场景下的类型标注。在声明后追加类型的写法,TypeScript 风格
最为合适:
vim9script
var name: string # 显式指定字符串类型
name = 'John'
const GREETING = 'Hello' # 自动推断为字符串类型
echo $'{GREETING} {name}'
变量声明的类型书写示例:
vim9script
var vlist: list<number>
final flist: list<string> = ['Vim']
def Func(arg1: string, arg2: number): string
return $'{arg1}{arg2}'
enddef
vlist[0] = 9
echo Func(flist[0], vlist[0]) # Vim9
曾考虑过两套备选语法方案:
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 的开发者更友好。第二种相比第一种没有明显优势,先排除第
二种。
因为 Vim9 支持类型推断,不写类型时会从赋值推导。如果采用类型在前的写法, var
后面无法区分接下来是类型名还是变量名,会增加 Vim 和人类阅读的解析难度。同时还
会出现变量名和类型名冲突的问题,例如 `var string string`,可读性极差。
最终选定用冒号分隔变量名与类型的语法。虽然多了标点符号,但可以清晰拆分声明的各
个组成部分。
表达式
表达式求值逻辑原本已经接近其他编程语言,但仍有部分反直觉细节有待改进。例如布尔
条件可以接受字符串,自动转为数值,判断是否非零。这种行为违背直觉,很容易出错,
因为以非数位开头的文本会被转为 0,判定为假。所以将字符串放进条件表达式经常不报
错静默判定为假,常会造成混淆。
Vim9 启用更严格的类型检查规避这类错误。在 :if ` 命令、 || 操作符这类条件场景
中,仅接受布尔类的值:
真值: true 、 v:true 、 1 、`0 < 9`
假值: false 、 v:false 、 0 、`0 > 9`
注意数值零为假,数值一为真。相比大多数语言更宽松。做出这个取舍是因为大量内置函
数返回这类数值,改动会得不偿失。经过实践验证,这套规则运行良好。
任意类型的值想转为布尔型,可用 !! 运算符 (参见 vim9-!! ):
vim9script
# 以下全部为真:
echo [!!'text', !![1], !!{'x': 1}, !!1, !!1.1]
# 以下全部为假:
echo [!!'', !![], !!{}, !!0, !!0.0]
JavaScript 中有以下快捷写法:
GetName() || 'unknown'
但这和 Vim9 条件只接受布尔值的规则冲突。为此新增了 "??" 运算符:
GetName() ?? 'unknown'
这里用户显式表达自己意愿,使用原值而非隐含作布尔型使用,这就是准假值运算符
falsy-operator ,参见 vim9-falsy 。
导入与导出
老式 Vim 脚本的一个问题是所有函数与变量缺省均为全局。虽然可以设置为脚本局部,
但其他脚本就无法访问。这与软件包仅导出选定成员,其余保持私有的理念相悖。
Vim9 脚本提供一套和 JavaScript 导入导出非常相似的机制。它是原有 :source 命令
的改良版本,行为符合预期:
- 不再全部缺省为全局: 所有成员缺省为脚本局部,一部分成员可显式导出。
- 导入脚本时显式列出要导入的符号,避免后续新增功能引发命名冲突与功能异常。
- 该机制适合编写大型脚本,对外拥有清晰 API: 导出的函数、变量、类。
- 在包内部使用相对路径导入,加载速度快,无需遍历大量目录检索。
- 脚本一旦导入,成员会被缓存,无需重复加载。
- 不再需要 Vim 特有的 "s:" 前缀标记脚本局部成员。
加载 Vim9 脚本 (无论加载者是 Vim9 脚本还是老式 Vim 脚本) 时,只能访问全局定义
的成员,不能直接访问导出成员。
曾评估过以下备选方案:
- 将所有导出成员直接变为当前脚本局部成员。缺点是无法控制引入哪些符号,有可能很
快就会产生问题。
- 自动将导出成员设为全局。缺点是无法避免全局命名空间的命名冲突。
- 完全禁止直接加载 Vim9 脚本,强制使用 :import 。不方便脚本测试,也难以在命令
行临时加载进行实验。
注意老式 Vim 脚本 同样 可以使用 :import ,见上。
尽早编译函数
函数在实际调用时或执行 :defcompile 时才编译。为什么不提前编译,以便尽早报告
语法和类型错误?
读到函数定义时无法立刻编译,因为存在前向引用,即引用后续才定义的函数。考虑定义
A、B、C 三个函数: A 调用 B,B 调用 C,C 又调用 A。无法通过调整函数顺序消除前向
引用。
另一个方案是先完整扫描文件,收集所有成员位置并推断类型,解决前向引用问题,之后
再执行脚本、编译函数。这需要对脚本做两次解析,速度变慢;并且脚本顶层的部分条件
判断 (如检测特性是否支持) 也难以处理。曾尝试实现,最终无法稳定工作。
也可以在脚本末尾统一编译全部函数。缺点是,即便函数永远不会被调用,编译开销依然
存在。因为启动速度是核心目标,多数场景下,最好延迟编译,并接受运行时才报告语法
和类型错误。如果希望提前捕获错误 (例如测试场景),在脚本末尾添加 :defcompile
可作救济。
为什么不选用现有的嵌入式语言?
Vim 支持对接 Perl、Python、Lua、Tcl 等语言的接口。但受各种因素影响,这些接口一
直没有大规模普及。设计 Vim9 时,决定降低这类外部语言接口的优先级,专注 Vim 脚
本自身。
当然插件开发者可能更熟悉其他语言,想要复用现有库或是追求更好性能。官方鼓励插件
作者用任意语言编写代码,通过作业和通道作为外部进程运行。后续也会持续优化这套流
程。
使用外部工具同样存在弊端。另一种思路是将工具代码转为 Vim 脚本。想要降低翻译成
本同时保证执行速度,Vim9 脚本就需要支持对应的语法结构。Vim9 现已支持类、对象、
接口、枚举,让这种转换变得越来越可行。
vim:tw=78:ts=8:noet:ft=help:norl: