vim9

vim9.txt 适用于 Vim 9.1 版本。 最近更新: 2025年12月 VIM 参考手册 by Bram Moolenaar 译者: Willis Vim9 脚本命令和表达式。 Vim9 vim9 多数表达式的帮助可见 eval.txt 。此文件是关于 Vim9 脚本的新语法和特性。 1. 什么是 Vim9 脚本? Vim9-script 2. 差别 vim9-differences 3. 新风格函数 fast-functions 4. 类型 vim9-types 5. 泛型函数 generic-functions 6. 命名空间、导入和导出 vim9script 7. 类和界面 vim9-classes 8. 理据 vim9-rationale

注意: 在本 vim9.txt 帮助文件里,以 vim9script 开始的 Vim9 脚本代码块 (以及 以 vim9cmd 开始的单行语句) 会使用 Vim9 脚本语法高亮。它们还可以被直 接执行,从而直观地看到运行后输出的结果。具体来说,用 :'<,'>source (见 :source-range ) 来执行,也就是,先可视选择内容,再用 V 进入可视 行模式,然后输入 :so 。例如,试试运行一下下面的 Vim9 脚本: vim9script echowindow "Welcome to Vim9 script!" 也有不应去执行的代码示例 - 它们解释概念,但不需要示例可执行。这样的代 码会使用通用的代码语法高亮,如下例: def ThisFunction() # 局部于脚本 def g:ThatFunction() # 全局 export def Function() # 用于 import 和 import autoload

