2.2 选择与比较

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

2.2 选择与比较

vim 在执行脚本时,一般是按顺序逐条语句执行的。但如果只能像流水帐地顺序执行,未 免太无趣了,功能也弱爆了。本节介绍顺序结构之外的最普遍的选择分支结构,它可以根 据某种条件有选择地执行或不执行语句块(一条或多条语句)。

在 VimL 中通过 :if 命令来表示条件选择,其基本语法结构是:

: if {expr}
:     " todo
: endif

如果满足表达式 {expr} ,或说其值为“真”,则执行其后至 :endif 之间的语句。貌 似突然迸进了许多新概念,得先理一理。

表达式与语句

什么叫表达式?这可难说了。我只能先描叙下在 VimL 中什么是与什么不是表达式:

  • 单独的变量就是表达式,常量也是表达式,选项值(&option)也是,但选项本身不是;
  • 函数调用是表达式;
  • 表达式有值,表达式之间的合法运算的结果也还是表达式。
  • 但表达式不是可执行语句,它只是语句的一部分。

至于语句,在第一章也讲过。VimL 语句就是 Vim 的 ex 命令行。笼统地说,有时说到 ex 命令 是指整个命令行,不过狭义地说,是指它第一个单词所指代的关键命令。于是 ,VimL 语言的大部分语句,可认为遵循以下范式:

VimL 语句 = ex 命令 + 表达式

为什么说大部分呢?因为我们已经很熟悉的赋值语句如 :let i=1 就不完全适合。在这 里,:let 是个命令,1 是个表达式。但 = 只是依附于 :let 命令的特殊语义符 号,它不是个表达式,也不是个运算符。变量 i 在被创建之前,也还算不上表达式。 而 i=1 写在一起,或为了增加可读性加些空白 i = 1,它也不是表达式,因为它没 有值,(并不能像 C 语言那样使用连等号赋值),下面这两个语句是非法的:

: let i = j = 1
: let i = (j = 1)

在 VimL 中的常用语句中,除了这个基础得有点平淡无奇的赋值语句,其他大多是 命令 + 表达式 范式的。比如已经大量使用的 :echo 语句,以及上节介绍过的给列 表添加一个元素的函数调用语句 :call add(list, item)

然而,其实也不必太拘泥于这些概念名词。理解就好。我们归纳出概念也不外是为了更地 理解。

逻辑值与条件表达式

:if 命令(以及下一节要将要的 :while命令)后面的表达式,就是一个条件表达式 。它期望这个表达式的值的类型是逻辑值,即 type() 的结果是 v:t_bool(=6) 的值 。如果值的类型不是逻辑值,则会自动将其他值转换为一个逻辑值。逻辑类型只有唯二的 两个值,v:true 表示真,v:false 表示假。

所以关键在于 VimL 如何判定其他值是否有真假,什么是真,什么是假?其转换规则如何? 这直接写代码测试一下吧:

: if 1 | echo v:true | endif
: if 0 | echo v:true | endif
: if -1 | echo v:true | endif

: if '0' | echo v:true | endif
: if '1' | echo v:true | endif
: if '' | echo v:true | endif
: if 'a' | echo v:true | endif

: if 1.23 | echo v:true | endif |" 非法用法
: if 0.23 | echo v:true | endif |" 非法用法
: if '0.23' | echo v:true | endif
: if '1.23' | echo v:true | endif

" 以下四条也都是非法用法
: if [1, 2, 3] | echo v:true | endif
: if [] | echo v:true | endif
: if {'x':1, 'y':2} | echo v:true | endif
: if {} | echo v:true | endif

注:由于语句比较简单,就将 :if:endif 直接写在一行了,用 | 分隔子语句 。正常代码建议写在不同行上且缩进布局。

