11.2 if 语句

我们在很早以前就已经见识过if语句了,如:

  1. # 假设在这之前已经定义了变量`name`,并为它赋予了某个值。
  2. if name == ""
  3. name = "handsome"
  4. end

if语句总是以if关键字开头,并以end关键字结尾。与if关键字处于同一行的必须是一个结果类型为Bool的表达式。我们通常称这样的表达式为条件表达式。在上面的这个例子中,name == ""显然就是条件表达式。只有当它的结果值为true时,if语句中的子语句,即name = "handsome",才会被执行。否则,其中的子语句就会被跳过而不执行。这里的子语句的数量可多可少,也可以是零个。因此,我们也可以称之为子语句组,或if子语句组。

当然了,Julia 中的if语句远不止这么简单。我们也可以让if语句在条件不满足的时候,即条件表达式的结果为false时,执行指定的子语句组。比如这样:

  1. if name == ""
  2. name = "handsome"
  3. else
  4. name = "dear " * name
  5. end

else关键字和end关键字之间的就是当条件不满足时会执行的子语句。与if子语句组一样,这里的子语句的数量也可以是任意的。我们可称之为else子语句组。

另外,if语句中的条件表达式可以不止一个。如果一条if语句拥有多个条件表达式,那么它们就必须各自独占一个分支,比如:

  1. if name == ""
  2. name = "handsome"
  3. elseif name == "Robert"
  4. name = "my master"
  5. else
  6. name = "dear " * name
  7. end

由关键字ifelseifelse引领的分支可以被分别叫做if分支、elseif分支和else分支。前两者都必须携带条件表达式,而后者则不能携带条件表达式。其中的elseif分支可以有任意个,并且必须处于if分支和else分支之间。

顾名思义,elseif分支属于一种备选分支。只有在前一个分支中的条件不满足时,它的条件表达式才会被求值。与if分支一样,一旦elseif分支中的条件满足,那么它包含的子语句就会被执行,而后续的分支则都会被跳过。从这个角度讲,else分支可以被视为默认分支。因为只有在前面所有分支中的条件都不满足时,它包含的子语句才会被执行。

正因为如此,我们可以说,在if语句里最多只会有一个分支被选中并执行。或者说,这些分支的执行是互斥的。而且,当默认分支不存在时,还可能会出现所有的分支都未被选中的情况。

最后,在编写if语句的时候,我们还有几点需要特别的注意。

第一点,if语句并不会自成一个作用域。换句话说,我们在其内部编写的那些有名称的程序定义都可以被外界的代码直接访问到。示例如下:

  1. julia> name = "Robert"
  2. "Robert"
  3. julia> if name == ""
  4. title = "handsome"
  5. elseif name == "Robert"
  6. title = "my master"
  7. else
  8. title = "dear " * name
  9. end
  10. "my master"
  11. julia> title
  12. "my master"
  13. julia>

可以看到,title是一个我在if语句里面定义的变量。而且,在这条if语句执行之后,我仍然可以在 REPL 环境中直接引用到这个变量。这就如同该变量被直接定义在了 REPL 环境中那样。更宽泛地讲,对于这种在if语句里面编写的程序定义,其作用域并非它们所属的if语句所占据的区域,而是包含了这条if语句的那个作用域。

如果你还使用过其他的编程语言的话,那么就很可能会发觉 Julia 的if语句在这方面与一些主流的编程语言并不相同。这也是我在此做出特别提示的主要原因。

第二点,你也许已经发现了,上例中的if语句是有结果值的,即:"my master"。实际上,在 Julia 中,if语句也属于一种复合表达式。我们同样可以把它赋给一个变量,例如:

  1. julia> result = if name == ""
  2. title = "handsome"
  3. elseif name == "Robert"
  4. title = "my master"
  5. else
  6. title = "dear " * name
  7. end
  8. "my master"
  9. julia> result
  10. "my master"
  11. julia>

这显然要比使用begin代码块复杂一些,因为if语句还会包含它自己的处理逻辑。if语句的结果值总会是实际执行的那个分支中的最后一条子语句所呈现的结果。在上例中,这个结果就是变量title所代表的值。

第三点,在if语句的条件表达式中可以存在多个条件。这就会涉及到多个条件之间的连接方式及其判断结果的合并方式。我们之前使用过的代表逻辑与的操作符&&就可以用于此处。示例如下:

  1. # 假设在这之前已经定义了变量`action`和`weather`,并为它们赋予了值。
  2. # 假设在这之前已经定义了变量`prompt`。
  3. if action == "walk" && weather == "rain"
  4. prompt = "Don't forget to bring an umbrella."
  5. end