1. 什么是 Vim9 脚本? Vim9-script Vim 脚本随着时间推移日渐庞大,同时需要保持后向兼容。这意味着过去所做的错误决定 常常不能再改变,而和 Vi 的兼容性限制了可能的解决方案。执行颇慢,每行在每次执行 时都要先解析。 Vim9 脚本的主要目标是大幅提高性能。这是通过把命令编译为可有效执行的指令完成 的。可以期待执行速度有成十上百倍的提高。 一个次要目标是避免 Vim 特定的构造,而尽量和包括 JavaScript、TypeScript 和 Java 这样的主流编程语言接近。 只有不 100% 后向兼容才能达到所需的性能提高。例如,要使函数参数可通过 "a:" 字典 获取,会增加不少开销。其它一些差别,比如错误处理的方式,则更细微一些。 Vim9 脚本语法、语义和行为会应用于: - 使用 :def 命令定义的函数 - 首个命令是 vim9script 的脚本文件 - 在上述上下文中,定义的自动命令 - 使用 vim9cmd 命令修饰符前缀的命令 Vim9 脚本文件里使用 :function 时,仍然会用老式语法并使用最高的 scriptversion 。不过,这容易混淆,不鼓励。 Vim9 脚本和老式的 Vim 脚本可以混合。重写旧脚本并非必需,它们会继续工作。但可用 一些 :def 函数来编写要求快速的代码。 :vim9[cmd] {cmd} :vim9 :vim9cmd 使用 Vim9 脚本语法、语义和执行方式来解析和执行 {cmd}。在手动键 入的命令、 :function 定义的函数和老式脚本里都可用。 下列的简短例子展示了老式 Vim 脚本命令和 :vim9cmd (因此使用 Vim9 脚本的上下文) 的异同。外观看来很相似,但区别不止是语法 上,还有语义和执行方式上。 >vim call popup_notification('entrée'[5:] \ ->str2list()->string(), #{time: 7000}) vim9cmd popup_notification('entrée'[5 :] ->str2list()->string(), {time: 7000}) < 备注: 1) 之所以输出结果不同,是因为 Vim9 脚本使用字符索引,而 老式 Vim 脚本使用字节索引 - 见 vim9-string-index 。 2) 语法也有区别。Vim9 脚本里: - "[5 :]" 里的空格是必须的 (见 vim9-white-space )。 - 续行符 "\" 是非必须的。 - "#" (以避免给字典键加上引号) 既非必要,也不合法 - 见 #{} E1164 :vim9cmd 不能单独使用;必须后跟一个命令。 :leg[acy] {cmd} :leg :legacy 使用老式脚本语法、语义和执行方式来解析和执行 {cmd}。只在 Vim9 脚本或 :def 函数里有用。要得到和上例等价的结果 (注释说明了为 什么输出结果会不同): vim9script # 老式脚本上下文 - 所以,创建弹出并显示 [769, 101] legacy call popup_notification('entrée'[5:] \ ->str2list()->string(), #{time: 7000}) # Vim9 脚本上下文 - 所以,创建弹出并显示 [101] popup_notification('entrée'[5 :] ->str2list()->string(), {time: 7000}) Vim9 脚本里,脚本局部变量可用前缀 "s:",和老式 Vim 脚本的语法 相同,不过在 Vim9 脚本里,此前缀是可选的,因为变量缺省就是局部 于脚本的。下例展示了语法上的差异: 在 Vim9 脚本里,"k" 定义了脚 本局部变量,而在老式 Vim 脚本的上下文里,须用 "s:k" 来访 问。 vim9script var k: string = "Okay" echo k legacy echo s:k E1189 在已编译的 Vim9 脚本里,用于控制流的命令不支持 :legacy 。 例如: vim9script def F_1189() if v:version == 900 # E1189: Cannot use :legacy with this command: endif legacy endif enddef F_1189() E1234 :legacy 不能单独使用;必须后跟一个命令。

2. 和老式 Vim 脚本的差异 vim9-differences 总览 E1146 使用 Vim9 脚本和 :def 函数最常见差异的简要小结;细节见后: - 注释以 # 开始而不是 ": echo "hello" # 注释 - 很少需要反斜杠用来作续行符: echo "hello " .. yourName .. ", how are you?" - 很多地方需要空格以提高可读性,见 vim9-white-space 。 - 赋值不用 :let E1126 ,变量用 :var 声明: var count = 0 count += 3 - 常量可用 :final:const 声明: final matches = [] # 之后可再扩充列表 const names = ['Betty', 'Peter'] # 不能改变 - :final 不再能用作 :finally 的缩写。 - 变量和函数缺省局部于脚本。 - 函数声明要带参数类型和返回值类型: def CallMe(count: number, message: string): bool - 调用函数不用 :call : writefile(['done'], 'file.txt') - 不能用以下的旧式的 Ex 命令: :Print :append :change :d 后跟 'd' 或 'p'。 :insert :k :mode :open :s 只带标志位 :t :xit - 有些命令,特别是用于流控制的命令,不能缩短。例如, :throw 不能写成 :th vim9-no-shorten - 不可用花括号名字。 - 命令前的范围必须用冒号前缀: :%s/this/that - 不再可以用 "@r" 来执行寄存器,需要冒号前缀或使用 :exe : :exe @a - 除非特别指出,使用最高的 scriptversion 。 - 定义表达式映射时,在定义所在的脚本上下文里计算表达式。 - 字符串索引以字符而不是字节计: vim9-string-index - 可能意想不到的一些差异: vim9-gotchas# 开始的注释 老式 Vim 脚本以双引号开始脚本注释。Vim9 脚本则用 # 开始脚本注释。 # 声明 var count = 0 # 出现的次数 原因是双引号也是字符串的开始,在很多地方,尤其是表达式内部换行处,很难区分双引 号真正的意思,因为字符串和注释都可以后跟任意文本。为避免混淆,现在只识别 # 注 释。这和外壳脚本和 Python 程序完全相同。 Vi 里 # 是带行号列出文本的命令。Vim9 脚本可用 :number 代替。 :101 number 为提高可读性,命令和开始注释的 # 之间必须有一个空格: var name = value # 注释 var name = value# 出错! E1170 不要以 #{ 开始注释,它看来像老式的字典常数,有歧义时这会报错。#{{ 和 #{{{ 则可 以,它们用来启动折叠。 开始读取脚本文件时 vim 不知道是 vim9 脚本,直到找到 vim9script 命令为止。 在那之前,需要用老式注释: " 老式注释 vim9script # vim9 注释 这很难看,最好在第一行就放上 vim9script : vim9script # vim9 注释 老式 Vim 脚本中 # 也用于轮换文件名。Vim9 脚本里,要改用 %%。## 可改用 %%% (代 表所有参数)。 Vim9 函数 E1099 :def 定义的函数进行编译。执行成倍加快,通常有 10 到 100 倍的加速。 许多错误在编译时已经可以发现,而不用等到函数执行的时候。有严格的语法,以确保代 码易读易理解。 以下情形之一时进行编译: - 函数第一次调用时 - 脚本中在函数定义后遇到 :defcompile 命令时 - 函数使用 :disassemble 时。 - 函数被已编译的函数调用或用作函数引用时 (为了检查参数和返回类型) E1091 E1191 编译失败后,下次调用时不会再次尝试编译,而会报错: "E1091: Function is not compiled: {name}"。 遇到尚未创建的用户命令时,编译会失败。这种情况可用 execute() 在运行时调用该 命令。 def MyFunc() execute('DefinedLater') enddef :def 不像 :function 那样有 "range"、"abort"、"dict" 或 "closure" 这样的选 项。 :def 函数总会在错误时中止 (除非命令使用了 :slient! 或错误在 :try 块 里捕获),不接受范围、不能是 "dict" 函数,而且总可以有闭包。 vim9-no-dict-function E1182 可用 Vim9 类 ( Vim9-class ) 而不是 "字典函数"。也可以显式传递入字典: def DictFunc(self: dict<any>, arg: string) echo self[arg] enddef var ad = {item: 'value', func: DictFunc} ad.func(ad, 'item') 但可以调用老式字典函数: func Legacy() dict echo self.value endfunc def CallLegacy() var d = {func: Legacy, value: 'text'} d.func() enddef 必须指定参数类型和返回类型。可用 "any" 类型,和老式函数一样,此时类型检查会在 运行时完成。 E1106 参数用不带 "a:" 的名字访问,就像其它语言一样。没有 "a:" 字典或 "a:000" 列表。 vim9-variable-arguments E1055 E1160 E1180 类似于 TypeScript,通过给定名字和列表类型的末项参数来定义可变参数。例如,数值 的列表: def MyFunc(...itemlist: list<number>) for item in itemlist ... 如果函数参数可选 (有缺省值),传递 v:none 作为参数会使用其缺省值。用于在使用 缺省值的参数之后需要指定其它参数的场合。例如: def MyFunc(one = 'one', last = 'last') ... enddef MyFunc(v:none, 'LAST') # 首个参数使用缺省值 'one' vim9-ignored-argument E1181 参数 "_" (下划线) 可用于忽略参数值。用于回调中不需要提供参数值但要给出一个参数 以匹配调用的场合。例如,使用 map() 时需要两个参数,键和值,要忽略键: map(numberList, (_, v) => v * 2) "_" 参数多次使用不会报错。也不需要指定类型。 函数和变量缺省局部于脚本 vim9-scopes 在 Vim9 脚本中,用 :function:def 来指定脚本级别的新函数时,函数局部于 脚本,就像老式脚本里用了 "s:" 前缀那样。"s:" 前缀可选。要定义全局函数或变量, 必须使用 "g:" 前缀。脚本里要被别人导入和在自动载入脚本定义的函数,需要使用 "export",才能被别处使用。 def ThisFunction() # 局部于脚本 def s:ThisFunction() # 局部于脚本 export def Function() # 用于 import 和 import autoload E1075 :function:def 来指定 :def 函数内的嵌套函数时,如果没给出命名空 间,此嵌套函数局部于定义所在的代码块。不能用传递字符串参数的 function() 形式 访问,但函数引用本身可以: def Outer() def Inner() echo 'inner' enddef var Fok = function(Inner) # 正确 var Fbad = function('Inner') # 不行 细节: 这是因为 "Inner" 实际会变成函数引用,指向一个名字内部生成的函数。 函数内不可以定义局部于脚本的函数。可以定义局部函数并赋值给局部于脚本的函数引用 (函数必须在脚本级别声明)。可以用 "g:" 前缀来定义全局函数。 引用函数时如果不带 "s:" 或 "g:" 前缀,Vim 会依次搜索函数: - 在函数作用域内,在块作用域内 - 在脚本作用域内 导入的函数可根据 :import 命令给出的前缀找到。 因为使用局部于脚本的函数引用时可以不带 "s:",其名字必须以大写字母开头,即使用 了 "s:" 前缀也是如此。老式脚本里因为不能以 "funcref" 来进行引用,所以可用 "s:funcref"。而 Vim9 脚本里这种直接引用的形式是可以的,因此必须使用 "s:Funcref" 形式,以防其名和内建函数起冲突。 vim9-s-namespace E1268 Vim9 脚本级别不支持使用 "s:" 前缀。所有不带前缀的函数和变量都是局部于脚本的。 :def 函数里,"s:" 的使用取决于脚本: 老式脚本里的局部于脚本的变量和函数要用 "s:",而 Vim9 脚本里它们不用 "s:"。这和文件其余部分你能看到的用法匹配。 老式函数里必须使用 "s:",见前。不管脚本是 Vim9 还是老式的都是如此。 在所有情况下,函数必须先定义,然后才能使用。所谓使用,是指函数调用时,或 :defcompile 导致调用被编译时,或调用它的函数被编译时 (以确定返回类型)。 其结果是,无命名空间的函数和变量通常可在其定义所在或导入的脚本内找到。而全局函 数和变量可以在任何地方定义 (要找到需要点运气!用 :verbose 往往可以看到它上次 是在哪里设置的)。 E1102 全局函数还是可以在几乎任何时候定义和删除。vim9 脚本里,局部于脚本的函数在脚本 载入时定义一次,且不能自己删除或替代 (但脚本重载时是会的)。 编译函数时和遇到函数调用时如果函数 (还) 未定义,不触发 FuncUndefined 自动命 令。如果需要,可用自动载入函数,或调用老式函数,那里会触发 FuncUndefined重载 Vim9 脚本时缺省清除函数或变量 vim9-reload E1149 E1150 再次载入同一老式 Vim 脚本时,不会删除任何东西,脚本里的命令会替代原有的变量和 函数,创建新的内容,而被删除的东西还会悬挂在那里。 再次载入同一 Vim9 脚本时,删除所有已有的局部于脚本的函数和变量,以便从干净的状 态开始。这可用于开发插件时新版本的实验。如果换了任何名字,不需要担心旧名字还存 在。 如果确实要保留项目,可用: vim9script noclear 可在脚本中使用此命令,以便在脚本重载时,在某些情况下用 finish 命令放弃载入。 例如,如果设置了某个缓冲区局部选项为函数时,函数不需要再次定义: vim9script noclear setlocal completefunc=SomeFunc if exists('*SomeFunc') finish endif def SomeFunc() .... 用 :var、:final 和 :const 声明变量 vim9-declaration :var E1079 E1017 E1020 E1054 E1087 E1124 局部变量须用 :var 定义。局部常量须用 :final:const 定义。我们把两者都 称为 "变量"。 变量可以局部于脚本、函数或代码块: vim9script var script_var = 123 def SomeFunc() var func_var = script_var if cond var block_var = func_var ... 变量只在定义所在的块和嵌套块中可见。块定义结束后,变量不再可访问: if cond var inner = 5 else var inner = 0 endif echo inner # 出错! 变量必须在使用之前进行声明: var inner: number if cond inner = 5 else inner = 0 endif echo inner 不过对简单值有更简短和更快的方法: var inner = 0 if cond inner = 5 endif echo inner E1025 E1128 要有意识地从之后的代码中隐藏某个变量,可用代码块: { var temp = 'temp' ... } echo temp # 出错! 用户命令里这特别有用: command -range Rename { var save = @a @a = 'some expression' echo 'do something with ' .. @a @a = save } 自动命令也是: au BufWritePre *.go { var save = winsaveview() silent! exe ':%! some formatting command' winrestview(save) } 不过,用 :def 函数可能更好些。 E1022 E1103 E1130 E1131 E1133 E1134 变量声明时如果带类型但不带初始块,其值初始为假值 (布尔型)、空 (字符串、列表、 字典,等等) 或零 (数值、any,等等)。特别重要的是,使用 "any" 类型时,缺省值为 数值零。例如声明列表时,可以加入项目: var myList: list<number> myList->add(7) 初始化变量为 null 值,如 null_list 时,和不初始化变量是不同的。以下例子会抛 出错误: var myList = null_list myList->add(7) # E1130: Cannot add to null list E1016 E1052 E1066 Vim9 脚本中不能用 :let 。已有的变量可直接赋值,不需要任何命令。全局、窗口、标 签页、缓冲区和 Vim 变量也是一样,因为它们并非真正通过声明产生,但它们也可用 :unlet 删除。 E1065 不能用 :va 来声明变量,必须使用全名 :var 来书写。这是为了方便阅读。 E1178 :lockvar 不能用于局部变量。可用 :const:final 代替。 exists()exists_compiled() 函数不能用局部变量或参数。 E1006 E1041 E1167 E1168 E1213 变量、函数和函数参数不能隐藏在同一脚本文件里之前定义或导入的变量和函数。 变量可以隐藏 Ex 命令,如有需要请给变量换名。 全局变量必须带上 "g:" 前缀,即使脚本级别的也是如此。 vim9script var script_local = 'text' g:global = 'value' var Funcref = g:ThatFunction 全局函数必须带上 "g:" 前缀: vim9script def g:GlobalFunc(): string return 'text' enddef echo g:GlobalFunc() 自动载入函数不需要 "g:" 前缀。 vim9-function-defined-later 尽管可以不带 "g:" 前缀调用全局函数,编译时它们必须已存在。加上 "g:" 前缀后,函 数可以在其后再定义。例如: def CallPluginFunc() if exists('g:loaded_plugin') g:PluginFunc() endif enddef 但如果像下面这么做,编译时会报错 "PluginFunc" 不存在,即使 "g:loaded_plugin" 不存在: def CallPluginFunc() if exists('g:loaded_plugin') PluginFunc() # 报错,函数找不到 endif enddef 可用 exists_compiled() 来避免此错误,但函数可能也不会被调用了,即使 "g:loaded_plugin" 之后被定义了也不会: def CallPluginFunc() if exists_compiled('g:loaded_plugin') PluginFunc() # Function 可能永远不会被调用 endif enddef 因为 `&opt = value` 现在用来给选项 "opt" 赋值,所以不再能用 ":&" 来重复 :substitute 命令了。 vim9-unpack-ignore 解包赋值里,下划线可用于忽略列表项目,类似于函数参数的忽略方法: [a, _, c] = theList 要忽略其余项目: [a, b; _] = longList E1163 E1080 可以使用解包记法,一次声明多于一个变量。每个变量可给出类型或从值推断: var [v1: number, v2] = GetValues() 仅当有带值列表的时候才建议使用此记法,一行声明一个变量更易阅读和修改。 常量 vim9-const vim9-final 常量如何工作因语言而异。有些语言把不能重新赋其它值的变量当作常量。JavaScript 是一个例子。其它语言则会使值也不可更改,因此如果常量使用了列表,列表本身也不可 改变。Vim9 可以兼有两者。 E1021 E1307 可用 :const 使变量及其值为常量。可用于复合结构以确保其不会被修改。示例: const myList = [1, 2] myList = [3, 4] # 出错! myList[0] = 9 # 出错! myList->add(3) # 出错! :final E1125 可用 :final 使变量为常量,而值仍然可更改。Java 用户对此很熟悉。例如: final myList = [1, 2] myList = [3, 4] # 出错! myList[0] = 9 # 正确 myList->add(3) # 正确 常量写入全大写 ALL_CAPS 是惯例,但不必如此。 常量限制只适用于值本身,而不是其引用的值。 final females = ["Mary"] const NAMES = [["John", "Peter"], females] NAMES[0] = ["Jack"] # 出错! NAMES[0][0] = "Jack" # 出错! NAMES[1] = ["Emma"] # 出错! NAMES[1][0] = "Emma" # 正确,现在 females[0] == "Emma" 省略 :call 和 :eval E1190 可直接调用函数而无需 :call : writefile(lines, 'file') :call 还可用,但不鼓励。 方法只要以标识符开始或不可能是 Ex 命令,可直接调用而无需 eval 。函数的 "(" 或 "->" 必须后跟内容,不能有换行。例如: myList->add(123) g:myList->add(123) [1, 2, 3]->Process() {a: 1, b: 2}->Process() "foobar"->Process() ("foobar")->Process() 'foobar'->Process() ('foobar')->Process() 在罕见情形下函数名和 Ex 命令有二义性,用 ":" 前缀来明确你想用的是 Ex 命令。例 如,既有 :substitute 命令,又有 substitute() 函数。如果一行以 substitute( 开始,会假定使用函数,要使用命令版本,加上冒号前缀: :substitute(模式 (替代 ( 如果表达式以 "!" 开始,解读为外壳命令,而不是条件取反。因而,以下是外壳命令: !shellCommand->something 要用 "!" 进行取反,把表达式放在括号里: (!expression)->Method() 注意 尽管变量在使用前必须先定义,函数在定义前就可以调用。为了允许函数间能相互 依赖,这是有必要的。但因此效率会稍稍低些,因为函数需要依名查找。且函数名的拼写 错误只有在函数被调用时才能发现。 省略 function() 表达式中,用户定义函数可用作函数引用,而无需通过 function() 。这时会检查参数 类型和返回类型。函数必须已经有定义。 var Funcref = MyFunction 而使用 function() 时,返回类型是 "func",一个可带任何数量参数和任何返回类型 (包括 void) 的函数。如果 function() 的参数用上引号,函数可以延后定义。 匿名函数使用 => 而不是 -> vim9-lambda 在老式脚本里,"->" 既用于函数调用,也用于匿名函数,这里可能产生混淆。另外,找 到 "{" 时解析器需要知道它是匿名函数还是字典的开始,现在由于参数类型的使用,这 里的情况更复杂。 为避免这些问题,Vim9 脚本的匿名函数使用不同的语法,这和 JavaScript 类似: var Lambda = (arg) => expression var Lambda = (arg): type => expression E1157 匿名函数在直到 "=>" 为止的参数里不允许换行 (这样 Vim 才能识别带括号的表达式和 匿名函数参数的区别)。这样可以: filter(list, (k, v) => v > 0) 这样不可以: filter(list, (k, v) => v > 0) 这样也不可以: filter(list, (k, v) => v > 0) 但可用反斜杠在解析发生前,把行进行连接: filter(list, (k, \ v) \ => v > 0) vim9-lambda-arguments E1172 在老式脚本里,匿名函数调用时可带任意多个额外参数,没办法警告不要这么做。在 Vim9 脚本里参数数目必须匹配。如果要接受任何参数,或任何额外参数,可用 "..._", 这使函数接受 vim9-variable-arguments 。例如: var Callback = (..._) => 'anything' echo Callback(1, 2, 3) # 显示 "anything" inline-function E1171 此外,匿名函数可以包含 {} 包围的多个语句: var Lambda = (arg) => { g:was_called = 'yes' return expression } 比如可用来实现计数器: var count = 0 var timer = timer_start(500, (_) => { count += 1 echom 'Handler called ' .. count }, {repeat: 3}) 结尾的 "}" 必须位于行首。后面可跟其它字符,例如: var d = mapnew(dict, (k, v): string => { return 'value' }) "{" 之后不能跟随命令,那里只可以有注释。 command-block E1026 定义用户命令时也可用代码块。在代码块内使用 Vim9 语法。 这里是个使用嵌入文档 (here-docs) 的例子: com SomeCommand { g:someVar =<< trim eval END ccc ddd END } 如果语句包含字典,其结束花括号不能写在行首。否则,被解析为代码块的结束。下面这 样不行: command NewCommand { g:mydict = { 'key': 'value', } # 错误: 会识别为代码块的结束 } 把 '}' 放在最后项目之后可以解决这个问题: command NewCommand { g:mydict = { 'key': 'value' } } 理据: "}" 不能在命令之后出现,因为需要对命令进行解析才能找到它。为了一致起见, "{" 之后也不能跟随命令。不幸的是,这就意味着不接受 "() => { command }",必须 使用换行。 vim9-curly 为了不让字典常量的 "{" 被识别为语句块,把它包围在括号里: var Lambda = (arg) => ({key: 42}) 和命令块的开始有歧义的话也是如此: ({ key: value })->method() 自动续行 vim9-line-continuation E1097 许多情况下,表达式显然会在下一行继续。这些情况不需要在行前加入反斜杠 (见 line-continuation )。例如,当列表跨越多行时: var mylist = [ 'one', 'two', ] 字典跨越多行时: var mydict = { one: 1, two: 2, } 使用函数调用时: var result = Func( arg1, arg2 ) 对不在 []、{} 或 () 内的表达式中的二元操作符而言,可在操作符之前或之后断行。例 如: var text = lead .. middle .. end var total = start + end - correction var result = positive ? PosFunc(arg) : NegFunc(arg) 方法调用的 "->" 和成员访问的句号之前都可断行: var result = GetBuilder() ->BuilderSetWidth(333) ->BuilderSetHeight(777) ->BuilderBuild() var result = MyDict .member 命令里如果有一组命令作为参数,行首的 | 字符指示行的继续: autocmd BufNewFile *.match if condition | echo 'match' | endif 注意 这意味着 here 文档里首行不能以竖线开始: var lines =<< trim END | 这不行 END 要么用一空行开始,要么不要用 here 文档。或者暂时在 'cpoptions' 里加入 "C" 标志 位: set cpo+=C var lines =<< trim END | 这样可以 END set cpo-=C 如果 here 文档在函数内出现,'cpoptions' 必须在 :def 前设置,在 :enddef 后恢 复。 在续行还需要反斜杠的地方,如断开长 Ex 命令时,注释可用 '#\ ' 开始: syn region Text \ start='foo' #\ 注释 \ end='bar' 就像在老式脚本里可用 '"\ ' 一样。即使续行不需要反斜杠和以竖线开始行的情况,也 要如此: au CursorHold * echom 'BEFORE bar' #\ 一些注释 | echom 'AFTER bar' E1050 要识别出现在行首的操作符,需要在范围之前加上冒号。要把 "start" 加上 "print": var result = start + print 相当于: var result = start + print 而要赋值 "start" 并显示一行: var result = start :+ print 范围后必须跟 Ex 命令。不带冒号时不用 :call 就可以调用函数,但在范围后需要: MyFunc() :% call MyFunc() 注意 +cmd 参数不需要冒号: edit +6 fname 函数声明中也可以在参数间换行: def MyFunc( text: string, separator = '-' ): string 因为续行不容易识别,命令的解析必须更严格。例如,因为首行有错,下面的第二行会看 作一个单独的命令: popup_create(some invalid expression, { exit_cb: Func}) 现在,"exit_cb: Func})" 实际是一个合法的命令: 保存任何改动到文件 "_cb: Func})" 并退出。为了避免此种错误,Vim9 脚本中在多数命令名和参数间必须有空白。 E1144 不过,不能识别作为命令参数的命令里的续行。例如 "windo echo expr" 里不能识别 "expr" 内部的换行。 注意: - "enddef" 不能用于续行的开始,它会结束当前函数。 - 赋值语句的 LHS 不接受换行。特别是列表解包 :let-unpack 。这样可以: [var1, var2] = Func() 这样不行: [var1, var2] = Func() - :echo:execute 和类似命令的参数与参数之间不接受换行。这样可以: echo [1, 2] [3, 4] 这样不行: echo [1, 2] [3, 4] - 有些情况 Vim 解析命令有困难,尤其是一个命令用作另一个命令的参数的时候,如 :windo 。这些情况续行必须使用反斜杠。 空白 vim9-white-space E1004 E1068 E1069 E1074 E1127 E1202 Vim9 脚本强制空白的正确使用。以下不再允许: var name=234 # 出错! var name= 234 # 出错! var name =234 # 出错! "=" 前后必须有空白: var name = 234 # 正确 命令之后开始注释的 # 之前必须有空白字符: var name = 234# 出错! var name = 234 # 正确 多数操作符需要用空白包围。 子列表 (切片) 表达式中的 ":" 两边需要空白,出现在开始和结束处的除外: otherlist = mylist[v : count] # v:count 有不同含义 otherlist = mylist[:] # 建立列表的备份 otherlist = mylist[v :] otherlist = mylist[: v] 以下位置不允许空白: - 函数名和 "(" 之间: Func (arg) # 出错! Func \ (arg) # 出错! Func (arg) # 出错! Func(arg) # 正确 Func( arg) # 正确 Func( arg # 正确 ) E1205 :set 命令在选项名和后续的 "&"、"!"、"<"、"="、"+="、"-=" 或 "^=" 之间不能有 空白。 没有花括号扩展 不能使用 curly-braces-names不忽略命令修饰符 E1176 不接受命令修饰符的命令如果用上命令修饰符,会报错。 E1082 另外,使用了命令修饰符但后面不跟命令,现在也是错误。 字典常量 vim9-literal-dict E1014 传统上 Vim 支持使用 {} 语法的字典常量: let dict = {'key': value} 后来越来越明显地,使用简单文本键非常常见,所以引入了支持后向兼容的常量字典: let dict = #{key: value} 不过,此种 #{} 语法不同于任何已有的语言。因为使用常量并使用表达式作为键更为常 见,也考虑到 JavaScript 也使用此种语法,使用 {} 形式的字典常量被认为是更有用的 语法。Vim9 脚本里,{} 形式使用常量键: var dict = {key: value} 这种形式可以用于字母数字字符,下划线和连字符。如果使用其它字符,使用单引号或双 引号: var dict = {'key with space': value} var dict = {"key\twith\ttabs": value} var dict = {'': value} # 空键 E1139 如果键需要表达式,可用方括号,就像 JavaScript 那样: var dict = {["key" .. nr]: value} 键类型可以是字符串、数值、布尔型或者浮点数。其它类型会报错。不带 [] 时直接用作 字符串值,保留引导的零。而用 [] 的表达式会先经过计算再转换为字符串。这样引导的 零就会被丢弃: var dict = {000123: 'without', [000456]: 'with'} echo dict {'456': 'with', '000123': 'without'} 浮点数只能在 [] 里工作,因为如果不是的话,句号不被接受: var dict = {[00.013]: 'float'} echo dict {'0.013': 'float'} 没有 :xit、:t、:k、:append、:change 或 :insert E1100 这些命令很容易会和局部变量名混淆。 :x:xit 可用 :exit 替代。 :t 可用 :copy 替代。 :k 可用 :mark 替代。 比较符 字符串的比较符不考虑 'ignorecase' 选项。 因此 "=~" 和 "=~#" 一样。 "is" 和 "isnot" ( expr-isexpr-isnot ) 用于字符串时,现在总是返回假值。老 式脚本里它们只是比较字符串内容,而在 Vim9 脚本里它们比较是否同一,因为字符串 使用时会先复制,因此两个字符串永不会等价 (有一天字符串不是复制而采用引用计数, 这可能会改变)。 错误后中断 老式脚本里,遇到错误时,Vim 会继续执行后续行。这会引起一长串错误,需要 CTRL-C 才能停止。Vim9 脚本的命令执行会在第一个错误处停止。例如: vim9script var x = 不-存-在 echo '不会执行' For 循环 E1254 循环变量不能事先声明: var i = 1 for i in [1, 2, 3] # 出错! 但可用全局变量: g:i = 1 for g:i in [1, 2, 3] echo g:i endfor 老式 Vim 脚本有一些技巧来实现在列表句柄上的 for 循环,以删除当前或前一项目。在 Vim9 脚本中,正常使用索引就可以了,如果项目被删除,列表中的对应项目会跳过。 示例老式脚本: let l = [1, 2, 3, 4] for i in l echo i call remove(l, index(l, i)) endfor 会显示: 1 2 3 4 而在编译后的 Vim9 脚本中,会得到: 1 3 一般而言,不应改变正在遍历的列表。如果需要的话,先建立备份。 遍历列表的列表时,可以修改内层的列表。循环变量是 "final",它不能改变,但它的值 可以改变。 E1306 循环深度,即 :for 和 :while 循环加起来,不能超过 10 层。 条件和表达式 vim9-boolean 多数情况下,条件和表达式就像其它语言里那样的用法。有些值和老式 Vim 脚本有所不 同: 值 老式 Vim 脚本 Vim9 脚本 0 准假值 准假值 1 准真值 准真值 99 准真值 出错! "0" 准假值 出错! "99" 准真值 出错! "text" 准假值 出错! 用于 "??" 操作符和使用 "!" 时,不会报错,每个值都是准假值或准真值。这很像 JavaScript,唯一的例外是空列表和字典也被视为准假值: 类型 何时为准真值 bool true、v:true 或 1 number 非零 float 非零 string 非空 blob 非空 list 非空 (和 JavaScript 不同) tuple 非空 (和 JavaScript 不同) dictionary 非空 (和 JavaScript 不同) func 有函数名时 special true 或 v:true job 非 NULL 时 channel 非 NULL 时 class 非 NULL 时 object 非 NULL 时 (TODO: 应为 isTrue() 返回 true 时) 布尔操作符 "||" 和 "&&" 期待值为布尔型、零或一: 1 || false == true 0 || 1 == true 0 || false == false 1 && true == true 0 && 1 == false 8 || 0 出错! 'yes' && 0 出错! [] || 99 出错! "!" 用作反转操作符时,任何类型都不会报错,结果为布尔型。"!!" 可用于把任何值转 换为布尔型: !'yes' == false !![] == false !![1, 2, 3] == true " .." 用作字符串连接时,简单类型的参数总是转化为字符串。 'hello ' .. 123 == 'hello 123' 'hello ' .. v:true == 'hello true' 简单类型是数值、浮点数、特殊类型和布尔型。其它类型可用 string() false true null null_blob null_channel null_class null_dict null_function null_job null_list null_object null_partial null_string E1034 Vim9 脚本可以使用如下预定义值: true false null null_blob null_channel null_class null_dict null_function null_job null_list null_tuple null_object null_partial null_string true 等价于 v:truefalse 等价于 v:falsenull 等价于 v:nullnull 类型为 "special",而其它的 "null_" 值可根据其名推定类型。null 值和空值 处理方式通常相同,但不绝对。这些值可用于清除局部于脚本的变量,因为它们不能用 :unlet 删除。例如: var theJob = job_start(...) # 让作业自己做事 theJob = null_job 这些值也可用于参数的缺省值: def MyFunc(b: blob = null_blob) # 注意: 和 null 比较,而不是 null_blob, # 以区分缺省值和空 blob。 if b == null # 没给出 b 参数 更多和 null 的测试的信息可见 null-compare 。 可以把 null 和任何值比较,不会给出类型错误。但把 null 和数值、浮点或布尔型 比较总返回 false 。这和老式脚本不同,那里把 null 和零或 false 比较会返回 true vim9-false-true 转换布尔型为字符串时使用 falsetrue ,而不像老式脚本那样用 v:falsev:truev:none 没有对应的 none 替代,在其他语言中它没有类似的结构。 vim9-string-index 字符串用 [idx] 索引 或 [idx : idx] 取切片时使用的是字符索引而非字节索引。组合 字符包含在内。例如: echo 'bár'[1] 在老式脚本里,这会得到字符 0xc3 (一个非法字节),在 Vim9 脚本里这会得到字符串 'á'。 负索引从结尾处开始计算,"[-1]" 代表末字符。要排除末字符,用 slice() 。 如果要组合字符分别计算,可用 strcharpart() 。 索引超出范围时,返回空字符串。 老式脚本中,接受 "++var" 和 "--var",不报错也没有效果。Vim9 脚本中则会报错。 零开始的数值不被识别为八进制,只有 "0o" 开始的数值才识别为八进制: "0o744". scriptversion-4 注意什么 vim9-gotchas Vim9 设计和常用的编程语言相近,而同时试图支持老式的 Vim 命令。这里不得不做出某 些妥协。这里是若干出人意外之处的一个小结。 Ex 命令范围需要冒号前缀。 -> 老式 Vim: 右移前行 ->func() Vim9: 继续行上的方法调用 :-> Vim9: 右移前行 %s/a/b 老式 Vim: 在所有行上替代 x = alongname % another Vim9: 续行上的取余操作符 :%s/a/b Vim9: 在所有行上替代 't 老式 Vim: 跳转到位置标记 t 'text'->func() Vim9: 方法调用 :'t Vim9: 跳转到位置标记 t 有些 Ex 命令在 Vim9 脚本中和赋值语句会引起混淆: g:name = value # 赋值 :g:pattern:cmd # :global 命令 要避免 :global:substitute 命令和表达式或赋值相混淆,当这些命令缩写为单 字母时,不能使用若干分隔符: ':'、'-' 和 '.'。 g:pattern:cmd # 非法命令 - 出错 s:pattern:repl # 非法命令 - 出错 g-pattern-cmd # 非法命令 - 出错 s-pattern-repl # 非法命令 - 出错 g.pattern.cmd # 非法命令 - 出错 s.pattern.repl # 非法命令 - 出错 另外,命令和分隔符之间也不能有空格: g /pattern/cmd # 非法命令 - 出错 s /pattern/repl # 非法命令 - 出错 :def 定义的函数会对整个函数进行编译。老式函数可以中途放弃,因而以下的行不会 被立即解析: func Maybe() if !has('feature') return endif use-feature endfunc 而 Vim9 函数会作为一个整体被编译: def Maybe() if !has('feature') return endif use-feature # 可能会报编译错误 enddef 一个临时解决方法是把它分割为两个函数: func Maybe() if has('feature') call MaybyInner() endif endfunc if has('feature') def MaybeInner() use-feature enddef endif 或者把不支持的代码放在 if 块里,带上计算值为假值的常量表达式: def Maybe() if has('feature') use-feature endif enddef 为此也可用 exists_compiled() 函数。 vim9-user-command 编译函数的另一个副作用是编译时要对用户命令的存在与否进行检查。如果用户命令在之 后定义,会报错。下面这样可以: command -nargs=1 MyCommand echom <q-args> def Works() MyCommand 123 enddef 但这样会报错,"MyCommand" 无定义: def Works() command -nargs=1 MyCommand echom <q-args> MyCommand 123 enddef 一个临时解决方法是用 :execute 间接执行命令: def Works() command -nargs=1 MyCommand echom <q-args> execute 'MyCommand 123' enddef 注意 在不识别的命令里,不检查 "|" 和后跟的命令。下例会报丢失 endif 的错误: def Maybe() if has('feature') | use-feature | endif enddef 其它差异 除非被显式覆盖,模式的用法假定置位了 'magic'。 不使用 'edcompatible' 选项值。 不使用 'gdefault' 选项值。 以下 wiki 可能对您有用。这是 Vim9 脚本的一位早期试用者编写的: https://github.com/lacygoill/wiki/blob/master/vim/vim9.md :++ :-- 新增了 ++ 和 -- 命令,类似于加一或减一: ++var var += 1 --var var -= 1 在表达式中使用 ++var 或 --var 目前还不支持。

3. 新风格函数 fast-functions :def :def[!] {name}([arguments])[: {return-type}] 定义名为 {name} 的新函数。在下面的行给出函数体,直到配 对的 :enddef 为止。 E1073 在同一脚本的局部级别上,不能重用 {name}: vim9script def F_1073() enddef def F_1073() # E1073: 名字已被定义: <SNR>... enddef E1011 {name} 必须少于 100 字节长。 E1077 {arguments} 是零或多个参数声明组成的序列。有以下三种形 式: {name}: {type} {name} = {value} {name}: {type} = {value} 第一种形式指定必选参数,声明里必须提供类型。示 例: vim9script def F_1077(x): void # E1077: x 缺少参数类型 enddef 在第二种形式里,因为声明没有显式提供类型,Vim 会根据赋 值自动推导参数的类型。在第二三两种形式里,调用者省略该 可选参数时,都会默认使用 {value} 值。示例: vim9script def SecondForm(arg = "Hi"): void echo $'2. arg 的类型为 "{arg->typename()}" ' .. $'而 arg 的缺省值为 "{arg}"' enddef SecondForm() def ThirdForm(arg2: number = 9): void echo $'3. arg2 的缺省值为 {arg2}' enddef ThirdForm() E1123 :def 定义的函数里,调用内建函数时,参数必须以逗号分 隔: vim9script def F_1123(a: number, b: number): void echo max(a b) # E1123: 参数前缺少逗号: b) enddef F_1123(1, 2) E1003 E1027 E1096 :return 所用的值类型必须匹配 {return-type}{return-type} 省略或为 "void" 时,函数不允许返回任何 值。示例: vim9script def F_1003(): bool return # E1003: 缺少返回值 enddef F_1003() vim9script def F_1027(): bool echo false # E1027: 缺少返回语句 enddef F_1027() vim9script def F_1096(): void return false # E1096: 在没有返回类型的函数中返 enddef F_1096() E1056 E1059 采用 ": {return-type}" 形式时,不能省略 {return-type} 部分 (也就是,只有一个冒号)。": " 的前面也不能有空格。 示例: >vim def F_1056(): # E1056: 期待类型: enddef def F_1059() : bool # E1059: 冒号前不允许有空白:... enddef < :def 定义的函数只有在实际调用、使用 :disassemble:defcompile 时 (示例可见 :disassemble ),才会被 编译为指令序列。那时才会报告语法和类型错误。 E1058 :def:function 里可以嵌套另一个 :def ,最多 可达 49 层。如果试图嵌套 50 或更深层,会报错 E1058 E1117 [!] 只能用于老式 Vim 脚本,因为它允许函数重定义 (和 :function ! 相同)。在 Vim9 脚本里,不接受 ! 形式,因 为脚本局部函数不能被删除或重定义,不过,重新载入相同的 脚本可以移除函数的定义。另外,也不能用 ! 来重定义嵌套 函数。示例: >vim " 老式 Vim 脚本的 :def! 示例 def! LegacyFunc() echo "def! 在老式 Vim 脚本里合法" enddef call LegacyFunc() < vim9script def Func() def! InnerFunc() # E1117: 不能对嵌套的 :def 使用 ! enddef enddef Func() vim9script def! F_477(): void # E477: 不能使用 "!" enddef vim9script def F_1084(): void enddef delfunction! F_1084 # E1084: 无法删除 Vim9 函数 F_1084 注意: 通用错误 E1028 ("编译 :def 函数失败") 代表编译 时出现了无法确定的错误。如果此错误可以重现,请在 https://github.com/vim/vim/issues 处提交报告,因为它可 能反映了 Vim 已有错误报告系统的不足。 :enddef E1057 E1152 E1173 :enddef 结束 :def 定义的函数。必须单独起一行。示例: vim9script def MyFunc() echo 'Do Something' | enddef # E1057: 缺少 :enddef vim9script def F_1173() enddef echo 'X' # E1173: 在 enddef 后发现的文本: echo 'X' vim9script def F_1152() function X() enddef # E1152: 不匹配的 enddef enddef 以下 wiki 可能对您有用。这是 Vim9 脚本的一位早期试用者编写的: https://github.com/lacygoill/wiki/blob/master/vim/vim9.md 如果 :def 函数是在 Vim9 脚本里定义的,可以不经 "s:" 前缀直接访问脚本局部变 量。但这些变量必须在函数编译之前被定义。无法通过条件判断来跳过未声明的变量 (例 如通过 exists() ) 以避免错误发生。例如: vim9script def MyVim9def() echo unus # 回显 1 # echo s:unus # 会出错 E1268 (在 Vim9 脚本中不能使用 s: if exists('duo') # echo duo # 会出错 E1001 (找不到变量: duo) endif enddef var unus: number = 1 MyVim9def() # MyVim9def 此时被编译 ("duo" 此时尚不存在) var duo: number = 2 如果 :def 函数是在老式脚本里定义的,脚本局部变量可用 "s:" 前缀,也可不用。不 过,使用 "s:" 会将变量解析延迟到运行时进行,从而避免因为变量尚不存在触发编译错 误,如下例所示: >vim " 老式 Vim 脚本 def! MyLegacyDef(): void echo [unus, s:unus] # 回显 [1, 1] # (如果取消下行的注释) 第一次执行 'echo s:duo' 会报错 E121 并导致 # 编译错误;后续再执行时,会回显 2: # echo s:duo if exists("s:duo") # 首次执行时: 跳过回显;后续再执行时: 回显 2 echo s:duo endif if exists("duo") # (如果取消下行的注释) 第一次执行 'echo s:duo' 会报错 E1001 并 # 导致编译错误;后续再执行时,会回显 2: # echo duo endif enddef let s:unus = 1 call MyLegacyDef() " 调用 MyLegacyDef() 并进行编译 (如果还没编译过) let s:duo = 2 < E1269 Vim9 脚本里脚本局部变量必须在脚本级别声明,而不能在 :def 函数里创建,也不 能在老式函数里试图使用 "s:" 前缀来声明脚本局部变量。例如: vim9script function F_1269() let s:i_wish = v:true endfunction F_1269() # E1269: 在函数中不能创建 Vim9 脚本变量: s:i_wish :defc :defcompile :defc[ompile] 编译尚未编译的在当前脚本中定义的函数和类 ( class-compile )。编译中如有任何错误,会报错。 示例: 执行下例中的前三行时 (直到 enddef 为止 (包 含)),不会报错,因为尚未编译该 Vim9 :def 函数。但当 执行完整的四行时,编译会失败: vim9script def F_1027(): string enddef defcompile F_1027 # E1027: 缺少返回语句 :defc[ompile] MyClass 编译类里的所有方法。(例见 :disassemble 。) :defc[ompile] {func} :defc[ompile] debug {func} :defc[ompile] profile {func} 有需要的话编译函数 {func}。"debug" 和 "profile" 指定不 同的编译模式。 编译中如有任何错误,会报错。 {func} 也可以是 "ClassName.functionName",以编译指定类 中的函数或方法。 {func} 还可以是 "ClassName",以编译指定类中的所有函数 和方法。 :disa :disassemble :disa[ssemble] {func} 显示 {func} 生成的指令序列 (反汇编)。 用于调试和测试。 如果 {func} 不存在,报错 E1061 {func} 也可以是 "ClassName.functionName",用以反汇编指 定类中的函数。 下例展示了如何向 :defcompile 传递 class 参数,同时 向 :disassemble 传递 "ClassName.functionName" 参数 (光标会定位在可视执行脚本的末行): vim9script class Line var lnum: number def new(this.lnum) enddef def SetLnum() cursor(this.lnum, 52) enddef endclass defcompile Line disassemble Line.SetLnum var vlast: Line = Line.new(line("'>")) vlast.SetLnum() # 光标会定位在此处->_ :disa[ssemble] profile {func} 类似于 :disassemble 但指令用于刨视。 :disa[ssemble] debug {func} 类似于 :disassemble 但指令用于调试。 注意: 命令行补全 {func} 时,脚本局部函数会显示相应的 <SNR> 前缀。取决于包括 wildmenumode() 在内的选项设置,补全可能会作用于 "s:"、"<S" 或者函数 名本身。(例如,假定 Vim 启动时传递了 -u NONE,":disa s:" 和 c_CTRL-E 在补全时都会列出脚本局部函数名。) 局限 计算字符串表达式时,局部于 `:def 函数的变量不可见。下例展示了局部于脚本的常量 "SCRIPT_LOCAL" 可见,但局部于函数的常量 "DEF_LOCAL" 却不可见: vim9script const SCRIPT_LOCAL = ['A', 'script-local', 'list'] def MapList(scope: string): list<string> const DEF_LOCAL: list<string> = ['A', 'def-local', 'list'] if scope == 'script local' return [1]->map('SCRIPT_LOCAL[v:val]') else return [1]->map('DEF_LOCAL[v:val]') endif enddef echo 'script local'->MapList() # 回显 ['script-local'] echo 'def local'->MapList() # E121: 未定义的变量: DEF_LOCAL 这里 map 参数是字符串表达式,它的计算会在函数作用域之外。要避免此局限,在 Vim9 脚本里,可用匿名函数代替: vim9script def MapList(): list<string> const DEF_LOCAL: list<string> = ['A', 'def-local', 'list'] return [1]->map((_, v) => DEF_LOCAL[v]) enddef echo MapList() # 回显 ['def-local'] 对于未编译的命令,如 :edit ,可用反引号扩展 backtick-expansion ,那里局部作 用域可见。例如: vim9script def EditNewBlah() var fname: string = 'blah.txt' split edit `=fname` enddef EditNewBlah() # 创建新分割,显示缓冲区 'blah.txt' 同一循环里定义的闭包或者会分享同一变量的引用,或者每个闭包会有变量的不同备份, 这取决于变量声明所在的位置。变量在循环之外声明时,所有的闭包会引用相同的共享变 量。下例展示了 "outloop" 变量只有一个备份时会有的效果: vim9script var flist: list<func> def ClosureEg(n: number): void var outloop: number = 0 # outloop 在循环外声明! for i in range(n) outloop = i flist[i] = (): number => outloop # 闭包都引用同一变量 endfor echo range(n)->map((i, _) => flist[i]()) enddef ClosureEg(4) # 回显 [3, 3, 3, 3] 放置在列表里的所有闭包都引用相同的实例,该变量最终的值为 3。 不过,如果变量在循环内声明,每个闭包会获得自己专有的备份,如下例所示: vim9script var flist: list<func> def ClosureEg(n: number): void for i in range(n) var inloop: number = i # inloop 在循环内声明 flist[i] = (): number => inloop # 闭包引用不同的 inloop 实例 endfor echo range(n)->map((i, _) => flist[i]()) enddef ClosureEg(4) # 回显 [0, 1, 2, 3] 让每个闭包有自己的上下文的另一方法是调用函数,在其中定义闭包: vim9script def GetClosure(i: number): func var infunc: number = i return (): number => infunc enddef var flist: list<func> def ClosureEg(n: number): void for i in range(n) flist[i] = GetClosure(i) endfor echo range(n)->map((i, _) => flist[i]()) enddef ClosureEg(4) # 回显 [0, 1, 2, 3] E1271 闭包必须在其定义所在的上下文里进行编译,以便找到在该上下文里的变量。大多数情况 下这没有问题,但在编译函数后,再用 :breakadd 标记该函数进行调试时会出问题。 为了避免这些问题,请在编译外层函数前就定义好断点。 E1248 有些情况下,如捕获局部变量的 Vim9 闭包先转换为字符串再执行时,会出错。这是因为 字符串执行的上下文不能访问闭包定义所在的上下文里的局部变量。例如: vim9script def F_1248(): void var n: number var F: func = () => { n += 1 } try execute printf("call %s()", F) catch echo v:exception endtry enddef F_1248() # Vim(call):E1248: 从无效上下文中调用了闭包 在 Vim9 脚本里,循环变量在循环结束后不再有效。下例中定时器会回显 0 到 2,每个 数值一行。不过,如果在 :endfor 之后试图使用 "n" 变量,会报错 E121 : vim9script for n in range(3) var nr: number = n timer_start(1000 * n, (_) => { echowindow nr }) endfor try echowindow n catch echo v:exception endtry 备注: 定时器中 :echowindow 很有用,因为它会使得信息在弹出中出现, 触发时不会干扰用户正在进行中的操作。 把 :function 转换为 :def convert_legacy_function_to_vim9 convert_:function_to_:def 要把 :function 转换为 :def 函数需要不少改动。这里列出一些需要的改动: - 把用于声明变量的 let 改为 varconstfinal 之一,并删除所有 script-variable 的 "s:" 前缀。 - 把 funcfunction 改为 def 。 - 把 endfuncendfunction 改为 enddef 。 - 为每个函数参数加上合适的类型 (或者 "any")。 - 删除所有 function-argument 的 "a:" 前缀。 - 删除不再适用的选项,如 :func-range:func-abort:func-dict:func-closure 。 - 如果函数有返回值,加上返回类型。(如果函数不返回任何值,最好加上 "void"。) - 删除那些在 Vim9 里不再需要的作为续行符的反斜杠。 - 给 g:b:w:t:l: 变量赋值时删除 let 。 - 使用 Vim9 脚本语法改写 lambda 表达式 (见 vim9-lambda )。 - 改变注释的引导符,注释以 # (以空白前导) 开始,而不再是 "。 - 在表达式里必要的地方插入空格 (见 vim9-white-space )。 - 把用于字符串连接的 "." 改为 " .. "。(替代方案是使用 interpolated-string 。) 下面的老式 Vim 脚本和 Vim9 脚本示例展示了上述的所有区别。首先给出老式 Vim 脚本 版本: >vim let s:lnum=0 function Leg8(arg) abort let l:pre=['结果', \': '] let b:arg=a:arg let s:lnum+=2 let b:arg*=4 let l:result={pre->join(pre,'')}(l:pre) return l:result.(b:arg+s:lnum)"注释前不需空白 endfunction call Leg8(10)->popup_notification(#{time: 3000})" 弹出 '结果: 42' 等价的 Vim9 脚本: vim9script var lnum: number def Vim9(arg: number): string final pre = ['结果', ': '] b:arg = arg lnum += 2 b:arg *= 4 const RESULT: string = ((lpre) => join(lpre, ''))(pre) return RESULT .. (b:arg + lnum) # #-开头的注释前需要空白 enddef Vim9(10)->popup_notification({time: 3000}) # 弹出 '结果: 42' 备注: 此例也展示了 (和 :def 函数的改写无关): - 老式 #{} 里的 "#" 被删除 - 见 vim9-literal-dict - :call 被省略 (Vim9 脚本里允许,但不必要) 在表达式选项里调用 :def 函数 expr-option-function 若干选项如 'foldexpr' 的值是一个表达式,该表达式会先经过计算再取得需要的值。这 里的计算会有相当负担。一个尽量减少负担同时也使选项值保持简单的方法,是先定义一 个编译过的函数,然后设置选项为该函数不带参数的调用。例如: vim9script def MyFoldFunc(): string # 此模式匹配从行首 (^) 开始,后跟一个数位、一个句号、一个空格或制 # 表和一个非英文字符所组成的行,并且下一行必须为空行 return getline(v:lnum) =~ '^[[:digit:]]\.[[:blank:]][^\%u0001-\%u00ff]' && getline(v:lnum + 1)->empty() ? '>1' : '1' enddef set foldexpr=MyFoldFunc() set foldmethod=expr norm! zM 警告: 此脚本在本 vim9.cnx 帮助缓冲区的一级标题级别上,创建和应用折叠。(执行 脚本后,在普通模式下可用 zR 以打开所有的折叠。)

4. 类型 vim9-types 支持以下类型,这里也显示了它们对应的内部 v:t_TYPE 变量: number v:t_number string v:t_string func v:t_func func: {type} v:t_func func({type}, ...) v:t_func func({type}, ...): {type} v:t_func list<{type}> v:t_list dict<{type}> v:t_dict float v:t_float bool v:t_bool none v:t_none job v:t_job channel v:t_channel blob v:t_blob class v:t_class object v:t_object typealias v:t_typealias enum v:t_enum enumvalue v:t_enumvalue tuple<{type}> v:t_tuple tuple<{type}, {type}, ...> v:t_tuple tuple<...list<{type}>> v:t_tuple tuple<{type}, ...list<{type}>> v:t_tuple void E1031 E1186 这些类型可用于声明,但没有任何简单值会对应 "void" 类型。试图使用 void 作为值类 型会报错。示例: vim9script def NoReturnValue(): void enddef try const X: any = NoReturnValue() catch echo v:exception # E1031: 不能使用空值 try echo NoReturnValue() catch echo v:exception # E1186: 表达式不产生值: NoReturnValue() endtry endtry E1008 E1009 E1010 E1012 不合乎规格的声明和不匹配的类型会报错。下面例子会分别给出错误 E1008、E1009、 E1010 和 E1012: vim9cmd var l: list vim9cmd var l: list<number vim9cmd var l: list<invalidtype> vim9cmd var l: list<number> = ['42'] Vim 没有数组类型,而用列表或元组代替。这些类型也可使用字面量 (常量)。下列中, [5, 6] 是列表常量,而 (7, ) 是元组常量。回显的列表也是一个列表常量: vim9script var l: list<number> = [1, 2] var t: tuple<...list<number>> = (3, 4) echo [l, t, [5, 6], (7, )] tuple-type 元组类型可用下列的方式声明: tuple<number> 带单个类型 Number 的项目的元组 tuple<number, string> 带两个类型 NumberString 的项目的元组 tuple<number, float, bool> 带三个类型 NumberFloatBoolean 的项 目的元组 tuple<...list<number>> 带可变参数零或多个类型 Number 的项目的元组 tuple<number, ...list<string>> 带一个类型 Number 的项目,后跟零或多个类型 String 的项目的元组 示例: var myTuple: tuple<number> = (20,) var myTuple: tuple<number, string> = (30, 'vim') var myTuple: tuple<number, float, bool> = (40, 1.1, true) var myTuple: tuple<...list<string>> = ('a', 'b', 'c') var myTuple: tuple<number, ...list<string>> = (3, 'a', 'b', 'c') variadic-tuple E1539 可变参数元组有零或多个相同类型的项目。可变参数元组必须以列表类型结尾。例如: var myTuple: tuple<...list<number>> = (1, 2, 3) var myTuple: tuple<...list<string>> = ('a', 'b', 'c') var myTuple: tuple<...list<bool>> = () vim9-func-declaration E1005 E1007 vim9-partial-declaration vim9-func-type 偏函数和函数可用以下方式声明: func 任何类型的函数引用,不检查参数和返回值类型 func: void 任何数量和类型的参数,无返回值 func: {type} 任何数量和类型的参数,特定返回类型 func() 无参数的函数,无返回值 func(): void 同上 func(): {type} 无参数的函数,指定返回类型 func({type}) 指定参数类型的函数,无返回值 func({type}): {type} 指定参数类型和返回类型的函数 func(?{type}) 指定可选参数类型的函数,无返回值 func(...list<{type}>) 带可变数目的指定类型的参数列表的函数,无返回值 func({type}, ?{type}, ...list<{type}>): {type} 带以下的函数: - 必选参数的类型 - 可选参数的类型 - 可变数目参数列表的类型 - 返回类型 如果返回类型为 "void",函数什么都不返回。 引用也可是 Partial ,此时它保存了额外参数和/或字典,而这些对调用者是不可见 的。因为调用方式相同,声明方式也是一致的。以下使用偏函数的交互示例会提示输入圆 的半径,并返回其面积,精确到两位小数: vim9script def CircleArea(pi: float, radius: float): float return pi * radius->pow(2) enddef const AREA: func(float): float = CircleArea->function([3.14]) const RADIUS: float = "输入半径: "->input()->str2float() echo $"\n半径为 {RADIUS} 的圆的面积为 " .. $"{AREA(RADIUS)} (π 精确到两位小数)" vim9-typealias-type 可用 :type 定义定制类型 ( typealias )。定制类型必须以大写字母开头 (避免与当 前或之后新增的内建类型名字起冲突),这和用户函数类似。下例创建完全平方的列表, 并报告其 type() (14,typealias 的类型值) 和 typename() : vim9script type Ln = list<number> final perfect_squares: Ln = [1, 4, 9, 16, 25] echo "Typename (Ln): " .. $"type() 为 {Ln->type()} 而 typename() 为 {Ln->typename()}" E1105 typealias 自身不能转换为字符串: vim9script type Ln = list<number> const FAILS: func = (): string => { echo $"{Ln}" # E1105: 无法将 typealias 转换为字符串 } vim9-class-type vim9-interface-type vim9-object-type classobjectinterface 都可用作类型。以下的交互示例会提示输入浮点 数,并返回两种不同形状的面积。此示例也会同时报告类、对象和界面的 type()typename() : vim9script interface Shape def InfoArea(): tuple<string, float> endinterface class Circle implements Shape var radius: float def InfoArea(): tuple<string, float> return ('圆 (π × r²)', 3.141593 * this.radius->pow(2)) enddef endclass class Square implements Shape var side: float def InfoArea(): tuple<string, float> return ('正方形 (s²)', this.side->pow(2)) enddef endclass const INPUT: float = "输入浮点数: "->input()->str2float() echo "\n形状的面积:" var myCircle: object<Circle> = Circle.new(INPUT) var mySquare: object<Square> = Square.new(INPUT) final shapes: list<Shape> = [myCircle, mySquare] for shape in shapes const [N: string, A: float] = shape.InfoArea() echo $"\t- {N} 的面积是 {A}" endfor echo "\n\t\ttype()\ttypename()\n\t\t------\t----------" echo $"Circle\t\t{Circle->type()}\t{Circle->typename()}" echo $"Square\t\t{Square->type()}\t{Square->typename()}" echo $"Shape\t\t{Shape->type()}\t{Shape->typename()}" echo $"MyCircle\t{myCircle->type()}\t{myCircle->typename()}" echo $"MySquare\t{mySquare->type()}\t{mySquare->typename()}" echo $"shapes\t\t{shapes->type()}\t{shapes->typename()}" vim9-enum-type vim9-enumvalue-type enum 也可用作类型 ( v:t_enum )。保存枚举值的变量在运行时会有枚举值类型 ( v:t_enumvalue )。以下的交互示例会提示输入一个字符,并返回正方形或菱形的相关 信息。此示例也会同时报告枚举和枚举值的 type()typename() : vim9script enum Quad Square('四条', '只有'), Rhombus('相对的', '没有') var eq: string var ra: string def string(): string return $"\n{this.name} 有" .. $"{this.eq}等边,而且{this.ra}直角\n\n" enddef endenum echo "菱形 (r) 还是正方形 (s)?" var myQuad: Quad = getcharstr() =~ '\c^R' ? Quad.Rhombus : Quad.Square echo myQuad.string() .. "\ttype()\ttypename()" echo $"Quad \t{Quad->type()} \t{Quad->typename()}" echo $"myQuad\t{myQuad->type()}\t{myQuad->typename()}" 备注: 此脚本使用内建方法 "string()" ( object-string() )。 Quad 和 myQuad 的 typename() 相同 ("enum<Quad>"),但 type() 不 同 (myQuad 返回 16,枚举值的类型值,而 Quad 返回 15,枚举的类型 值)。 变量类型和类型转换 variable-types Vim9 脚本或 :def 函数中声明的变量都有类型,或者通过显式指定,或者通过初始化 推导得出。 全局、缓冲区、窗口和标签页变量没有特定类型。因而,其值可在任何时候改变,类型也 会同时随之改变。为此,编译后的代码假定这些变量使用 "any" 类型。 如果希望有更严格的类型检查,这会有问题。例如,声明列表时: var l: list<number> = [1, b:two] 因为 Vim 不知道 "b:two" 的类型,表达式类型会因此变成 list<any>。这种情况下,运 行时会在赋值前检查列表是否匹配声明的类型。 type-casting 要进行更精确的类型检查,可使用类型转换。这样,在构造列表前会先检查变量的类型, 而不是在赋值前检查列表项是否匹配声明的类型。例如: var l: list<number> = [1, <number>b:two] 这里的类型转换会检查 "b:two" 是否为数值,如果不是会报错。 下例展示其中的区别。在函数引用变量 "NTC" 里,Vim 推断表达式 "[1, b:two]" 的类 型为 list<any>,然后会检查它是否可以赋值给返回值类型 list<number>。而在函数引 用变量 "TC" 里,类型转换会使 Vim 先检查 "b:two" 是否为 <number> 类型: vim9script b:two = '2' const NTC: func = (): list<number> => { return [1, b:two] } disassemble NTC # 3 CHECKTYPE list<number> stack [-1] try NTC() catch echo v:exception .. "\n\n" # 预期 list<number> 但得到 list<any> endtry const TC: func = (): list<number> => { return [1, <number>b:two] } disassemble TC # 2 CHECKTYPE number stack [-1] try TC() catch echo v:exception # 预期 number 但得到 string endtry 备注: 可以从错误信息的差异看出,类型检查是在不同的阶段发生的。 E1104 类型转换的语法是 "<{type}>"。如果缺失了 "<" ( E121 ) 或者 ">" ( E1104 ),会报 错。另外,"<" 之后 ( E15 ) 或 ">" 之前 ( E1068 ) 不能有空格,以避免和小于号以 及大于号操作符混淆。 虽然类型转换会强制类型检查,但是它不会更改变量的值以及类型。如果要改变类型,可 用 string() 函数来转换为字符串型,或者用 str2nr() 来把字符串转换为数值。 应用在链式表达式上时,类型转换必须和最终的结果兼容。例如: vim9script # 以下这些类型转换可行 echo <list<any>>[3, 2, 1]->extend(['Go!']) echo <string>[3, 2, 1]->extend(['Go!'])->string() echo <tuple<...list<number>>>[3, 2, 1]->list2tuple() # 以下类型转换会失败 echo <number>[3, 2, 1]->extend(['Go!'])->string() E1272 如果在不期待类型的上下文中使用类型,会报错 E1272。例如: :vim9cmd echo islocked('x: string') 备注: 本例必须在 Vim 的命令行上运行,不能用 :source E1363 如果类型不完整,例如对象所属的类未知,报错 E1363。例如: vim9script var E1363 = null_class.member # E1363: Incomplete type 另一个和 null 对象相关的错误是 E1360 : vim9script var obj = null_object var E1360 = obj.MyMethod() # E1360: Using a null object 类型推导 type-inference 显式声明类型有很多优点,包括精确的类型检查和更清晰的错误信息。不过,Vim 在类型 省略时常常可以自动推断类型。例如,以下每个变量的类型都可以被推导出来, type()typename() 会回显推断后的类型: vim9script echo "\t type()\t typename()" var b = true | echo $"{b} \t {b->type()} \t {b->typename()}" var f = 4.2 | echo $"{f} \t {f->type()} \t {f->typename()}" var l = [1, 2] | echo $"{l} \t {l->type()} \t {l->typename()}" var n = 42 | echo $"{n} \t {n->type()} \t {n->typename()}" var s = 'yes' | echo $"{s} \t {s->type()} \t {s->typename()}" var t = (42, ) | echo $"{t} \t {t->type()} \t {t->typename()}" 列表、元组和字典的类型来自其值的公共类型。如果所有值为相同类型,则使用该类型。 如果有不同类型的混合,则用 "any" 类型。下例中,每个常量回显的 typename() 展 示了这些情况: vim9script echo [1, 2]->typename() # list<number> echo [1, 'x']->typename() # list<any> echo {ints: [1, 2], bools: [false]}->typename() # dict<list<any>> echo (true, false)->typename() # tuple<bool, bool> 函数引用的公共类型里,如果参数数目不尽相同,会用 "(...)" 来指明参数数目不确 定。下例展示了 "list<func(...): void>": vim9script def Foo(x: bool): void enddef def Bar(x: bool, y: bool): void enddef var funclist = [Foo, Bar] echo funclist->typename() Vim9 脚本里脚本局部变量会进行类型检查,即使对在老式函数里声明的变量也会。 例如: vim9script var my_local = (1, 2) function Legacy() let b:legacy = [1, 2] endfunction Legacy() echo $"{my_local} 的类型是 {my_local->type()} ({my_local->typename()})" echo $"{b:legacy} 的类型是 {b:legacy->type()} ({b:legacy->typename()})" E1013 一个列表、元组或字典声明类型后,该类型会与之捆绑。类似地,即使未显式声明类型, Vim 也会用推断出来的类型与之捆绑。不管哪种情况,如果在此之后有表达式试图改变类 型,会报错 E1013。下例展示了类型推断和 E1013: vim9script var lb = [true, true] # 两个 bool,所以 Vim 会推断为 list<bool> 类型 echo lb->typename() # 回显 list<bool> lb->extend([0]) # E1013 参数 2: 类型不匹配,... 如果要接受多种类型的列表,可以显式使用 <any>,或者声明时用空列表来初始化 (或者 两者兼有,如 `list<any> = []`)。示例: vim9script final la: list<any> = [] echo la->extend(['two', 1]) final le = [] echo le->extend(la) 类似地,要接受多种类型的字典: vim9script final da: dict<any> = {} echo da->extend({2: 2, 1: 'One'}) final de = {} echo de->extend(da)->string() 另外,虽然元组本身是只读的,但可以通过 "any" 或空元组来实现能接受多种类型的元 组连接操作: vim9script var t_any: tuple<...list<any>> = (3, '2') t_any = t_any + (true, ) echo t_any var t_dec_empty = () t_dec_empty = t_dec_empty + (3, '2', true) echo t_dec_empty 如果列表常量或字典常量没有和变量绑定,则其类型可变,如下例所示: vim9script echo [3, 2, 1]->typename() # list<number> echo [3, 2, 1]->extend(['Zero'])->typename() # list<any> echo {1: ['One']}->typename() # dict<list<string>> echo {1: ['One']}->extend({2: [2]})->typename() # dict<list<any>> 更严格的类型检查 type-checking 在老式 Vim 脚本中,在本应是数值的地方使用了字符串,Vim 会自动转换为数值。这对 像 "123" 这样实际为数值的情形很方便,但对不以数值开始的字符串会出现意料不到的 问题 (且不报错)。这种特性常常会导致很难发现的漏洞。例如,在老式 Vim 脚本里,下 例会回显 "1": >vim echo 123 == '123' < 不过,如果不小心多了一个空格,就会回显 "0": >vim echo 123 == ' 123' < E1206 在 Vim9 脚本中,类型检查会更严格。只要使用的值匹配期待的类型,绝大多数地方会和 以前一样正常工作。例如,在老式 Vim 脚本和 Vim9 脚本里,在期待字典的地方试图用 其他类型都会报错: >vim echo [8, 9]->keys() vim9cmd echo [8, 9]->keys() # E1206: 参数 1 需要字典 < E1023 E1024 E1029 E1030 E1174 E1175 E1210 E1212 不过,有些情况下, Vim9 脚本会报错,而老式脚本里不会,这会导致后向不兼容。下面 的示例展示了各种不兼容的情况。先给出老式 Vim 脚本中不报错时的行为。然后再展示 相同的命令在 Vim9 脚本里触发的错误。 - 期待布尔值时使用了数值 (0 或 1 除外): >vim echo v:version ? v:true : v:false vim9cmd echo v:version ? true : false # E1023: 将整数作布尔值使用... < - 期待字符串时使用了数值: >vim echo filter([1, 2], 0) vim9cmd echo filter([1, 2], 0) # E1024: 将整数作字符串使用 < - 期待数值时不使用数值: >vim " 此例中,Vim 脚本把 v:false 视作 0 function Not1029() let b:l = [42] | unlet b:l[v:false] endfunction call Not1029() | echo b:l < vim9script def E1029(): void b:l = [42] | unlet b:l[false] enddef E1029() # E1029: 预期 number 但得到 bool - 将字符串用作数值: >vim let b:l = [42] | unlet b:l['#'] | echo b:l vim9cmd b:l = [42] | vim9cmd unlet b:l['#'] # E1030: 将字符串作整... < - 参数需要字符串时不使用字符串: echo substitute('Hallo', 'a', 'e', v:true) vim9cmd echo substitute('Hallo', 'a', 'e', true) # E1174: 需要字符串 - 参数需要非空字符串时使用空串: echo exepath('') vim9cmd echo exepath('') # E1175: 参数 1 需要非空字符串 - 参数需要数值时不使用数值: >vim echo gettabinfo('a') vim9cmd echo gettabinfo('a') # E1210: 参数 1 需要整数 < - 参数需要布尔值时不使用布尔值: >vim echo char2nr('¡', 2) vim9cmd echo char2nr('¡', 2) # E1212: 参数 2 需要布尔值 < - 赋值需要数值时不使用数值 ( E521 ): >vim let &laststatus='2' vim9cmd &laststatus = '2' < - 赋值需要字符串时不使用字符串 ( E928 ): >vim let &langmenu = 42 vim9cmd &langmenu = 42 # E928: 需要字符串 < - 将 Special'is' 作比较有些情况下会失败 ( E1037 , E1072 ): >vim " 回显 1,因为两次比较均返回 true echo v:null is v:null && v:none is v:none " 回显 0,因为所有比较均返回 false echo v:none is v:null || v:none is 8 || v:true is v:none " Vim9 脚本里以下均报错 vim9cmd echo v:null is v:null # E1037: 不能对 special 使用 'is' vim9cmd echo v:none is v:none # E1037: 不能对 special 使用 'is' vim9cmd echo v:none is v:null # E1037: 不能对 special 使用 'is' vim9cmd echo v:none is 8 # E1072: 不能比较 special 和 number vim9cmd echo v:true is v:none # E1072: 不能比较 bool 和 special < 备注: 在最后两例中,Vim9 脚本中如果用 v:none 会报错,但若改用 null (等同于 v:null - 见 v:null ) 则会返回 false : vim9script echo null is 8 # false echo true is null # false - 期待布尔值时使用字符串 ( E1135 ): >vim echo '42' ? v:true : v:false vim9cmd echo '42' ? true : false # E1135: 将字符串作布尔值使用: "42" < - 期待数值时使用布尔值 ( E1138 ): >vim let &laststatus=v:true vim9cmd &laststatus = true < - 参数需要字符串时不使用字符串 ( E1174 ) >vim echo substitute('Hallo', 'a', 'e', v:true) vim9cmd echo substitute('Hallo', 'a', 'e', true) # E1174: 需要字符串 < 一个后果是,如果项目类型已声明或被推导得到,传递给 map() 的列表或字典不能再 更改其项目类型。下例中,Vim9 脚本里会报错但老式 Vim 脚本里不会: >vim " 老式 Vim 脚本会将 s:mylist 修改为 ['item 0', 'item 1'] let s:mylist = [0, 1] call map(s:mylist, {i -> $"item {i}"}) echo s:mylist < vim9script var mylist = [0, 1] # Vim 推导 mylist 为 list<number> map(mylist, (i, _) => $"item {i}") # E1012: 类型不匹配... 此错误之所以发生,是因为 map() 会试图修改列表项为字符串值,而这和列表已声明 的类型不相符。 为了避免这个问题,可用 mapnew() 代替 map() 。它会创建新列表,未指定类型时 Vim 会自动推导其类型。下例分别展示了推导出的和显式声明的类型: vim9script var mylist = [0, 1] var infer = mylist->mapnew((i, _) => $"item {i}") echo [infer, infer->typename()] var declare: list<string> = mylist->mapnew((i, _) => $"item {i}") echo [declare, declare->typename()] 这里的关键概念是,带有已声明或推导得到类型的变量,不能随着包含该变量的容器类型 改变而改变。不过,下面情况可以接受类型的 "改变": - 使用容器常量 (不和任何变量绑定),或者 - 使用 copy()deepcopy() 的方法链式调用。 这两种情况在下例中都有所展示: vim9script # 列表常量 echo [1, 2]->map((_, v) => $"#{v}") echo [1, 2]->map((_, v) => $"#{v}")->typename() # 用 deepcopy() 的方法链式调用 var mylist = [1, 2] echo mylist->deepcopy()->map((_, v) => $"#{v}") echo mylist->deepcopy()->map((_, v) => $"#{v}")->typename() echo mylist 这里背后的逻辑是,列表被传递和修改时,如果类型已被声明或推导,必须保证已经声 明/推导得到的类型保持不变,这样才能依靠类型来匹配已声明的类型。而对列表常量或 被完整复制的列表,不需要类型安全,因为原先的列表未被修改 (如果上例中 "echo mylist" 所示)。 如果项目类型未声明而被推导为 "<any>",即使之后所有的项目变成了相同类型,推导类 型不会改变。不过,使用 mapnew() 时,新的推导会使新列表正确反映新类型。 例如: vim9script # list<any> var mylist = [1, '2'] # 混合类型,也就是 list<any> echo (mylist, mylist->typename()) # ([1, '2'], 'list<any>') mylist->map((_, v) => $"item {v}") # 所有值都成了字符串类型 echo (mylist, mylist->typename()) # 都是字符串,但还是 list<any> # mapnew() var newlist = mylist->mapnew((_, v) => v) echo (newlist, newlist->typename()) # newlist 变成了 list<string> extend()extendnew() 情况也类似,同样地,前者可用列表常量,所以下例没问 题: vim9cmd echo [1, 2]->extend(['3']) # [1, 2, 3] 但下例不行: vim9script var mylist: list<number> = [1, 2] echo mylist->extend(['3']) # E1013: 参数 2: 类型不匹配... 要扩充类型确定的已有列表,必须用 extendnew() ,除非扩充内容和列表的现有类型匹 配 (也包括 "any")。例如,要先扩充一个相同类型的项目,再扩充一个不同类型的项 目,可用: vim9script var mylist: list<number> = [1, 2] mylist->extend([3]) echo mylist->extendnew(['4']) # [1, 2, 3, '4'] E1158 Vim9 脚本不支持 flatten() ,因为它的目的就是修改类型。即使对列表常量也是如此 (这一点和 map()extend() 不同)。必须使用 flattennew() 代替: vim9cmd [1, [2, 3]]->flatten() # E1158: Vim9 中不能用 flatten vim9cmd echo [1, [2, 3]]->flattennew() # [1, 2, 3] 赋值给指定参数的函数引用 (见 vim9-func-declaration ) 会严格检查参数的类型。 下例可以正常工作: vim9script var F_name_age: func(string, number): string F_name_age = (n: string, a: number): string => $"Name: {n}, Age: {a}" echo F_name_age('Bob', 42) 而下例会报错 E1012 (类型不匹配): vim9script var F_name_age: func(string, number): string F_name_age = (n: string, a: string): string => $"Name: {n}, Age: {a}" 可变数目参数必须使用相同类型,如下例所示: vim9script var Fproduct: func(...list<number>): number Fproduct = (...v: list<number>): number => reduce(v, (a, b) => a * b) echo Fproduct(3, 2, 4) # 回显 24 <any> 可用于需要混合类型的函数: vim9script var FlatSort: func(...list<any>): any FlatSort = (...v: list<any>) => flattennew(v)->sort('n') echo FlatSort(true, [[[5, 3], 2], 4]) # 回显 [true, 2, 3, 4, 5] 注意: 使用 <any> 的匿名函数赋值给的函数引用不会跳过类型检查。函数引 用的声明类型仍然适用,如下例所示,如果类型不匹配,仍然会在运行 时或编译时报错: vim9script var FuncSN: func(string): number FuncSN = (v: any): number => v->str2nr() echo FuncSN('162')->nr2char() # 回显 ¢ echo FuncSN(162)->nr2char()) # E1013 (运行时报错) vim9script var FuncSN: func(string): number FuncSN = (v: any): number => v->str2nr() def FuncSNfail(): void echo FuncSN('162')->nr2char() # 没有回显,因为 ... echo FuncSN(162)->nr2char() # 编译时报错 enddef FuncSNfail() 没有指定参数的函数引用不进行参数类型检查。下例中 FlexArgs 第一次调用时接受字符 串参数,第二次调用时接受列表参数: vim9script var FlexArgs: func: string FlexArgs = (s: string): string => $"It's countdown time {s}..." echo FlexArgs("everyone") FlexArgs = (...values: list<string>): string => join(values, ', ') echo FlexArgs('3', '2', '1', 'GO!') E1211 E1217 E1218 E1219 E1220 E1221 E1222 E1223 E1224 E1225 E1226 E1228 E1235 E1238 E1251 E1253 E1256 E1297 E1298 E1301 E1528 E1529 E1530 E1531 E1534 大多数内建函数会检查类型,以便查找错误。下面的单行 :vim9 调用内建函数的命令 展示了许多类型检查相关的错误: vim9 9->list2blob() # E1211: 参数 1 需要列表 vim9 9->ch_close() # E1217: 参数 1 需要通道或任务 vim9 9->job_info() # E1218: 参数 1 需要任务 vim9 [9]->cos() # E1219: 参数 1 需要浮点数或整数 vim9 {}->remove([]) # E1220: 参数 1 需要字符串或整数 vim9 null_channel->ch_evalraw(9) # E1221: 参数 1 需要字符串或 blob vim9 9->col() # E1222: 参数 1 需要字符串或列表 vim9 9->complete_add() # E1223: 参数 1 需要字符串或字典 vim9 setbufline(9, 9, {}) # E1224: 参数 3 需要字符串或整数或列 vim9 9->count(9) # E1225: String, List, Tuple or Dict vim9 9->add(9) # E1226: 参数 1 需要列表或 blob vim9 9->remove(9) # E1228: 需要列表、字典或 blob vim9 getcharstr('9') # E1235: Bool or number required for vim9 9->blob2list() # E1238: 需要 blob vim9 9->filter(9) # E1251: List, Tuple, Dictionary, Bl vim9 9->reverse() # E1253: String, List, Tuple or Blob vim9 9->call(9) # E1256: 参数 1 需要字符串或函数 vim9 null_dict->winrestview() # E1297: Non-NULL Dictionary require vim9 {}->prop_add_list(null_list) # E1298: Non-NULL List required for vim9 {}->repeat(9) # E1301: String, Number, List, Tuple vim9 9->index(9) # E1528: List or Tuple or Blob vim9 9->join() # E1529: List or Tuple required for vim9 9->max() # E1530: List or Tuple or Dictionary vim9 9->get(9) # E1531: Argument of get() must be a vim9 9->tuple2list() # E1534: Tuple required for argument 保留作将来使用: E1227 E1250 E1252 E1227: 参数 %d 需要列表或字典 E1250: %s 的参数必须是列表、字符串、字典或 blob E1252: 参数 %d 需要字符串、列表或 blob 变量种类、缺省和 null 处理 variable-categories null-variables 变量有以下三个种类: 原始类型 number、float、boolean 容器类型 string、blob、list、tuple、dict 特殊类型 function、job、channel、user-defined-object 声明没有初始块的变量时,必须显式提供类型。每个种类有不同的缺省初始块语法。 原始类型缺省值为类型特定的初始值。该初始值的 empty() 值为真,但不等于 null : vim9script var n: number | echo [n, n->empty(), n == null] # [0, 1, false] var f: float | echo [f, f->empty(), f == null] # [0.0, 1, false] var b: bool | echo [b, b->empty(), b == null] # [false, 1, false] 容器类型缺省值为空容器。只有字符串类型的缺省值等于 null : vim9script var s: string | echo [s, s->empty(), s == null] # ['', 1, true] var z: blob | echo [z, z->empty(), z == null] # [0z, 1, false] var l: list<string> | echo [l, l->empty(), l == null] # [[], 1, false] var t: tuple<any> | echo [t, t->empty(), t == null] # [(), 1, false] var d: dict<number> | echo [d, d->empty(), d == null] # [{}, 1, false] 特殊类型缺省值为 null : vim9script var F: func | echo [F, F == null] # [function(''), true] var j: job | echo [j, j == null] # ['no process', true] var c: channel | echo [c, c == null] # ['channel fail', true] class Class endclass var o: Class | echo [o, o == null] # [object of [unknown], true] enum Enum endenum var e: Enum | echo [e, e == null] # [object of [unknown], true] 备注: empty() 说明了作业、通道和对象类型何时为空。 Vim 没有你熟悉的 null 值;但它有若干预定义的 null_<type> 值,如 null_stringnull_listnull_job 。原始类型则没有相应的 null_<type>。 null_<type> 的典型用法有: - 用于清除变量,释放其资源, - 用于作为函数定义里的参数缺省值 (例见 null_blob ),或 - 赋值给容器或特殊类型变量,用于之后和 null 作比较 (例见 null-compare )。 对于像 job 这样的特殊类型变量而言,null_<type> 可用于清理资源。例如: vim9script var mydate: list<string> def Date(channel: channel, msg: string): void mydate->add(msg) enddef var myjob = job_start([&shell, &shellcmdflag, 'date'], {out_cb: Date}) echo [myjob, myjob->job_status()] sleep 2 echo $"当前日期时间是 {mydate->join('')}" echo [myjob, myjob->job_status()] myjob = null_job # 清除变量;并释放作业资源。 echo myjob 对于容器类型变量而言,将空容器值赋给变量也可以用来清理资源。例如: vim9script var perfect: list<number> = [1, 4] perfect->extend([9, 16, 25]) perfect = [] echo perfect 使用空容器而不是 null_<type> 来清除容器类型变量可以避免 null-anomalies 描述 的 null 的复杂性。 容器类型变量和特殊类型变量使用不同的初始块语法。对于容器类型而言: - 未初始化的容器类型变量缺省为空,但不等于 null (未初始化的字符串除 外) - 用 []、()、{}、""、或 0z 初始化的容器为空,但不等于 null 。 - 用 null_<type> 初始化的容器缺省为空,且该值等于 null 。 下例中,未初始化的列表 ("lu") 和 [] 初始化的列表 ("li") 是等价的,也无法区分, 而 "ln" 是 null 容器,它与空容器相似但并不等价 (见 null-anomalies )。 vim9script # 未初始化: 空容器,不等于 null var lu: list<any> echo ['lu', $"empty={lu->empty()}", $"null={lu == null}"] # 已初始化: 空容器,不等于 null var li: list<any> = [] echo ['li', $"empty={li->empty()}", $"null={li == null}"] # 已初始化: 空容器,等于 null var ln: list<any> = null_list echo ['ln', $"empty={ln->empty()}", $"null={ln == null}"] 特殊类型变量缺省等于 null。下例中作业的不同初始化方式等价且无法区分: vim9script var j1: job var j2: job = null_job var j3 = null_job echo (j1 == j2) == (j2 == j3) # true (等价,无法区分) 声明列表、元组或字典时,如果项目类型未指定且无法推导,类型会缺省判定为 "any": vim9script var [t1, t2] = [(), null_tuple] echo $'t1 是 {t1->typename()} 而 t2 也是 {t2->typename()}' 元组和函数 (或者偏函数) 还有若干不同的声明方式。见 tuple-typevariadic-tuplevim9-func-declaration null-compare 对于常见的和 null 比较的语法,如果要求空容器和 null 容器不相等,比较时不要使用 null_<type>。这是因为,Vim9 脚本里,虽然 null_<type> == null ,但是,将: - 空容器和 null 比较会返回 false ,但将 - 空容器和 null_<type> 比较会返回 true 。 所以,请和 null 相比较,而不是 null_<type>。示例: vim9script var bonds: dict<list<string>> = {g: ['007', '008'], o: ['007', '009']} def Search(query: string): list<string> return query == "\r" ? null_list : bonds->get(query, []) enddef echo "金手指 (g) 还是八爪女 (o)?: " const C: string = getcharstr() var result: list<string> = C->Search() if result == null # <<< _此处不要使用_ null_list! echo "错误: 没有输入" else echo result->empty() ? $"没有 '{C}' 的匹配" : $"{result}" endif 备注: 使用 "result == null_list" 而非 "result == null" 会无法区分错 误 (未输入任何选择) 和合法 (但找不到匹配) 的结果,因为 [] == null_list 而 [] != null。 概念上,可以将 null_<type> 构造想像为通用的 null 容器和有类型的 empty 容器 的一种混合/桥梁,兼有两种属性。下一小节会给出关于比较结果的更多细节。 null-details null-anomalies 本小节描述使用 null 和 null_<type> 的问题;下面列举 null 比较各种可能的结果。 有些情况下,如果熟悉了 vim9 的 null 语法,程序员还是可以选择使用 null_<type> 来进行比较和/或用于其他情景。 在文档的其他地方有说过: "null 值和空值处理方式通常相同,但不绝对”。例如,不能 在 null 容器上添加元素: vim9script var le: list<any> = [] le->add('Okay') # le 现在是 ['Okay'] var ln = null_list ln->add("E1130") # E1130: 不能加到 null 列表 如同 null-compare 已经解释过的, null 、null_<type>empty 容器三者之间 不满足传递关系。下例简单回顾一下: vim9cmd echo (null_dict == {}, null_dict == null, {} != null) 唯一的例外是未初始化的字符串。它等于 null (和 null_string 是同一实例)。 'is' 操作符 ( expr-is ) 可用于判断字符串是否未初始化: vim9script var s: string echo s == null_string # true echo s is null_string # true (相同实例) echo s == null # true (可能意想不到) echo s is null # false (并非相同实例) 其它容器类型的行为则有所不同,用 'is' 和相应的 null_<type> 比较时,会返回 false : vim9script var d: dict<any> echo d == null_dict # true echo d is null_dict # false (并非相同实例) echo d == null # false (意料之中) echo d is null # false (并非相同实例) 这里关键的区别是,未初始化的字符串的实现是 null_string ,而未初始化的列表、字 典、元组或 blob 的实现是空容器 (分别是 []、{}、() 和 0z)。所以,这些类型的未初 始化值和它们相应的 null_<type> 相等但并非相同实例,如下例所示: vim9script var t: tuple<any> echo t == null_tuple # true echo t is null_tuple # false 不过,用 null_<type> 初始化的变量不仅和 null_<type> 相等,也和 null 相等。 示例: vim9script var t: tuple<any> = null_tuple echo t == null_tuple # true echo t is null_tuple # true echo t == null # true 未初始化的容器类型变量都不等于 null,只有未初始化的字符串除外,这一点,上面已 有解释。所以,下例都会回显 true : vim9script var b: blob | echo b != null var d: dict<any> | echo d != null var l: list<any> | echo l != null var t: tuple<any> | echo t != null var s: string | echo s == null 未初始化的特殊类型变量都等于 null,所以,下例都会回显 true : vim9script var c: channel | echo c == null var F: func | echo F == null var j: job | echo j == null class Class endclass var nc: Class | echo nc == null enum Enum endenum var ne: Enum | echo ne == null 备注: 像作业这样的特殊类型变量缺省为 null,所以它们没有对应的空值。 用空值初始化的容器类型变量等于 null_<type>,所以,下例都会返回 true : vim9script var s: string = "" | echo s == null_string var b: blob = 0z | echo b == null_blob var l: list<any> = [] | echo l == null_list var t: tuple<any> = () | echo t == null_tuple var d: dict<any> = {} | echo d == null_dict 不过,用空值初始化的容器类型变量不等于 null,所以,下例都会返回 true : vim9script var s: string = "" | echo s != null var b: blob = 0z | echo b != null var l: list<any> = [] | echo l != null var t: tuple<any> = () | echo t != null var d: dict<any> = {} | echo d != null

generic-functions 5. 泛型函数 泛型函数允许使用相同的函数但带不同的类型参数,而保留对参数和返回值的类型检查。 这样可以提供类型安全性和代码重用性。 声明 generic-function-declaration E1553 E1554 泛型函数用紧跟函数名后的尖括号 "<" 和 ">" 里的类型参数声明其类型变量。多种类型 名以逗号分隔: def[!] {函数名}<{类型} [, {类型}]>([参数])[: {返回类型}] {函数体} enddef generic-function-example 然后在函数签名和函数体里,这些类型参数可以和任何其他类型一样被使用。下例把两个 列表组合为元组的列表: vim9script def Zip<T, U>(first: list<T>, second: list<U>): list<tuple<T, U>> const LEN: number = ([first->len(), second->len()])->min() final result: list<tuple<T, U>> = [] for i in range(LEN) result->add((first[i], second[i])) endfor return result enddef var n: list<number> = [61, 62, 63] var s: list<string> = ['a', 'b', 'c'] echo $"Zip example #1: {Zip<number, string>(n, s)}" echo $"Zip example #2: {Zip<string, number>(s, n)}" type-variable-naming E1552 type-parameter-naming 如上例所示,一般惯例是,类型变量使用单个大写字母 (如 T、U、A 等等)。虽然可以有 多于一个字母,但必须以大写字母开头。下例中,"Ok" 合法,但 "n" 不是: vim9script def MyFail<Ok, n>(): void enddef # E1552: Type variable name must start with an uppercase letter: n... E1558 E1560 函数可以必须作为泛型函数或常规函数之一来声明和使用 - 但不能同时是两者。下面的 Vim9 脚本显示这些错误: vim9script My1558<number>() # E1558: Unknown generic function: My1558 vim9script def My1560(): void enddef My1560<string>() # E1560: Not a generic function: My1560 E1561 类型参数名不能和其他标识符冲突: vim9script def My1561<D, E, D>(): D enddef # E1561: Duplicate type variable name: D vim9script enum E Yes, No endenum def My1041<E>(): E enddef # E0141: Redefining script item "E" 调用泛型函数 generic-function-call 要调用泛型函数,在函数名和参数列表之间的 "<" 和 ">" 里指定具体类型: MyFunc<number, string, list<number>>() 注意: 本小节有几个可用的示例,可以直接执行,包括 generic-function-example E1555 E1556 E1557 E1559 传递给函数的类型实参数目必须和声明中类型参数的数目匹配。不允许空类型列表。 示例: vim9script def My1555<>(): void enddef # E1555: Empty type list specified for generic function ... vim9script def My1556<T>(): void enddef My1556<bool, bool>() # E1556: Too many types specified for generic function ... vim9script def My1557<T, U>(): void enddef My1557<bool>() # E1557: Not enough types specified for generic function ... vim9script def My1559<T>(): T enddef My1559() # Vim(eval):E1559: Type arguments missing for generic function ... 任何 Vim9 类型 ( vim9-types ) 都可用于泛型函数的具体类型。 以下各处不接受空格: - 函数名和 "<" 之间 ( E1068 ) - ">" 和左括号 "(" 之间 ( E1068 ),或 - "<" 和 ">" 之间,分隔类型的逗号之后所需的空格除外 ( E1202 )。 和常规函数一样,泛型函数可被导出和导入。见 :export:import 。 泛型函数可在另一个常规或泛型函数里定义。示例: vim9script def Outer(): void # 返回列表的首个项目,或者缺省值 def FirstOrDefault<T, U>(lst: list<T>, default: U): any return lst->len() > 0 ? lst[0] : default enddef echo FirstOrDefault<string, bool>(['B', 'C'], false) # 回显 B echo FirstOrDefault<number, number>([], 42) # 回显 42 enddef Outer() 使用类型变量作为类型参数 类型变量也可被传递为类型参数。例如: vim9script # T 声明为类型参数 # 它可用作 'value' 参数类型和返回值类型 def Id<T>(value: T): T return value enddef # U 声明为类型参数 # 它可用作 'value' 参数类型和返回值类型 def CallId<U>(value: U): U # U 是类型变量,可被传递/用作类型参数 return Id<U>(value) enddef echo CallId<string>('I am') .. ' ' .. CallId<number>(42) 这可用于像字典列表或像下例那样的列表字典这样的复杂数据结构: vim9script def Flatten<T>(x: list<list<T>>): list<T> final result: list<T> = [] for inner in x result->extend(inner) endfor return result enddef const ENGLISH: list<dict<string>> = [{1: 'one'}, {2: 'two'}] const MANDARIN: list<dict<string>> = [{1: '壹'}, {2: '贰'}] const ARABIC_N: list<dict<number>> = [{1: 1}, {2: 2}] echo Flatten<dict<string>>([ENGLISH, MANDARIN]) echo Flatten<dict<any>>([ENGLISH, ARABIC_N]) 在 "Flatten<T>" 里,"T" 是声明的类型参数。而在函数的其他地方,"T" 是引用该类型 参数的类型变量。 泛型类方法 Vim9 类方法可以是泛型函数: vim9script class Config var settings: dict<any> def Get<T>(key: string): T return this.settings[key] enddef endclass var c: Config = Config.new({timeout: 30, debug: true}) echo c.Get<number>('timeout') echo c.Get<bool>('debug') E1432 E1433 E1434 基类的泛型类方法可被子类的泛型方法覆盖。两个方法的类型变量数量必须一致。而已具 体化的类方法则不能被泛型方法覆盖,反之亦然。 泛型函数引用 函数引用 ( Funcref ) 可为泛型函数。这样可以创建对应指定类型的函数工厂: vim9script # 匹配字符串里的指定字符或匹配列表里的字符十进制值。 # 注意: '*' 的十进制值 是 42 (U+002A) var c: string = "*" var char_dec: tuple<string, string> = (c, c->char2nr()->string()) def Matcher<T>(pattern: string): func(T): bool return (value: T): bool => match(value, pattern) >= 0 enddef var StringMatch = Matcher<string>(char_dec[0]) echo "*+"->StringMatch() # true (有 *) echo ",-"->StringMatch() # false var ListMatch = Matcher<list<number>>(char_dec[1]) echo [42, 43]->ListMatch() # true (有 42) echo [44, 45]->ListMatch() # false 编译和反汇编泛型函数 :defcompile 命令可用于编译泛型函数,须指定具体类型的列表: defcompile MyFunc<number, list<number>, dict<string>> :disassemble 命令可用于列出泛型函数生成的汇编指令列表: disassemble MyFunc<string, dict<string>> disassemble MyFunc<number, list<blob>> 限制和未来计划 目前,Vim 不支持: - 类型变量的类型推导: 调用泛型函数时所有的类型必须显式指定。 - 类型约束: 类型变量不能限定为指定的类或界面 (例如, `T extends SomeInterface`)。 - 缺省类型参数: 提供类型参数未显式指定时的的缺省类型。

6. 命名空间、导入和导出 vim9script vim9-export vim9-import 可编写 Vim9 脚本,被其它脚本导入。具体地说,有意地导出若干项目,使之被其它脚本 所用。导出项目的脚本被其它脚本导入,后者就可以使用那些导出项目了。导出脚本的其 余项目依然局部于脚本而不能被导入脚本访问。 此机制的存在是为了编写被其它脚本执行 (导入) 的脚本,同时保证其它脚本只能访问你 要它们访问的项目。这也避免了使用全局命名空间,后者有名字冲突的风险。比如当有两 个功能相近的插件的时候。 可显式使用全局命名空间来作弊。这只应该用于真正需要全局的情况。 命名空间 vim9-namespace 为了识别可导入的文件,文件出现的第一个语句必须是 vim9script 语句 ( vim9-mix 介绍一个例外)。它告知 Vim 脚本在自己的命名空间而不是全局命名空间里被解释。如果 文件这样开始: vim9script var myvar = 'yes' 那么 "myvar" 只存在于此文件中。如果没有 vim9script ,其它脚本和函数可用 g:myvar 进行访问。 E1101 文件级别的变量和老式脚本里的局部 "s:" 变量非常类似,但省略 "s:"。而且不能被删 除。 和以前一样,Vim9 脚本中仍然可用全局 "g:" 命名空间。还有 "w:"、"b:" 和 "t:" 命 名空间。它们的共同点是变量不声明,没有特定类型且可以删除。 E1304 :vim9script 一个副作用是 'cpoptions' 选项设为 Vim 缺省值,类似于: :set cpo&vim 其中一个效果是 line-continuation 总是打开。 脚本结束时则会恢复 'cpoptions' 原先的值,在脚本里增加或删除的标志位也会从原先 的值里进行相应的增删,以达到相同效果。标志位的顺序可能会变化。 这不适用于启动时执行的 vimrc 文件。 vim9-mix 有一个办法可以在一个脚本文件里同时使用老式和 Vim9 语法: " 这里是 _老式 Vim 脚本_ 注释 if !has('vim9script') " 这里是 _老式 Vim 脚本_ 注释/命令 finish endif vim9script # 从这里开始是 _Vim9 脚本_ 注释/命令 echowindow $"has('vim9script') == {has('vim9script')}" 这样就可以编写脚本,在可能的情况下利用 Vim9 脚本的语法,但能防止在 Vim 版本不 支持 'vim9script' 时,执行该命令导致出错。 注意 Vim9 语法 在 Vim 9 之前有过改变,所以使用正确语法的脚本 (如 "import from" 而不是 "import") 可能会报错。如果不想如此,检查 v:version >= 900 会更安全些 (因为 Vim 8.2 补丁 3965 就会使 "has('vim9script')" 返回 v:true )。有时检查更 晚版本会更谨慎些。Vim9 脚本的特性集会继续增长,例如,如果使用了元组 (在 Vim 9.1 补丁 1232 引入),更好的条件是: if !has('patch-9.1.1232') echowindow $"Fail: Vim 没有补丁 9.1.1232" finish endif vim9script echowindow $"Pass: 版本号 {v:versionlong}。 继续中 ..." 不管使用哪种 vim 混合条件,只能有两种方法可以工作: 1. "if" 语句计算为假,跳过直到 endif 部分为止的命令, vim9script 是第一 个真正执行的命令。 2. "if" 语句计算为真,执行直到 endif 部分为止的命令,而 finish 在到达 vim9script 前退出。 导出 :export :exp 可以这样导出项目: export const EXPORTED_CONST = 1234 export var someValue = ... export final someValue = ... export const someValue = ... export def MyFunc() ... export class MyClass ... export interface MyClass ... export enum MyEnum ... E1043 E1044 就像这里暗示的,只能导出常数、变量、 :def 函数、类、界面和枚举。 E1042 :export 只能用于 Vim9 脚本的脚本级别。 导入 :import :imp E1094 E1047 E1262 E1048 E1049 E1053 E1071 E1088 E1236 导出项目可在另一个脚本里被导入。导入语法有两种形式。简单形式: import {filename} {filename} 是计算结果为字符串的表达式。此形式里的文件名必须以 ".vim" 结尾, ".vim" 之前的部分会成为该命名空间的脚本局部名。例如: import "myscript.vim" 这使得 "myscript.vim" 里的所有导出项目都可用 "myscript.item" 的形式访问。 :import-as E1257 E1261 如果名字太长或有二义性,以下形式可用于指定别名: import {longfilename} as {name} 这里 {name} 成为代表被导入命名空间的特定的脚本局部名。因此 {name} 必须由字母、 数位和 "_" 组成,就像 internal-variables 那样。{longfilename} 表达式的计算结 果必须为文件名。例如: import "thatscript.vim.v2" as that E1060 E1258 E1259 E1260 这样你就可用 "that.item" 等等。可以自由选择名字 "that"。请用可以识别导入脚本的 名字。避免命令名、命令修饰符和内建函数名,因为你选的名字会使那些被隐藏。最好不 要用大写字母开头的名字,因为那样也可能会隐藏全局用户命令和函数。此外,也不能和 脚本其它项目,如函数或变量名,重名。 如果不想要带句号的名字,可为函数提供本地引用: var LongFunc = that.LongFuncName 对常量这样也可以: const MAXLEN = that.MAX_LEN_OF_NAME 对变量不可以,因为值会被复制一次,而修改变量会改变备份,而不是原先的变量。所 以,只能用带句号的全名。 :import 不能在函数内使用。导入项目应该出现在脚本级别,且只能导入一次。 import 之后的脚本名可以是: - 相对路径,以 "." 或 ".." 开始。这会找到相对于脚本文件自身所在位置的文件。可 用于把大型插件分割为几个文件。 - 绝对路径,Unix 上以 "/" 开始,或 MS-Windows 上以 "D:/" 开始。很少用到。 - 既非相对也不是绝对的路径。会在 'runtimepath' 项目的 "import" 子目录中寻找。 名字通常较长且唯一,以避免载入错误的文件。 - 注意 不使用 "after/import"。 如果名字不以 ".vim" 结尾,必须使用 "as name" 的形式。 Vim9 脚本文件一旦导入,结果会被缓冲,下次导入相同脚本时,会使用缓冲而不会再次 读取文件。 不能导入同一脚本两次,即使用两个不同的 "as" 名字也不行。 使用导入名时,句号和项目名必须在同一行,中间不能断行: echo that. name # 出错! echo that .name # 出错! import-map Vim9 脚本从另一个脚本导入函数时,用 <SID> 前缀可在映射里引用导入的函数: noremap <silent> ,a :call <SID>name.Function()<CR> 映射在定义时, "<SID>name." 会被替换为 <SNR> 和导入脚本的脚本 ID。 更简单的方案是用 <ScriptCmd> : noremap ,a <ScriptCmd>name.Function()<CR> 注意 这只适用于函数,不适用于变量。 import-legacy legacy-import :import 也可用于老式 Vim 脚本。即使未给出 "s:" 前缀,导入命名空间仍然是脚本 局部的。例如: import "myfile.vim" call s:myfile.MyFunc() 使用 "as name" 形式: import "otherfile.vim9script" as that call s:that.OtherFunc() 不过,不能直接解析命名空间本身: import "that.vim" echo s:that " ERROR: E1060: Expected dot after name: s:that 这也影响老式映射上下文里 <SID> 的使用。因为 <SID> 只是函数的合法前缀而 是命名空间的,你不能用之在局部于脚本的命名空间里局限函数的作用域。因此,不要用 <SID> 函数前缀,而应用 <ScriptCmd> 。例如: noremap ,a <ScriptCmd>:call s:that.OtherFunc()<CR> :import-cycle import 命令在见到时就会执行。如果脚本 A 导入脚本 B,而 B 已经 (直接或间接地) 导入了 A,会跳过前者。A 在 "import B" 之后的项目此时尚处于未处理和未定义状态。 所以,循环导入可以存在且不会直接报错,但 A 在 "import B" 之后的项目可能会因为 未定义而出错。这不适用于自动载入导入,见下一小节。 在自动载入脚本中导入 vim9-autoload import-autoload 要有最佳的启动速度,应该尽量延迟脚本的载入直到实际需要为止。建议使用自动载入机 制: E1264 1. 在插件中定义指向自动载入脚本导入的项目的用户命令、函数和/或映射。 import autoload 'for/search.vim' command -nargs=1 SearchForStuff search.Stuff(<f-args>) 应放在 .../plugin/anyname.vim。 "anyname.vim" 可自由选择其名字。现在就 可用 "SearchForStff" 命令了。 :import 的 "autoload" 参数意味着直到其中项目被实际用到,脚本暂缓载 入。这类脚本可在 'runtimepath' 的 "autoload" 目录下找到,而不是 "import" 目录。此处,也可用相对或绝对名字,见下。 2. 主要代码放在自动载入脚本。 vim9script export def Stuff(arg: string): void ... 放在 .../autoload/for/search.vim 里。 "search.vim" 脚本放在 "/autoload/for/" 目录下的效果是 "for#search#" 会 加在每个导出的项目之前。前缀由文件名获取,就像老式自动载入脚本里你会手 动做的一样。因而,上例中的导出函数可由 "for#search#Stuff" 获取,但通常 会用 `import autoload` 而不是直接用前缀 (在函数的编译过程中时如果遇到 此函数,后者有载入自动载入脚本的副作用)。 自动载入脚本中,可以根据需要分割功能和导入其它文件。这样可以在插件间分 享代码。 在 'runtimepath' 里的所有项目里搜索自动载入脚本颇耗时。如果插件知道脚本的位置 的话,使用相对路径是常见的。这样可以避免搜索,会快很多。另一个优点是脚本名不需 要是唯一的。也可用绝对路径。例如: import autoload '../lib/implement.vim' import autoload MyScriptsDir .. '/lib/implement.vim' 使用导入的自动载入脚本的映射在定义时,可用特殊键 <ScriptCmd> 。这使映射中的命 令可以使用映射定义所在脚本的上下文。 编译 :def 函数时,如果遇到自动载入脚本中的函数,不载入该脚本,直到 :def 函 数被调用时才载入。这意味着只有在运行时才看到错误,因为参数和返回类型此时尚未 知。但如果直接用带 '#' 字符的名字,那么 确实会 载入自动载入脚本。 小心不要意外触发自动载入脚本的载入。例如,设置选项时如果用到函数名,要用字符串 而非函数引用: import autoload 'qftf.vim' &quickfixtextfunc = 'qftf.Func' # _不载入_自动载入脚本 &quickfixtextfunc = qftf.Func # _载入_自动载入脚本 另一方面,需要报告错误的时候,应早点载入脚本。 为测试用, test_override() 函数使 `import autoload` 立即载入脚本,这样可以马 上检查项目和类型,而无须等待实际使用它们的时候再检查: test_override('autoload', 1) 之后要复位的话: test_override('autoload', 0) 或者: test_override('ALL', 0)

7. 类和界面 vim9-classes 老式 Vim 脚本里字典可以通过加入函数作为成员,来用作某种形式的对象。但这很低 效,需要作者自行确保所有的对象有正确的成员。见 Dictionary-function 。 就像绝大多数流行的面向对象编程语言那样, Vim9 脚本可有类、对象、界面和枚举。 因为牵涉大量功能,这些描述放在了单独的帮助文件里: vim9class.txt

8. 理据 vim9-rationale :def 命令 插件作者一直要求有更快的 Vim 脚本。调查发现,继续保持原有的函数调用语义会使性 能提高近乎不可能,因为涉及的函数调用、局部函数作用域的设置以及行的执行引起的负 担。这里需要处理很多细节,比如错误信息和例外。创建用于 a: 和 l: 作用域的字典、 a:000 列表和若干其它部分增加了太多不可避免的负担。 因此定义新风格函数的 :def 方法应运而生,它接受使用不同语义的函数。多数功能不 变,但有些部分有变化。经过考虑,这种定义函数的新方法是区别老式风格代码和 Vim9 脚本代码的最佳方案。 使用 "def" 定义函数来源于 Python。其它语言使用 "function",这和老式的 Vim 脚本 使用的有冲突。 类型检查 应在编译时尽可能地把 Vim 代码行编译为指令。延迟到运行时会使执行变慢,也意味着 错误只能在后期才能发现。例如,如果遇到 "+" 字符时编译成通用的加法指令,在运行 时,此指令必须检查参数类型并决定要执行的是哪种加法。如果类型是字典要抛出错误。 如果类型已知为数值型,就可用 "数值相加" 指令,这会快很多。编译时可报错,而运行 时就无需错误处理,因为两个数值相加几乎不会出错。 备注: 稍为离题一下,唯一例外是整数溢出,如果结果超出了最大的整数值的话。例 如,加到 64-位带符号整数使结果大于 2^63: vim9script echo 9223372036854775807 + 1 # -9223372036854775808 echo 2->pow(63)->float2nr() + 1 # -9223372036854775808 类型语法,包括使用 <type> 用作复合类型,类似于 Java,因为容易理解,也广泛使 用。类型名是 Vim 之前所用的加上新增的 "void" 和 "bool" 等类型。 去除臃肿和怪异行为 一旦决定 :def 函数可以和老式函数有不同的语法,我们就有自由去新增改进,使了解 常用编程语言的用户对代码能更熟悉。换而言之: 删除只有 Vim 才采取的怪异行为。 我们也可以去除臃肿,这里主要是指使 Vim 脚本和陈旧的 Vi 命令后向兼容的那些部 分。 例如: - 调用函数不再需要 :call ,而计算表达式不再需要 :eval 。 - 续行不再需要前导反斜杠,可以自动判断表达式何处终止。 不过,这也意味有些部分需要改变: - 注释改用 # 而不是 " 开头,以避免和字符串混淆。这也很好,若干流行的语言也是如 此。 - Ex 命令范围需要有冒号前导,以避免和表达式混淆 (单引号可以是字符串或位置标 记,"/" 可能是除法或搜索命令,等等)。 目标是尽量减少差异。一个好的标准是如果不小心用了旧语法,很有可能你会得到错误信 息。 来自流行语言的语法和语义 脚本作者抱怨 Vim 脚本语法和他们习惯使用的有出乎意外的差异。为了减少抱怨,使用 流行的语言作为范例。与此同时,我们不想放弃老式的 Vim 脚本为人熟知的部分。 有很多方面我们跟随 TypeScript。这是新近的语言,已得到广泛流行,且和 Vim 脚本有 相似性。它也有静态类型 (总是有已知值类型的变量) 和动态类型 (在运行时可决定有不 同类型的变量) 的混合。既然老式 Vim 脚本是动态类型的,许多现有功能 (尤其是内建 函数) 依赖于这一点,而静态类型允许更快地执行,我们需要在 Vim9 脚本中支持两者的 混合。 我们无意完全照搬 TypeScript 语法和语义。只想借用可用于 Vim 的部分,使 Vim 用户 可以开心地接受。TypeScript 是个复杂的语言,有它自己的历史,优点和缺点。关于缺 点部分,可阅读此书: "JavaScript: The Good Parts"。或找找此文章 "TypeScript: the good parts" 并阅读 "Things to avoid" 一节。 熟悉其它语言 (Java、Python 等等) 的人士可能会不喜欢或不理解 TypeScript 的其它 一些部分。我们也试图避免那些部分。 避免的 TypeScript 特定项目: - 重载 "+" 同时用于加法和字符串连接。这有违老式的 Vim 脚本,也经常引发错误。为 此原因,我们继续使用 ".." 用于字符串连接。Lua 也如此使用 ".."。这也方便把更 多值转换为字符串。 - TypeScript 可用形如 "99 || 'yes'" 的表达式作为条件,但不能把该值赋给布尔型。 这不统一也很讨厌。Vim 识别带 && 或 || 的表达式,并可以把结果用作布尔型。新增 了 falsy-operator 支持使用缺省值的机制。 - TypeScript 把空串当作假值,而空列表或字典当作真值。这不统一。Vim 中空列表和 字典也都当作假值。 - TypeScript 有若干 "只读" 类型,其用途有限,因为类型转换可抹除其不可更改的性 质。Vim 则对值进行锁定,这更灵活,但只在运行时进行检查。 - TypeScript 有复杂的 "import" 语句,这和 Vim 导入机制不匹配。可用更简单的机制 代替,以满足导入脚本只执行一次的要求。 声明 老式 Vim 脚本中,每个赋值都要用 :let 语句,而 Vim9 中只需用之作声明。既有此 不同,最好采用另一个命令: :var 。它在很多语言中都有用到。语义或有些许不同,但 都很容易把它识别为声明。 用 :const 来定义常量很常见,但语义有差别。有些语言只使变量本身不可变,而其它 语言则使其值不可变。考虑到 "final" 在 Java 里使变量不可变已为人熟知,我们决定 采用它来实现该语义。而 :const 可用作使两者都不可变。这也用于老式 Vim 脚本 里,含义近乎相同。 最后,我们采用的和 Dart 十分类似: :var name # 可变的变量和值 :final name # 不可变的变量,可变的值 :const name # 不可变的变量和值 因为老式和 Vim9 脚本会混合使用,全局变量也会共享,所以类型检查最好可选。另外, 类型推断机制会在很多场合下不再需要对类型直接指定。TypeScript 语法最适合为声明 加入类型: var name: string # 指定字符串类型 ... name = 'John' const greeting = 'hello' # 推断为字符串类型 这是我们如何在声明在放入类型: var mylist: list<string> final mylist: list<string> = ['foo'] def Func(arg1: number, arg2: string): bool 考虑过两种其它方案: 1. 把类型放在名字前,类似于 Dart: var list<string> mylist final list<string> mylist = ['foo'] def Func(number arg1, string arg2) bool 2. 为类型放在变量名后,但不用冒号,类似于 Go: var mylist list<string> final mylist list<string> = ['foo'] def Func(arg1 number, arg2 string) bool 第一种对用过 C 或 Java 的用户很熟悉。而第二种和第一种比起来没有任何好处,我们 先排除第二种。 因为使用了类型推断机制,如果可以从值中推断,类型可以省略。这意味着在 var 后 我们不知道是跟着类型还是名字。这使解析复杂化,不仅对 Vim,对人而言也是如此。另 外,这样也没法使用和类型名重名的变量名,用 `var string string` 太混淆了。 我们最终选择了用冒号分隔名字和类型的语法。它需要引入标点符号,但实际更易分辨声 明的不同部分。 表达式 表达式计算已经和其它语言采用的方式很接近。有些细节有出入,有改进的空间。例如, 布尔条件可以接受字符串,先把它转换为数值,并检查该值是否非零。这不符一般期望, 经常引发错误,因为不以数值开头的文本会转换为零,也就被当作假值。如此,字符串用 作条件时就经常会不报错就而被当作假值,这常产生混淆。 Vim9 的类型检查更严格以防出错。需要使用条件时,例如用 :if 命令或 || 操作符 的时候,只接受类似于布尔的值: 真: truev:true1 、`0 < 9` 假: falsev:false0 、`0 > 9` 注意 数值零为假,而数值一为真。这比绝大多数语言要宽一些。这是因为许多内建函数 会返回这些值,而改变这一点得不偿失。用了一段时间后,现在效果看来不错。 如用你有任何类型的值并希望把它当作布尔型来使用,使用 !! 操作符 (见 expr-! ): vim9script # 下面的值都为 true: echo [!!'text', !![1], !!{'x': 1}, !!1, !!1.1] # 下面的值都为 false: echo [!!'', !![], !!{}, !!0, !!0.0] JavaScript 这样的语言中,我们有这样方便的构造: GetName() || 'unknown' 不过,这和在使用条件的地方只接受布尔型有冲突。为此,引入 "??" 操作符: GetName() ?? 'unknown' 这里,你在显式地表达自己的意愿,按值本身去使用,而不是用作布尔值。这叫作 falsy-operator导入和导出 老式 Vim 脚本的一个问题是所有函数和变量缺省都是全局的。可以使它们局部于脚本, 但因而就不能为其它脚本所用。这就违背了软件包只能有选择性地导出项目,其它保持局 部的概念。 Vim9 脚本里支持和 JavaScript 导入导出非常相似的机制。这是已有的 :source 命令 的变种,且工作方式符合人们期待: - 和所有的缺省都是全局相反,所有的都是局部于脚本的,有些被导出。 - 导入脚本时显式列出要导入的符号,避免以后新增功能时的名字冲突和可能的失败。 - 此机制允许编写大而长又有清晰 API 的脚本: 导出函数、变量和类。 - 通过使用相对路径,同一包里导入的载入会更快,无需搜索许多目录。 - 导入一旦使用,其中项目会被缓冲而避免了再次载入。 - Vim 特定使事物局部于脚本的 "s:" 用法可以不需要了。 (从 Vim9 或老式脚本里) 执行 Vim9 脚本时,只能使用全局定义的项目,而不是导出的 项目。考虑过以下备选方案: - 所有导出项目成为局部于脚本的项目。这样没法控制什么项目有定义,有可能很快就会 有问题。 - 使用导出项目成为全局项目。缺点是这样就不能避免全局空间的名字冲突。 - 完全禁止 Vim9 脚本的执行,而必须使用 :import 。这样就很难用脚本进行测试,或 在命令行上执行脚本进行实验。 注意可以 在老式 Vim 脚本中使用 :import ,见上。 尽早编译函数 函数在实际调用时或使用 :defcompile 时才进行编译。为什么不尽早编译函数,以便 尽快报告语法和类型错误呢? 函数不能在遭遇时立刻编译,因为可能有之后定义的函数的前向引用。考虑定义函数 A、 B 和 C,其中 A 调用 B,B 调用 C,而 C 又调用 A。这里不可能通过对函数重新排列来 避免正向引用。 一个替代方案是先扫描整个文件以定位项目并判断其类型,这样就能找到正向引用,然后 再执行脚本并编译函数。这意味着脚本要解析两次,这会减慢速度,且脚本级别的某些条 件,如检查某特性是否支持等等,会难于使用。有过这方面的尝试,但结果是不能很好地 工作。 也可以在脚本最后编译所有函数。这样做的缺点是如果函数从未被调用,仍然要承受其编 译的开销。因为启动速度至关重要,绝大多数情况下最好延后处理,并在那时才报告语法 和类型错误。如果确实希望早报告错误,譬如测试期间,在脚本结束处的 :defcompile 命令可作救济。 为什么不用已有的嵌入式语言? Vim 支持 Perl、Python、Lua、Tcl 和一些其它语言的接口。但由于种种原因,这些从未 广泛使用过。Vim 9 设计时作了一个决定,降低这些接口的优先级,并集中在 Vim 脚本 上。 不过,插件作者可能对其它语言更加熟悉,想用已有的库或要提高性能。我们鼓励脚本作 者使用任何语言编程并作为外部进程运行,使用作业和通道通信。我们可以想办法使之更 加便捷。 使用外部工具也有其不足。一种替代方案是把工具转换为 Vim 脚本。要尽量减少翻译的 工作量,并且同时保持代码快速,需要支持工具使用的构造。因为 Vim9 脚本现在支持了 类、对象、界面和枚举,这种做法现在更加可行。 vim:tw=78:ts=8:noet:ft=help:norl: