You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

301 lines
14 KiB

2 years ago
# 第四章 VimL 数据结构进阶
在第 2.1 章已经介绍了 VimL 的变量与类型的基本概念。本章将对变量类型所指代的数
据结构作进一步的讨论。
## 4.1 再谈列表与字符串
### 引用与实体
前文讲到,列表作为一种集合变量,与标量变量(数字或字符串)有着本质的区别。其中
首要理解的就是一个列表变量只是某个列表实体的引用。
直接用示例说话吧,先看数字变量与字符串变量的平凡例子:
```vim
: 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`,也是
这个过程。
然后再看看列表变量:
```vim
: 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` 的关系很一致呢。
但是,我们重新看下面这个例子:
```vim
: 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']`
相当于分以下两步执行工作:
1. new 列表实体 = ['a', 'aa', 'aaa']
2. let aList = 列表实体的引用
然后命令 `: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` 的目的。
总结,只要牢记以下两条准则:
* 标量变量保存的是值;
* 列表变量保存的是引用。
### 函数参数与引用
我们再通过函数调用参数来进一步说明列表的引用特性。
举个简单的例子,交换两个值,可以引入一个临时变量,由三条语句完成:
```vim
: let tmp = a
: let a = b
: let b = tmp
```
这种交换值的需求挺常见的,考虑包装成一个函数如何?
```vim
: 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)` 时,相当于先执行以下两个赋值语句给参数赋值:
```vim
: let a:iValue = a
: let a:jValue = b
```
此外,不管在函数内不管怎么倒腾参数 `a:iValue``b:jValue`,都不会影响原来的
`a``b` 变量。因为如前所述,标量赋值,只是拷贝了值,等号两边的变量是再无联
系的。
但是,交换列表不同位置上的元素是可实现的,比如把上面那个交换函数改成三参数版,
第一个参数是列表,跟着两个索引:
```vim
: 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
```
请试运行以下语句确认这个函数的有效性:
```vim
: echo aList
: call Swap(aList, 0, 1)
: echo aList
```
在写较复杂的 VimL 函数时,一般不建议在函数体内大量使用 `a:` 作用域参数。因为传
入的参数是无类型的,很可能是不安全的。最好在函数的开始作一些检查,合法后再将
`a:` 参数赋给一个 `l:` 变量,然后在函数主体中只对该局部变量操作。此后,如果入参
的需求有变动,就只修改函数前面几行就可以了。例如再将交换函数改成如下
版本:
```vim
: 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
```
再用以下语句来测试修改版的交换函数:
```vim
: call Swap(aList, 1, 2)
: echo aList
```
可见,即使在函数体内,将参数 `a:list` 赋给另一个局部变量 `l:list`,交换工作也
正常运行。因为 `g:aList` `a:list``l:list` 其实都是同一个列表实体的引用啊。
### 列表解包
在 3.4 节我们用 `execute` 定义了一个 `:LET` 命令,用于实现连等号赋值。但实际上
可以直接用列表赋值的办法实现类似的效果。例如:
```vim
: 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),即相当于把列表元素提取出来放在独立的变量中。显然
用这种方法为多个变量赋值更具灵活性,可以为不同变量赋不同的值。
这个语法除了可多重赋值外,还能方便地实现变量交换,如:
```vim
: let [x, y] = [y, x]
```
用过 python 的对此用途应该很有亲切感。不过在 VimL 中,等号两边的中括号不可省略
,且等号两边的列表元素个数必需相同,否则会出错。不过在左值列表中可以用分号分隔
最后一个变量,用于接收右值列表的剩余元素,如:
```vim
: let [v1, v2; rest] = list
" 相当于
: let v1 = list[0]
: let v2 = list[1]
: let rest = list[2:]
```
在上例中假设 `list` 列表元素只包含简单标量,则解包赋值后,`v1` `v2` 都是只接收
了一个元素值的标量,而 `rest` 则接收了剩余元素,它还是个(稍短的)列表变量。而
`list[2:]` 的语法是列表切片(slice)。
### 索引与切片
这里再归纳一下列表的索引用法:
* 索引从 0 开始,不是从 1 开始。
* 可以使用负索引,-1 表示最后一个索引。
* 可以使用多个索引,这也叫切片,表示列表的一部分。
要索引一个列表元素时,用正索引或负索引等效的,这取决于应用场合用哪个方便。如果
列表长度是 `n`,则以下表示法等效:
```vim
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`,这里仅归纳概要。
+ 查询列表信息的函数:
* len(list) 取列表长度,列表的最大索引是 len(list)-1。
* empty(list) 判断列表是否为空,即列表长度为 0。
* get(list, i) 相当于 list[i],但是当 i 超出索引范围时,get() 函数不会出错,且
可再提供第三参数表示超出索引时的默认值(如果省略,默认值0)。
* index(list, item) 查找一个元素在列表中的位置,如果不存在该元素,则返回 -1。
* count(list, item) 检查一个元素在列表中出现多少次。
* max(list) min(list) 查询一个列表中的最大或最小元素。
* string(list) 将列表转化为字符串表示法。
* join(list, sep) 将列表中的元素用指定分隔符连接为一个字符串表示。
+ 修改列表元素的函数:
* add(list, item) 在列表末尾添加一个元素。
* insert(list, item) 在列表头部添加一个元素,比 add() 尾添加低效。但 insert()
可额外提供第三参数表示要插入的索引位置,省略即 0 表示插在最前面。
* remove(list, idx) 删除位置 idx 上的一个元素,remove(list, i, j) 删除从 i 到
j 索引之间的所有无素,相当于 unlet list[i:j]。
+ 生成列表的函数:
* range() 支持一至三个参数,生成连续或定步长的数字列表。
* extend(list1, list2) 连接两个列表,相当于 list1+list2,但 extend 会原位修改
list1 列表。与 add() 函数不同的是,add 只增加一个元素,而 extend 是加入另一
个列表。
* repeat(list, count) 相当于不断连接自身,总计重复 count 次,生成一个更长的列
表。
* copy(list),生成一个列表副本,用等号赋值只是引用同一个列表实体,用 copy() 函
数才能生成另一个新列表(每个元素值与原列表相同而已)。copy() 函数是浅拷贝,
列表元素直接赋值。如果要考虑列表元素也可能是列表或字典(引用),则用
deepcopy(list) 递归拷贝完全的副本。
* reverse(list) 将一个列表倒序排列,原位修改原列表。
* split(list, pattern),将一个字符串分解为列表,相当于 join() 的反函数。
+ 分析列表的高阶函数:
* sort(list) 为一个列表排序。
* uniq(list) 删除列表中相邻的重复元素,列表需已排序。
* map(list, expr) 将列表每个元素进行某种运算,将结果替换原元素。
* filter(list, expr) 将列表每个元素进行某种运算,若结果为 0,则删除相应元素。
这些高阶函数,除了都会原位修改作为第一个参数的列表外,都还能接收额外参数表明如
何处理每个元素。由于额外参数可以是另一个函数(引用),所以称之为高阶函数。其具
体用法略复杂,在后面相关章节将继续讲解部分示例。
### 字符串与列表的关系
字符串在很大程序上可以理解为字符列表,可以用类似的索引与切片机制。但是,字符串
与列表的最大区别在于,字符串是一个完整的不可变标量。所以,凡是可以改变列表内部
某个元素的操作(如索引赋值、切片赋值)或函数(如 add/remove 等),都不可作用于
字符串。而 copy() 也没必要用于字符串,直接用等号赋值即可。不过 repeat() 函数作
用于字符串很有用,能方便生成长字符串。
将字符串打散为字符数组,可用如下函数方法:
```vim
: let string = 'abcdefg'
: let list = split(string, '\zs')
: echo list
```
split(string, pattern) 函数是将字符串按某种模式分隔成列表的。`\zs` 不过是一种
特殊模式,它可以匹配任意字符之间(详情请参考正则表达式文档),所以结果就是将每
个字符分隔到列表中了。