前文讲到,列表作为一种集合变量,与标量变量(数字或字符串)有着本质的区别。其中 首要理解的就是一个列表变量只是某个列表实体的引用。
直接用示例说话吧,先看数字变量与字符串变量的平凡例子:
: let x = 1
: let y = x
: echo 'x:' x 'y:' y
: let y = 2
: echo 'x:' x 'y:' y
:
: let a = 'aa'
: let b = a
: echo 'a:' a 'b:' b
: let b = 'bb'
: echo 'a:' a 'b:' b
我们先创建了一个数字变量 x
,并为其赋值为 1
,然后再创建一个变量 y
,并为
x
的值赋给它。显然,现在 x
与 y
的值都为 1
。随后我们改变 y
的值,重
赋为 2
,再查看两个变量的值,发现只有变量 y
的值改变了,x
的值是没改变的
。因此,即使在创建 y
变量时用 :let y = x
看似将它与 x
关联了,但这两个变
量终究是两个独立不同的变量,唯一有关联的也不外是 y
初始化时获取了 x
的值。
此后这两个变量分道扬镳,可分别独立地改变运作。对于字符串变量 a
与 b
,也是
这个过程。
然后再看看列表变量:
: let aList = ['a', 'aa', 'aaa']
: let bList = aList
: echo 'aList:' aList 'bList:' bList
: let bList = ['b', 'bb', 'bbb']
: echo 'aList:' aList 'bList:' bList
结果似乎与上面的数字或字符中标题很相似,没什么差别嘛。虽然 bList
一开始与
aList
表示同一个变量,但后来给 bList
重新定义了一个列表,也没有改变原来的
aList
列表。这与字符串 a
b
的关系很一致呢。
但是,我们重新看下面这个例子:
: unlet! aList bList
: let aList = ['a', 'aa', 'aaa']
: let bList = aList
: echo 'aList:' aList 'bList:' bList
: let bList[0] = 'b'
: echo 'aList:' aList 'bList:' bList
这里先把原来的 aList
bList
变量删除了,以免上例的影响。仍然创建了列量变量
aList
,与 bList
并让它们“相等”。然后我们通过 bList
变量将列表的第一项
[0]
改成另一个值 b
,再查看两个列表的值。这时发现 aList
列表也改变了,与
bList
作出了同样的改变,两者仍是“相等”。
通过这组试验,想说明的是,当 VimL 创建一个列表(变量)时,它其实是在内部维护了
一个列表实体,然后这个变量只是这个列表实体的引用。命令 :let aList = ['a', 'aa', 'aaa']
相当于分以下两步执行工作:
然后命令 :let bList = aList
,它只是将 aList
变量对其列表实体的引用再赋值给
变量 bList
,结果就是,这两个变量都引用了同一个列表实体,或说指向了同一个列表
实体。而命令 :let bList[0] = 'b'
则表示通过变量 bList
修改了它所引用的列表
的第一个元素。但变量 aList
也引用这个列表实体,所以再次查看 aList
时,发现
它的第一个元素也变成 'b'
了。实际上,不管是对 aList
还是 bList
进行索引
操作,都是对同一个它们所引用的那个列表实体进行操作,那是无差别的。
对于普通标量变量,则是另一种情况。当执行命令 :let b = a
时,变量 b
就已经
与 a
是无关的两个独立变量,它只是将 a
的值取出来并赋给 b
而已。但
:let bList = aList
是将它们指向同一个列表实体,在用户使用层面上,可以认为它
们是同一个东西。但是当执行 :let bList = ['b', 'bb', 'bbb']
后,变量 bList
就指向另一个列表实体了,它与 aList
就再无联系了。
可见,当对列表变量 bList
进行整体赋值时,就改变了该变量所代表的意义。这时与
对字符串变量 b
整体赋值是一样的意义。然而,标量始终只能当作一个完整独立的值
使用,它再无内部结构。例如,无法使用 let b[0] = 'c'
来改变字符串的第一个字符
,只能将另一个字符串整体赋给 b
而达到改变 b
的目的。
总结,只要牢记以下两条准则:
我们再通过函数调用参数来进一步说明列表的引用特性。
举个简单的例子,交换两个值,可以引入一个临时变量,由三条语句完成:
: let tmp = a
: let a = b
: let b = tmp
这种交换值的需求挺常见的,考虑包装成一个函数如何?
: function! Swap(iValue, jValue) abort
: let l:tmp = a:iValue
: let a:iValue = a:jValue
: let a:jValue = l:tmp
: endfunction
但是,当尝试调用 :call Swap(a, b)
时,vim 报错了。因为参数作用域 a:
是只读
变量,所以不能给 a:iValue
或 a:jValue
赋另外的值。但是,即使参数不是只读的
,这样的交换函数也是没效果的(比如用 C 或 python 改写这个交换函数)。因为在调
用 Swap(a, b)
时,相当于先执行以下两个赋值语句给参数赋值:
: let a:iValue = a
: let a:jValue = b
此外,不管在函数内不管怎么倒腾参数 a:iValue
与 b:jValue
,都不会影响原来的
a
与 b
变量。因为如前所述,标量赋值,只是拷贝了值,等号两边的变量是再无联
系的。
但是,交换列表不同位置上的元素是可实现的,比如把上面那个交换函数改成三参数版, 第一个参数是列表,跟着两个索引:
: function! Swap(list, idx, jdx) abort
: let l:tmp = a:list[a:idx]
: let a:list[a:idx] = a:list[a:jdx]
: let a:list[a:jdx] = l:tmp
: endfunction
请试运行以下语句确认这个函数的有效性:
: echo aList
: call Swap(aList, 0, 1)
: echo aList
在写较复杂的 VimL 函数时,一般不建议在函数体内大量使用 a:
作用域参数。因为传
入的参数是无类型的,很可能是不安全的。最好在函数的开始作一些检查,合法后再将
a:
参数赋给一个 l:
变量,然后在函数主体中只对该局部变量操作。此后,如果入参
的需求有变动,就只修改函数前面几行就可以了。例如再将交换函数改成如下
版本:
: function! Swap(list, idx, jdx) abort
: if type(a:list) == v:t_list || type(a:list) == v:t_dict
: let list = a:list
: else
: return " 只允许第一参数为列表或字典
: endif
:
: let i = a:idx + 0 " 显式转为数字
: let j = a:jdx + 0
:
: let l:tmp = list[i]
: let list[i] = list[j]
: let list[j] = l:tmp
: endfunction
再用以下语句来测试修改版的交换函数:
: call Swap(aList, 1, 2)
: echo aList
可见,即使在函数体内,将参数 a:list
赋给另一个局部变量 l:list
,交换工作也
正常运行。因为 g:aList
a:list
与 l:list
其实都是同一个列表实体的引用啊。
在 3.4 节我们用 execute
定义了一个 :LET
命令,用于实现连等号赋值。但实际上
可以直接用列表赋值的办法实现类似的效果。例如:
: LET x=y=z=1
: let [x, y, z] = [1, 1, 1]
: let [x, y, z] = [1, 2, 3]
其中前两个语句的结果完全一样,都是为 x
y
z
三个变量赋值为 1
。注意等号
左边也需要用中括号把待赋值变量括起来,分别用等号右侧的列表元素赋值。这种行为就
叫做列表解包(List unpack),即相当于把列表元素提取出来放在独立的变量中。显然
用这种方法为多个变量赋值更具灵活性,可以为不同变量赋不同的值。
这个语法除了可多重赋值外,还能方便地实现变量交换,如:
: let [x, y] = [y, x]
用过 python 的对此用途应该很有亲切感。不过在 VimL 中,等号两边的中括号不可省略 ,且等号两边的列表元素个数必需相同,否则会出错。不过在左值列表中可以用分号分隔 最后一个变量,用于接收右值列表的剩余元素,如:
: let [v1, v2; rest] = list
" 相当于
: let v1 = list[0]
: let v2 = list[1]
: let rest = list[2:]
在上例中假设 list
列表元素只包含简单标量,则解包赋值后,v1
v2
都是只接收
了一个元素值的标量,而 rest
则接收了剩余元素,它还是个(稍短的)列表变量。而
list[2:]
的语法是列表切片(slice)。
这里再归纳一下列表的索引用法:
要索引一个列表元素时,用正索引或负索引等效的,这取决于应用场合用哪个方便。如果
列表长度是 n
,则以下表示法等效:
list[n-1] == list[-1]
list[0] == list[-n]
list[i] == list[i-n]
然而,不管正索引,还是负索引,都不能超出列表索引(长度)范围。
列表切片(slice)是指用两个索引提取一段子列表。list[i:j]
表示从索引 i
到索
引 j
之间(包含两端)的元素组成的子列表。注意以下几点:
i
j
同样支持负索引,不管用正负索引,如果 i
索引在 j
索引之后,则切片
结果是空列表。i
超出了列表左端(0
或 -n
),或 j
超出列表右端,结果也是空列表
。i
,则默认起始索引为 0
;省略结束索引 j
,则默认是最后一个索
引 -1
;如果都省略,只剩一个冒号,list[:]
与原列表 list
是一样的(但是
另一个拷贝列表)。list[i:j:step]
或 list[i:j:step]
在 VimL 中是非法
的,不支持跳格切片,只支持连续切片。list[s:e]
表示法有歧义,因为可能存在脚本局部变量 s:e
,则用该变量值单索引
列表。可在冒号前后加空格避免歧义,list[s : e]
表示切片。VimL 提供了一些基本的内置函数用于列表的常用操作,详细用法请参考文档
:help list-functions
,这里仅归纳概要。
查询列表信息的函数:
修改列表元素的函数:
生成列表的函数:
分析列表的高阶函数:
这些高阶函数,除了都会原位修改作为第一个参数的列表外,都还能接收额外参数表明如 何处理每个元素。由于额外参数可以是另一个函数(引用),所以称之为高阶函数。其具 体用法略复杂,在后面相关章节将继续讲解部分示例。
字符串在很大程序上可以理解为字符列表,可以用类似的索引与切片机制。但是,字符串 与列表的最大区别在于,字符串是一个完整的不可变标量。所以,凡是可以改变列表内部 某个元素的操作(如索引赋值、切片赋值)或函数(如 add/remove 等),都不可作用于 字符串。而 copy() 也没必要用于字符串,直接用等号赋值即可。不过 repeat() 函数作 用于字符串很有用,能方便生成长字符串。
将字符串打散为字符数组,可用如下函数方法:
: let string = 'abcdefg'
: let list = split(string, '\zs')
: echo list
split(string, pattern) 函数是将字符串按某种模式分隔成列表的。\zs
不过是一种
特殊模式,它可以匹配任意字符之间(详情请参考正则表达式文档),所以结果就是将每
个字符分隔到列表中了。