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
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` 不过是一种
|
||
|
特殊模式,它可以匹配任意字符之间(详情请参考正则表达式文档),所以结果就是将每
|
||
|
个字符分隔到列表中了。
|