结果归纳于下:

  • 数字 0 为假,其他正数或负数为真;
  • 字符串先自动转为数字,转为 0 的话认为假,能转为其他数字认为真;
  • 浮点数不能转为逻辑值,无法判断真假;
  • 列表与字典也不能直接判断真假。

其实可进一步归纳为一句话,在 VimL 中,只能对整数值判断真假,0 是假的,其他都 是真的,字符串先自动转为数字再判断真假。其他类型的值不能直接判断真假。(至 vim8.0 版本是此规则,后面是否会改就不得而知了)

然而,我们还是经常需要判断其他类型的值的某种状态。这时可以利用一个内建函数 empty() 来帮忙。它可以接收任何类型的一个参数,如果它是“空”的,就返回真( v:true),否则返回假('v:false')。在很大程度上,它可以代替直接使用 :if 的 条件表达式,只不过在值上恰好是逻辑取反;优点则是写法统一,适用于所有类型。

在上面这个例子中,可以都再次尝试把 :if 后面的表达式包在 empty() 的参数中执 行看看结果,或用 !empty() 取反判断,如:

: if empty('0.23') | echo v:true | endif
: if !empty('a') | echo v:true | endif

综合建议:用 :if !empty({expr}) 代替 :if {expr},避免逻辑烧脑,并且大部分 情况下应该是你想要的。

比较运算符

两个整数进行相等性的比较,或大小性的比较,结果返回一个或真或假的逻辑值。整数支持 的比较运算符包括:==, !=>>=<<=

浮点数支持与整数相同的比较运算,但由于浮点误差,不建议用相等性判断。

字符串也支持与整数相同的那六个比较运算。虽然整数在直接 :if 命令中自动转为数 字处理,但在比较运算中表现良好,就是按正常的编码序逐字符比较。不过有一点特别要 注意的是,字符串比较结果受选项 &ignorecase 的影响,即有可能按忽略大小写的方 式来比较字符串。比如,观察一下如下结果吧:

: set ignorecase
: echo 'abc' == 'ABC'
: set noignorecase
: echo 'abc' == 'ABC'

因此,为了使比较结果不受用户个人的 vimrc 配置 &ignorecase 的影响,VimL 另 外提供两套限定大小写规则的比较运算符。在以上比较运算符之后再加 # 符号就表示 强制按大小写敏感方式比较,后面加上 ? 符号就表示强制按大小写不敏感的方式比较 。比如:

: echo 'abc' ==# 'ABC'
: echo 'abc' ==? 'ABC'

所以,强烈建议在进行字符串比较时,只用 ==#==? 系的比较运算符。当然由于 弱类型,字符串变量与数字变量其实是不可分的,所以将 ==#==? 之类的运用于 数字上比较,也是完全没有关系的。

此外,字符串除了相等性比较,还有匹配性比较,即用 =~ !~ 运算符判断一个字符 串是否匹配另一个作为正则表达式的字符串。正则表达式是另一个高级话题,这里不再展 开。当然,匹配运算符也有限定大小写是否敏感的衍生运算符,而且一般建议用 =~#!~# 匹配,毕竟正则表达本身有表达大小写的能力。

对于列表与字典变量,可进行相等性比较,但不能进行大小性比较。如果两个列表或字典 的对应元素都相等,则认为它们相等。此外,列表与字典还另外有个同例性比较运算符, isisnot。注意,这两个是类似 == 的运算符号,不是关键词,虽然它们用英 文单词来表示。同样地,也有 is#isnot? 的衍生运算,不过这主要为了语法的 统一整齐,其实 is# is?is 的结果是一致的。同例性比较的具体含义涉及实 例引用的概念,这留待后面的章节继续展开。

逻辑运算符

在 VimL 中的逻辑值所支持的或、且、非运算并无意外,分别用符号 || && ! 表 示就是,而且也支持短路计算特性。

  • expr1 || expr2, 只要 expr1expr2 其中一个是真,整个表达式就是真 ,两个都是假才是假。如果 expr1 已经是真的,expr2 不必计算就直接获得真的 结果。
  • expr1 && expr2,只有两个表达式都是真,结果才是真。如果 expr1 是假,则 不必计算 expr2 就返回结果假。
  • !expr ,对表达式真假取反。

if 分支流程

在了解这些逻辑值判断之后,理解 :if 的选择分支语句就容易多了,其完整语法结构 如下:

: if {expr}
:     {block_if}
: elseif {expr}
:     {block_elseif}
: elseif {expr}
:     {block_elseif}
: ......
: else
:     {block_else}
: endif
  • 首先执行的是 :if 后面的 {expr} 表达式,它可能只是个简单表达式,也可能是 多个逻辑值的复合运算,或者是很多表达式运算后得到的一个数字结果或逻辑值。只要 它最终能被解释为真,就执行其后的 {block_if} 语句块。
  • 如果 :if 的表达式为假,则依次寻找下一个表达式为真的 :elseif 语句块。
  • 最后如果没有真的 :if:elseif 条件满足,就执行 :else 语句块。
  • 只有 :if:endif 关键命令是必须的,:elseif:else 及其语句块是可 选的。
  • 在任一条件下,最多只有一个语句块被执行,然后流程跳转到 :endif 之后,结束整 个选择分流程。
  • 如果没有 :else 语句块,则在没有任何一个条件满足时,就不会执行任何一个语句块 。在有 :else 时,则至少会执行一个语句块。

注意:elseif 是直接将 elseif 这两个单词拼在一起的,中间没有空格,也 没有缩写。在许多不同的语言中,else if 的写法可能是变化最多的。

在 VimL 中,目前也没有 switch case 的类似语句,如果要实现多分支,只能叠加 :elseif

在非常简单的 if else endif 语句中,也可以用条件表达式 expr1 : expr2 ? expr3, 这类似于:

: if expr1
:     expr2
: else
:     expr3
: endif

整个表达式的值是 expr2expr3 的值。至于条件表达式是否可以嵌套,这个我也 不知道,反正我不用,也不建议用。就是条件表达式本身,也只推荐在一些有限的场合用 ,不推荐大量使用。因为一开始以为简单的逻辑判断,也可能以后会被修改的复杂起来, 仍然是用 :if 清晰一些。

然后推荐 :if 的一个特殊技法。VimL 并没有块注释,但是可以把多行语句嵌套放在 :if 0 ... :endif 之间,然后其内的语句就完全不会被执行了,甚至有不合 VimL 语 法的行也没事。然而仍然只建议这样“注释”合法的语句行,因为 :if 0 的潜意识是在 某个时刻可能需要将其改为 :if 1 以重新激活语句。这主要是用于更方便地切换测试 某块语句的运行效果。

: if 0
:     这里被注释了
: endif
: echo 'done'

*运算符优先级

本节为讲叙选择分支语句,也引申讲了不少有关语句、表达式、运算符的相关问题。落到 实处就是各种运算符的使用了,这就需要特别注意运算符的优先级问题。在此并不打算罗 列 VimL 的运算符优先级表,因为到这里可能还有些内容未覆盖到。而且运算符优先级的 问题太过琐碎,只看一遍教程并无多大助益,需要经常查文档,并自行验证。可以通过这 个命令 :help expression-syntax 查看表达式语法表,其中也基本是按运算符优先级 从低到高排列的,请经常查阅。

虽然由于运算符优先级会引起一些自己意想不到的问题,但回避这类问题的办法也是很简 单的,这里是一些建议:

  • 首先按自己的理解去使用运算符,要相信大部分语言的设计都是人性化的,不会故意设 些奇怪的违反常理的规则。
  • 对于自己不确定优先级,或者发现运算结果不符合自己所想时,添加小括号组合,使表 达式运算的次序明确化。
  • 拆分复杂表达式,借助中间变量,写成多行语句,不要写过长的语句。