第3章 语句和表达式
语句和表达式是Rust语言实现控制逻辑的基本单元。
3.1 语句
一个Rust程序,是从main函数开始执行的。而函数体内,则是由一条条语句组成的。
Rust程序里,表达式(Expression)和语句(Statement)是完成流程控制、计算求值的主要工具,也是本节要讲的核心部分。在Rust程序里面,表达式可以是语句的一部分,反过来,语句也可以是表达式的一部分。一个表达式总是会产生一个值,因此它必然有类型;语句不产生值,它的类型永远是()。如果把一个表达式加上分号,那么它就变成了一个语句;如果把语句放到一个语句块中包起来,那么它就可以被当成一个表达式使用。
3.2 表达式
在Rust Reference中有这样一句话:
Rust is primarily an expression language.
Rust基本上就是一个表达式语言。“表达式”在Rust程序中占据着重要位置,表达式的功能非常强大。Rust中的表达式语法具有非常好的“一致性”,每种表达式都可以嵌入到另外一种表达式中,组成更强大的表达式。
Rust的表达式包括字面量表达式、方法调用表达式、数组表达式、索引表达式、单目运算符表达式、双目运算符表达式等。Rust表达式又可以分为“左值”(lvalue)和“右值”(rvalue)两类。所谓左值,意思是这个表达式可以表达一个内存地址。因此,它们可以放到赋值运算符左边使用。其他的都是右值。
3.2.1 运算表达式
Rust的算术运算符包括:加(+)、减(-)、乘(*)、除(/)、求余(%),示例如下:
fn main() { let x = 100; let y = 10; println!("{} {} {} {} {}", x + y, x - y, x * y, x / y, x % y); }
在上面例子中,x + y、x - y这些都是算术运算表达式,它们都有自己的值和类型。常见的整数、浮点数类型都支持这几种表达式。它们还可以被重载,让自定义的类型也支持这几种表达式。运算符重载相关的内容会在第26章介绍标准库的时候会详细说明。
Rust的比较运算符包括:等于(==)、不等于(! =)、小于(<)、大于(>)、小于等于(<=)、大于等于(>=)。比较运算符的两边必须是同类型的,并满足PartialEq约束。比较表达式的类型是bool。另外,Rust禁止连续比较,示例如下:
fn f(a: bool, b: bool, c: bool) -> bool { a == b == c }
编译时,编译器提示“连续比较运算符必须加上括号”:
$ rustc --crate-type rlib test.rs error: chained comparison operators require parentheses --> test.rs:2:7 | 2 | a == b == c | ^^^^^^^^^ error: aborting due to previous error
这也是故意设计的,避免不同知识背景的用户对这段代码有不同的理解。
Rust的位运算符具体见表3-1。
表3-1
示例如下:
fn main() { let num1 : u8 = 0b_1010_1010; let num2 : u8 = 0b_1111_0000; println!("{:08b}", !num1); println!("{:08b}", num1 & num2); println!("{:08b}", num1 | num2); println!("{:08b}", num1 ^ num2); println!("{:08b}", num1 << 4); println!("{:08b}", num1 >> 4); }
执行结果为:
$ ./test 01010101 10100000 11111010 01011010 10100000 00001010
Rust的逻辑运算符具体见表3-2。
表3-2
取反运算符既支持“逻辑取反”也支持“按位取反”,它们是同一个运算符,根据类型决定执行哪个操作。如果被操作数是bool类型,那么就是逻辑取反;如果被操作数是其他数字类型,那么就是按位取反。
bool类型既支持“逻辑与”、“逻辑或”,也支持“按位与”、“按位或”。它们的区别在于,“逻辑与”、“逻辑或”具备“短路”功能。示例如下:
fn f1() -> bool { println!("Call f1"); true } fn f2() -> bool { println!("Call f2"); false } fn main() { println!("Bit and: {}\n", f2() & f1()); println!("Logic and: {}\n", f2() && f1()); println!("Bit or: {}\n", f1() | f2()); println!("Logic or: {}\n", f1() || f2()); }
执行结果为:
$ ./test Call f2 Call f1 Bit and: false Call f2 Logic and: false Call f1 Call f2 Bit or: true Call f1 Logic or: true
可以看到,所谓短路的意思是:
❏ 对于表达式A&&B,如果A的值是false,那么B就不会执行求值,直接返回false。
❏ 对于表达式A||B,如果A的值是true,那么B就不会执行求值,直接返回true。
而“按位与”、“按位或”在任何时候都会先执行左边的表达式,再执行右边的表达式,不会省略。
另外需要提示的一点是,Rust里面的运算符优先级与C语言里面的运算符优先级设置是不一样的,有些细微的差别。不过这并不是很重要。不论在哪种编程语言中,我们都建议,如果碰到复杂一点的表达式,尽量用小括号明确表达计算顺序,避免依赖语言默认的运算符优先级。因为不同知识背景的程序员对运算符优先级顺序的记忆是不同的。
3.2.2 赋值表达式
一个左值表达式、赋值运算符(=)和右值表达式,可以构成一个赋值表达式。示例如下:
// 声明局部变量,带 mut 修饰 let mut x : i32 = 1; // x 是 mut 绑定,所以可以为它重新赋值 x = 2;
上例中,x = 2是一个赋值表达式,它末尾加上分号,才能组成一个语句。赋值表达式具有“副作用”:当它执行的时候,会把右边表达式的值“复制或者移动”(copy or move)到左边的表达式中。关于复制和移动的语义区别,请参见第11章的内容。赋值号左右两边表达式的类型必须一致,否则是编译错误。
赋值表达式也有对应的类型和值。这里不是说赋值表达式左操作数或右操作数的类型和值,而是说整个表达式的类型和值。Rust规定,赋值表达式的类型为unit,即空的tuple ()。示例如下:
fn main() {
let x = 1;
let mut y = 2;
// 注意这里专门用括号括起来了
let z = (y = x);
println!("{:? }", z);
}
编译,执行,结果为:()。
Rust这么设计是有原因的,比如说可以防止连续赋值。如果你有x: i32、y: i32以及z: i32,那么表达式z = y = x会发生编译错误。因为变量z的类型是i32但是却用()对它初始化了,编译器是不允许通过的。
C语言允许连续赋值,但这个设计没有带来任何性能提升,反而在某些场景下给用户带来了代码不够清晰直观的麻烦。举个例子:
#include <stdio.h> int main() { int x = 300; char y; int z; z = y = x; printf("%d %d %d", x, y, z); }
在这种情况下,如果变量x、y、z的类型不一样,而且在赋值的时候可能发生截断,那么用户很难一眼看出最终变量z的值是与x相同,还是与y相同。
这个设计同样可以防止把==写成=的错误。比如,Rust规定,在if表达式中,它的条件表达式类型必须是bool类型,所以if x = y {}这样的代码是无论如何都编译不过的,哪怕x和y的类型都是bool也不行。赋值表达式的类型永远是(),它无法用于if条件表达式中。
Rust也支持组合赋值表达式,+、-、*、/、%、&、|、^、<<、>>这几个运算符可以和赋值运算符组合成赋值表达式。示例如下:
fn main() { let x = 2; let mut y = 4; y += x; y *= x; println!("{} {}", x, y); }
LEFT OP= RIGHT这种写法,含义等同于LEFT = LEFT OP RIGHT。所以,y += x的意义相当于y = y + x,依此类推。
Rust不支持++、--运算符,请使用+= 1、-= 1替代。
3.2.3 语句块表达式
在Rust中,语句块也可以是表达式的一部分。语句和表达式的区分方式是后面带不带分号(;)。如果带了分号,意味着这是一条语句,它的类型是();如果不带分号,它的类型就是表达式的类型。示例如下:
// 语句块可以是表达式,注意后面有分号结尾,x的类型是() let x : () = { println! ("Hello."); }; // Rust将按顺序执行语句块内的语句,并将最后一个表达式类型返回,y的类型是 i32 let y : i32 = { println! ("Hello."); 5 };
同理,在函数中,我们也可以利用这样的特点来写返回值:
fn my_func() -> i32 {
// ... blablabla 各种语句
100 }
注意,最后一条表达式没有加分号,因此整个语句块的类型就变成了i32,刚好与函数的返回类型匹配。这种写法与return 100;语句的效果是一样的,相较于return语句来说没有什么区别,但是更加简洁。特别是用在后面讲到的闭包closure中,这样写就方便轻量得多。
3.3 if-else
Rust中if-else表达式的作用是实现条件分支。if-else表达式的构成方式为:以if关键字开头,后面跟上条件表达式,后续是结果语句块,最后是可选的else块。条件表达式的类型必须是bool。
示例如下:
fn func(i : i32) -> bool { if n < 0 { print!("{} is negative", n); } else if n > 0 { print!("{} is positive", n); } else { print!("{} is zero", n); } }
在if语句中,后续的结果语句块要求一定要用大括号包起来,不能省略,以便明确指出该if语句块的作用范围。这个规定是为了避免“悬空else”导致的bug。比如下面这段C代码:
if (condition1) if (condition2) { } else { }
请问,这个else分支是与第一个if相匹配的,还是与第二个if相匹配的呢?从可读性上来说,答案是不够明显,容易出bug。规定if和else后面必须有大括号,可读性会好很多。
相反,条件表达式并未强制要求用小括号包起来;如果加上小括号,编译器反而会认为这是一个多余的小括号,给出警告。
更重要的是,if-else结构还可以当表达式使用,比如:
let x : i32 = if condition { 1 } else { 10 };
//------------------- ^ -------- ^
//------------------- 这两个地方不要加分号
在这里,if-else结构成了表达式的一部分。在if和else后面的大括号内,最后一条表达式不要加分号,这样一来,这两个语句块的类型就都是i32,与赋值运算符左边的类型刚好匹配。所以,在Rust中,没有必要专门设计像C/C++那样的三元运算符(? :)语法,因为通过现有的设计可以轻松实现同样的功能。而且笔者认为这样的语法一致性、扩展性、可读性更好。
如果使用if-else作为表达式,那么一定要注意,if分支和else分支的类型必须一致,否则就不能构成一个合法的表达式,会出现编译错误。如果else分支省略掉了,那么编译器会认为else分支的类型默认为()。所以,下面这种写法一定会出现编译错误:
fn invalid_expr(cond: bool) -> i32 { if cond { 42 } }
编译器提示信息是:
= note: expected type `()` found type `i32`
这看起来像是类型不匹配的错误,实际上是漏写了else分支造成的。如果此处编译器不报错,放任程序编译通过,那么在执行到else分支的时候,就只能返回一个未初始化的值,这在Rust中是不允许的。
3.3.1 loop
在Rust中,使用loop表示一个无限死循环。示例如下:
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语句还可以在多重循环中选择跳出到哪一层的循环。
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结构也可以作为表达式的一部分。
fn main() { let v = loop { break 10; }; println!("{}", v); }
在loop内部break的后面可以跟一个表达式,这个表达式就是最终的loop表达式的值。如果一个loop永远不返回,那么它的类型就是“发散类型”。示例如下:
fn main() { let v = loop {}; println!("{}", v); }
编译器可以判断出v的类型是发散类型,而后面的打印语句是永远不会执行的死代码。
3.3.2 while
while语句是带条件判断的循环语句。其语法是while关键字后跟条件判断语句,最后是结果语句块。如果条件满足,则持续循环执行结果语句块。示例如下:
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语句在运行时没有什么区别,它们主要是会影响编译器内部的静态分析结果。比如:
let x; loop { x = 1; break; } println!("{}", x)
以上语句在Rust中完全合理。因为编译器可以通过流程分析推理出x=1;必然在println!之前执行过,因此打印变量x的值是完全合理的。而下面的代码是编译不过的:
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循环语句。举例如下:
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章。