2.3 循环与迭代

优质
小牛编辑
114浏览
2023-12-01

2.3 循环与遍历

程序的比人肉强大的另一个特性就是可以任劳任怨地重复地做些单调无聊(或有聊)的 工作。本节介绍在 VimL 语言中,如何控制程序,命令其循环地按规则干活。

遍历集合变量

首先介绍的是如何依次访问列表如字典内的所有元素,毕竟在 2.1 节介绍的索引方法只 适于偶尔访问查看某个具体的元素。这里要用到的是for ... in 语法。

例如遍历列表:

: let list = [0, 1, 2, 3, 4,]
: for item in list
:     echo item
: endfor

在这个例子中,变量 item 每次获取 list 列表中的一个元素,直到取完所有元素。 相当于在循环中,依次执行 :let item=list[0] :let item=list[1] ... 等语句。 这个变量也可以叫做“循环变量”。遍历列表保证是有序的。

对于字典的 for ... in 语法略有不同,因为在字典内的每个元素是个键值对,不仅仅 是值而已。其用法如下:

: let dict = {'x':1, 'y':2, 'z':3, 'u':4, 'v':5, 'w':6,}
: for [key,val] in items(dict)
:     echo key . '=>' . val
: endfor

注意:字典内的元素是无序的。

可以单独遍历键,利用内建函数 keys() 从字典变量中构造出一个列表:

: for key in keys(dict)
:     echo key . '=>' . dict[key]
: endfor

这里的输出结果应该与上例完全一致。

遍历字典键时,如有需要,也可以先对键排个序:

: for key in sort(keys(dict))
:     echo key . '=>' . dict[key]
: endfor

遍历字典还有个只遍历值的方式,不过这种方式用途应该不多:

: for val in values(dict)
:     echo val
: endfor

总之,对于 :for var in list 语句结构,var 变量每次获取列表 list 内的一个值。 字典不是列表,所以要利用函数 items() keys() values() 等先从中构造出一个 临时数组。

固定次数循环

如果要循环执行某个语句至某个固定次数,依然可利用 for ... in 语法。只不过要利 用 range() 函数构造一个计次列表。例如,以下语句输出 Hello World! 5 次:

: for _ in range(5)
:     echo 'Hello World!'
: endfor

这里,我们用一个极简的合变量,单下划线 _ 来作为循环变量,因为我们在循环体中 根本用不着这个变量。不过这种用法并不常见,这里只说明可用 range() 实现计次循 环。

那么,range() 函数到底产生了怎样的一个列表呢,这可用如下的示例来测试:

: for i in range(5)
:     echo i
: endfor

可见,range(n) 产出一个含 n 个元素的列表,元素内容即是数字从 0 开始直到 n, 但不包含 n,用数学术语就叫做左闭右开。

其实,range() 函数不仅可以接收一个参数,还可以接收额外参数,不同个数的参数使 得其产出意义相当不一样,可用以下示例来理解一下:

: echo range(10)       |" => [0, 10)
: echo range(1, 10)    |" => [1, 10]
: echo range(1, 10, 2) |" => 从 1 开始步长为 2 的序列,不能超过 10
: echo range(0, 10, 2) |" => 从 0 开始步长为 2 的序列,恰好能包含 10

利用 range() 函数的这个性质,也就可以写出不同需求的计次 for ... in 循环。

注:VimL 没有类似 C 语言的三段式循环 for(初化;条件;更新)。只有这个 for ... in 循环,在某些语言中也叫 foreach 循环。

不定次数循环

不定循环用 :while 语句实现,当条件满足时,一直循环,基本结构如:

: let i = 0
: while i < 5
:     echo i
:     let i += 1
: endwhile

:while 循环一个重要的注意点是须在循环前定义循环变量,并记得在循环体内更新 循环变量。否则容易出现死循环,如果出现死循环,vim 没响应,一般可用 Ctrl-C 中 断脚本或命令执行。

如果 :while 条件在一开始就不满足,则 :while 循环一次也不执行。在 :for ... in 循环中,空列表也是允许的,那就也不执行循环体。

在某些情况下,死循环是设计需求,那就可用 :while 1:while v:true 来实现 ,而 for 循环无法实现,因为构建一个无限大的列表是不现实的。

循环内控制