这条if语句中的条件有两个,即:动作为“散步”和天气为“雨”。它们由操作符&&相连。所以,这个条件表达式所代表的整体条件就是,动作为“散步”并且天气为“雨”。

但要注意,只要多个条件之间均由&&相连,对条件的判断就会形成短路条件求值。更具体地说,Julia 会从左到右地依次判断条件表达式中的每一个条件,一旦它判断当前条件的结果为false,它就会停下来并忽略掉对后续条件的判断,进而直接裁决该条件表达式的最终结果为false。这就是“短路”一词所代表的求值行为。所以说,对于这样的条件表达式,只有其中所有条件的判断结果都为true,它的最终结果才可能是true。下面有一些示例可用于验证这个求值的过程:

  1. julia> action = "walk"; weather = "rain"; prompt = "";
  2. julia> is_walk(action) = (println("Check action (1)"); action == "walk")
  3. is_walk (generic function with 1 method)
  4. julia> is_rain(weather) = (println("Check weather (1)"); weather == "rain")
  5. is_rain (generic function with 1 method)
  6. julia> if is_walk(action) && is_rain(weather)
  7. prompt = "Don't forget to bring an umbrella."
  8. end
  9. Check action (1)
  10. Check weather (1)
  11. "Don't forget to bring an umbrella."
  12. julia> action = "sleep";
  13. julia> if is_walk(action) && is_rain(weather)
  14. prompt = "Don't forget to bring an umbrella."
  15. end
  16. Check action (1)
  17. julia>

函数is_walkis_rain包含的第一条语句都是打印语句。这样我们就可以知道这些函数是否被调用了。可以看到,在我把变量action的值改为sleep之后,那条if语句中的第二个条件(即is_rain(weather))就不再有被求值的机会了。这是因为该语句中的第一个条件(即is_walk(action))的求值结果变成了false,从而造成了“短路”。

我们再来看一个很不一样的例子:

  1. julia> is_sleep(action) = (println("Check action (2)"); action == "sleep")
  2. is_sleep (generic function with 1 method)
  3. julia> is_sunny(action) = (println("Check weather (2)"); action == "sunny")
  4. is_sunny (generic function with 1 method)
  5. julia> if is_sleep(action) || is_sunny(weather)
  6. prompt = "The idea looks good."
  7. end
  8. Check action (2)
  9. "The idea looks good."
  10. julia>

这条if语句也包含了两个条件,即:动作为“睡觉”和天气为“晴”。但不同的是,这两个条件是由操作符||相连的。所以,其整体条件就是,动作为“睡觉”或者天气为“晴”。

操作符||同样可用于短路条件求值。但正如我刚刚所讲,它代表的是“或者”,而不是“并且”。Julia 仍然会从左到右地依次判断条件表达式中的每一个条件,但只要它发现当前条件的判断结果为true,它就不会再继续做判断了,而是直接裁决该条件表达式的最终结果为true。也就是说,对于这样的条件表达式,只有其中所有条件的判断结果都为false,它的最终结果才可能是false

含有多个条件的条件表达式可以是很复杂的。这主要是因为其中可以同时出现&&||。并且,表达式的编写者还可以利用圆括号改变条件求值的默认次序。下面是一个还算简单的示例:

  1. julia> action = "drive"; weather = "rain"; road_condition = "bad"; prompt = ""
  2. ""
  3. julia> if weather != "sunny" && (road_condition != "good" && (action == "ride" || action == "drive"))
  4. prompt = "Please pay attention to traffic safety."
  5. end
  6. "Please pay attention to traffic safety."
  7. julia>

在一般情况下,为了提高代码的可读性和逻辑的清晰性,我们往往会在条件表达式中添加一些必要的圆括号。即便这些圆括号对于条件的判断次序没有影响,也是如此。当然了,无论怎样,我们都不应该编写过长的条件表达式。如果你编写的条件表达式很长,那么很可能就说明你应该对它进行整理(或者说重构)了。

好了,我再复述一下我们需要特别注意的三点,即:if语句不会自成一个作用域、if语句属于一种复合表达式,if语句中的条件表达式可以包含多个条件(实际上,所有的条件表达式都是如此)。希望你能够在编写if语句的时候想起它们。