if-else: 表达式还是语句?

Posted on April 25, 2020 by τ

if-else 语句应该是语句还是表达式呢?这是一个有趣的问题,不同答案会使得编程语言的表现形式很不一样。

表达式,是运行后会产生值的语句,例如 a+ba>btrue 等。简单来说,如果一个语句是表达式,那么这个语句可以写于赋值符号的右边,例如 let a = a > b。而除了表达式语句以外的语句运行后不会产生值。可以看出,表达式严格而言是语句的一个子集。本文中,我使用『语句』的狭义含义,也就是仅表示语句中不包含表达式语句的部分。

一般在函数式编程语言中,例如 Haskell 中,if-else 是表达式。

let a = if 2 > 1 then 3 else 1

很多支持函数式范式的语言也有类似的表达式,如 Python:

a = 3 if 3 > 1 else 1
# 3 if 3 > 1 else 1 执行后会产生 3 或者 1

但是 Python 中也存在不是表达式的 if-else 语句,如:

if 3 > 1:
    a = 3
else:
    a = 1
# `a` 直接在 *if-else* 的分支中被赋值的,*if-else* 语句本身并没有产生一个值

在上面三个示例中我们可以看出:

  • if-else 如果是表达式,那么 else 是不可缺少的。
  • 作为表达式的 if-else 的表达能力更弱。
  • if-else 作为表达式设计上更加优雅(个人看法)。
  • 在函数式编程语言中if-else 作为表达式是必须的(因为函数式语言是 immutable, 所以 if-else 必须将其执行结果通过表达式值表现出来)。

这就出现了一个设计上的矛盾:if-else 作为表达式或者语句,各有其优缺点。而且如果要引入另一种范式,可能需要增加新的语法,如 Python。Rust 很优雅地解决了这个矛盾。

Rust 中 if-else 语句也是表达式,且其每一个分支必须产生相同类型的值。if-else 之后都必须是大括号包括的代码块,代码块的最后一行即是产生的值。在 else 分支缺省的情况下,else 分支产生的值为 (),即 Rust 中的 nothing。

let a = if x > 2 {
        x-10
    } else {
        x + 10
    };

如果在代码块的最后一行的表达式加 ;, 会忽略表达式产生的值,即代码块产生的值为 ()。也就是说,如果 if 后的代码块产生的值是 ()时,可以省略 else

let mut x = 10;
if x > 10 {
    x -= 10;
} // 因为 if 产生的值为 `()`,  结尾不需要加 `;`

从上面的几个例子可以看到,Rust 中 if-else 语句虽然是表达式,却同时可以具有语句的灵活性。通过引入代码块的产生值的概念,; 忽略表达式的值等设计,巧妙地将两种看似不同的设计融合在一起。相比 Python 通过两种语法来支持两种不同范式的设计,Rust 只用了一种语法却可以兼容两种写法,实在是妙啊。

对于这个问题,我们可以思考的更加深入一些。上面提到了在纯函数式编程语言中,因为 immutability ,在执行分支的时候,分支不能改变之前的『变量』的值,分支执行的效果必须通过表达式的返回值表达出来(暂时忽略 IO 产生的效果)。所以 if-else 语句必须作为语句存在于函数式编程语言中。我们可以得出一个武断的结论:如果一门编程语言要支持 immutability, 那么 if-else 必须是表达式。

回到 Rust,考虑下面的功能:现要根据 x 的值是否大于 20 来设定 y 的值为 1 或者 0。如果允许使用 mut, 实现就是我们很熟悉的样子:

let x = 20;
let mut y = 0;
if x > 20 {
    y = 1;
}

但是 如果不允许使用 mut,我们就无法在 if 后面的代码块中去更改 y 的值,而必须通过表达式的值来设置 y

let x = 20;
let y = if x > 20 { 1 } else { 0 };

可以看到之所以会出现 if-else 的两种不同设计,其更加根本的原因在于是否是 immutable。而 Rust 恰好想同时支持 mutability 和 immutability,所以其 if-else 语句采用了两种特性可以兼容的设计。

需要指出的是,if-else 作为表达式在 immutable 编程语言中是必须的,在 mutable 编程语言中也是可以存在的。例如 C 语言中有 :? 这种三目运算符,Python 中也有 if-else 作为表达式的语法。这不是必须的,但是有时候能让代码看起来更加简洁。C 语言中的三目运算符一般可以翻译成不需要分支预测的汇编代码(SET 系列指令),从而提高代码效率。

Rust 这个语法设计上的小细节实在是让我感到惊喜。在一门编程语言的设计中,语法和语言支持的特性不是独立存在的,他们常常是相互影响的。两者是形式和内容的关系。内容需要某种形式去表达,反过来,形式也限制了内容的表达。而一门优秀的语言应该充分考虑两者的相互作用。