循环除了正常结束,还另外有两个命令改变循环的执行流程:

  • :break 结束整个循环,流程跳转到 :endfor:endwhile 之后。
  • :continue 提前结束本次循环,开始下次循环,流程跳转到循环开始,对于 :for 循环来说,循环变量将获取下一个值,对于 :while 循环来说,会再次执行条件判断 。
  • 这两个命令一般要结合 :if 条件语句使用,在特定条件下才改变流程,否则没有太 多实际意义。

举些例子:

: for i in range(10)
:     if i >= 5
:         break
:     endif
:     echo i
: endfor
: echo 'done'

这里只打印了前 5 个数,因为当 i 变量到达 5 时,直接 break 了。

: for i in range(10)
:     if i % 2
:         continue
:     endif
:     echo i
: endfor
: echo 'done'

在这里,i % 2 是求模运算,如果是奇数,余数为 1 ,:if 条件满足后由于 :continue 直接开始下一次循环,:echo i 就被跳过,所以只会打印偶数。

在用 :while 循环时,要慎重用 :continue,例如以下示例:

: let i = 0
: while i < 10
:     if i % 2
:         continue
:     endif
:     echo i
:     let i += 1
: endwhile
: echo 'done'

这原意是将上个打印偶数的 :for 循环改为 :while 循环,但是好像陷入了死循环, 先 <Ctrl-C> 中止再来分析原因。那原因就是 :continue 语句跳过了 :let i+=1 的循环变量更新语句,使它陷在同一个循环中再也出不来了。

所以,如果你的 :while 是需要更新循环变量的,而且还用了 :continue,最好将更 新语句放在所有 :continue 之前。不过就这个例子而言,若作些修改后,还要同时修 改一些判断逻辑,才能实现原有意图。

*循环变量作用域与生存期

对于 :while 循环,循环变量是在循环体之外定义的,它的作用域无可厚非应与循环结 构本身同级。但对于 :for 循环,其循环变量是在循环头语句定义的,(可见 :let 并不是唯一定义或创建变量的命令,:for也可以呢),那么在整个 :for 结构结束之 后,循环变量是否还存在,值是什么呢?

: unlet! i
: for i in range(10)
:     echo i
: endfor
: echo 'done: ' . i

在这个例子中,为避免之前创建的变量 i 的影响,先调用 :unlet 删了它,然后执 行一个循环,在循环结束查看这个变量的值。可见在循环结束后,循环变量仍然存在,且 其值是 :for 列表中的最后一个元素。

那么空循环又会怎样呢?

: unlet! i
: for i in []
:     echo i
: endfor
: echo 'done: ' . i

这个示例执行到最后会报错,提示变量不存在。所以循环变量 i 并未创建。因此准确 地说,循环变量是在第一次进入循环时被赋值而创建的,而空循环就没能执行到这步。

再看一下示例:

: unlet! i
: for i in range(10)
:     echo i
:     unlet i
: endfor
: echo 'done: ' . i

在这个例子中,只在循环体最后多加了一个语句,:unlet i 将循环变量删除了。这种 写法在 Vim7 以前版本中很常见。因为列表中是可以保存不同类型的其他变量的,甚至包 括另一个列表或字典。因此在后续循环中,循环变量将可能被重赋与完全不同类型的值, 这在 Vim7 是一个“类型不匹配”的错误。所以在每次循环后将循环变量删除,能避免这个 错误,使之适用性更广。在 Vim8 之后,这种情况不再视为错误,所以这个 :unlet 语 句不是必要。只是在这里故意加回去,讨论一下循环变量作用域与生存期的问题。

运行这个示例,可见在循环打印了 10 个数字后,最后那条语句报错,变量 i 不存在 。这也是可理解的,因为这个变量在每次循环中反复删除重建。在第 10 次循环结束后, 删除了 i ,但循环无法再进入第 11 次循环,也就 i 没有再重建,所以之后 i 就不存在了。

这里想说明的问题是,如果从安全性考虑,或对变量的作用域有洁癖的话,可以在循环体 内 :unlet 删除循环变量。这样可避免循环变量在循环结束后的误用,尤其是循环中有 :break 时,退出循环时那个循环变量的最后的值是很不直观的,你最好不要依赖它去 做什么事情(除非是有意设计并考虑清楚了)。不过这有个显然的代价是反复删除重建变 量会消耗一些性能(别说 VimL 反正慢就不注重性能了,性能都是相对的)。

小结

VimL 只有两种循环,:for ... in:while。语义语法简单明了,没有其他太多变 种需要记忆负担,掌握起来其实应该不难。