Appearance
3.3 if-else
Rust 中 if-else 表达式的作用是实现条件分支。if-else 表达式的构成方式为:以 if 关键字开头,后面跟上条件表达式,后续是结果语句块,最后是可选的 else 块。条件表达式的类型必须是 bool。
示例如下:
rust
fn func(i: i32) -> std::cmp::Ordering {
if i < 0 {
std::cmp::Ordering::Less
} else if i > 0 {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
}
在 if 语句中,后续的结果语句块要求一定要用大括号包起来,不能省略,以便明确指出该 if 语句块的作用范围。这个规定是为了避免“悬空 else”导致的 bug。比如下面这段 C 代码:
rust
if (condition1)
if (condition2) {
}
else {
}
请问,这个 else 分支是与第一个 if 相匹配的,还是与第二个 if 相匹配的呢?从可读性上来说,答案是不够明显,容易出 bug。规定 if 和 else 后面必须有大括号,可读性会好很多。
相反,条件表达式并未强制要求用小括号包起来;如果加上小括号,编译器反而会认为这是一个多余的小括号,给出警告。
更重要的是,if-else 结构还可以当表达式使用,比如:
rust
let x : i32 = if condition { 1 } else { 10 };
//------------------- ^ -------- ^
//------------------- 这两个地方不要加分号
在这里,if-else 结构成了表达式的一部分。在 if 和 else 后面的大括号内,最后一条表达式不要加分号,这样一来,这两个语句块的类型就都是 i32,与赋值运算符左边的类型刚好匹配。所以,在 Rust 中,没有必要专门设计像 C/C++ 那样的三元运算符(?:
)语法,因为通过现有的设计可以轻松实现同样的功能。而且笔者认为这样的语法一致性、扩展性、可读性更好。
如果使用 if-else 作为表达式,那么一定要注意,if 分支和 else 分支的类型必须一致,否则就不能构成一个合法的表达式,会出现编译错误。如果 else 分支省略掉了,那么编译器会认为 else 分支的类型默认为()。所以,下面这种写法一定会出现编译错误:
rust
fn invalid_expr(cond: bool) -> i32 {
if cond {
42
}
}
编译器提示信息是:
rust
1= note: expected type `()`
found type `i32`
这看起来像是类型不匹配的错误,实际上是漏写了 else 分支造成的。如果此处编译器不报错,放任程序编译通过,那么在执行到 else 分支的时候,就只能返回一个未初始化的值,这在 Rust 中是不允许的。
3.3.1 loop
在 Rust 中,使用 loop 表示一个无限死循环。示例如下:
rust
fn main() {
let mut count = 0u32;
println!("Let's count until infinity!");
// 无限循环
loop {
count += 1;
if count == 3 {
println!("three");
// 不再继续执行后面的代码,跳转到 loop 开头继续循环
continue;
}
println!("{}", count);
if count == 5 {
println!("OK, that's enough");
// 跳出循环
break;
}
}
}
其中,我们可以使用 continue 和 break 控制执行流程。continue;语句表示本次循环内,后面的语句不再执行,直接进入下一轮循环。break;语句表示跳出循环,不再继续。
另外,break 语句和 continue 语句还可以在多重循环中选择跳出到哪一层的循环。
rust
fn main() {
// A counter variable
let mut m = 1;
let n = 1;
'a: loop {
if m < 100 {
m += 1;
} else {
'b: loop {
if m + n > 50 {
println!("break");
break 'a;
} else {
continue 'a;
}
}
}
}
}
我们可以在 loop while for 循环前面加上“生命周期标识符”。该标识符以单引号开头,在内部的循环中可以使用 break 语句选择跳出到哪一层。
与 if 结构一样,loop 结构也可以作为表达式的一部分。
rust
fn main() {
let v = loop {
break 10;
};
println!("{}", v);
}
在 loop 内部 break 的后面可以跟一个表达式,这个表达式就是最终的 loop 表达式的值。如果一个 loop 永远不返回,那么它的类型就是“发散类型”。示例如下:
rust
fn main() {
let v = loop {};
println!("{:?}", v);
}
编译器可以判断出 v 的类型是发散类型,而后面的打印语句是永远不会执行的死代码。
3.3.2 while
while 语句是带条件判断的循环语句。其语法是 while 关键字后跟条件判断语句,最后是结果语句块。如果条件满足,则持续循环执行结果语句块。示例如下:
rust
fn main() {
// A counter variable
let mut n = 1;
// Loop while `n` is less than 101
while n < 101 {
if n % 15 == 0 {
println!("fizzbuzz");
} else if n % 3 == 0 {
println!("fizz");
} else if n % 5 == 0 {
println!("buzz");
} else {
println!("{}", n);
}
// Increment counter
n += 1;
}
}
同理,while 语句中也可以使用 continue 和 break 来控制循环流程。
看到这里,读者可能会产生疑惑:loop{}
和 while true{}
循环有什么区别,为什么 Rust 专门设计了一个死循环,loop
语句难道不是完全多余的吗?
实际上不是。主要原因在于,相比于其他的许多语言,Rust 语言要做更多的静态分析。loop
和 while true
语句在运行时没有什么区别,它们主要是会影响编译器内部的静态分析结果。比如:
rust
let x;
loop { x = 1; break; }
println!("{}", x)
以上语句在 Rust 中完全合理。因为编译器可以通过流程分析推理出 x=1;必然在 println!之前执行过,因此打印变量 x 的值是完全合理的。而下面的代码是编译不过的:
rust
let x;
while true { x = 1; break; }
println!("{}", x);
因为编译器会觉得 while 语句的执行跟条件表达式在运行阶段的值有关,因此它不确定 x 是否一定会初始化,于是它决定给出一个错误:use of possibly uninitialized variable,也就是说变量 x 可能没有初始化。
3.3.3 for 循环
Rust 中的 for 循环实际上是许多其他语言中的 for-each 循环。Rust 中没有类似 C/C++的三段式 for 循环语句。举例如下:
rust
fn main() {
let array = &[1,2,3,4,5];
for i in array {
println!("The number is {}", i);
}
}
for 循环的主要用处是利用迭代器对包含同样类型的多个元素的容器执行遍历,如数组、链表、HashMap、HashSet 等。在 Rust 中,我们可以轻松地定制自己的容器和迭代器,因此也很容易使 for 循环也支持自定义类型。
for 循环内部也可以使用 continue 和 break 控制执行流程。
有关 for 循环的原理以及迭代器相关内容,参见第 24 章。