Appearance
3.2 表达式
在 The Rust Programming Language 中有这样一句话:
Because Rust is an expression-based language, this is an important distinction to understand. If you add a semicolon to the end of an expression, you turn it into a statement.
Rust 基本上就是一个表达式语言。“表达式”在 Rust 程序中占据着重要位置,表达式的功能非常强大。Rust 中的表达式语法具有非常好的“一致性”,每种表达式都可以嵌入到另外一种表达式中,组成更强大的表达式。
Rust 的表达式包括字面量表达式、方法调用表达式、数组表达式、索引表达式、单目运算符表达式、双目运算符表达式等。Rust 表达式又可以分为“左值”(lvalue)和“右值”(rvalue)两类。所谓左值,意思是这个表达式可以表达一个内存地址。因此,它们可以放到赋值运算符左边使用。其他的都是右值。
3.2.1 运算表达式
Rust 的算术运算符包括:加(+
)、减(-
)、乘(*
)、除(/
)、求余(%
),示例如下:
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 禁止连续比较,示例如下:
rust
fn f(a: bool, b: bool, c: bool) -> bool {
a == b == c
}
编译时,编译器提示“连续比较运算符必须加上括号”:
rust
$ 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 的位运算符具体如下:
运算符 | 作用 |
---|---|
! | 按位取反(注意不是~ 符号) |
& | 按位与 |
| | 按位或 |
^ | 按位异或 |
<< | 左移 |
>> | 右移 |
示例如下:
rust
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);
}
执行结果为:
txt
01010101
10100000
11111010
01011010
10100000
00001010
Rust 的逻辑运算符具体见表 3-2。
运算符 | 作用 |
---|---|
&& | 逻辑与 |
|| | 逻辑或 |
! | 逻辑取反 |
取反运算符既支持“逻辑取反”也支持“按位取反”,它们是同一个运算符,根据类型决定执行哪个操作。如果被操作数是 bool 类型,那么就是逻辑取反;如果被操作数是其他数字类型,那么就是按位取反。
bool 类型既支持“逻辑与”、“逻辑或”,也支持“按位与”、“按位或”。它们的区别在于,“逻辑与”、“逻辑或”具备“短路”功能。示例如下:
rust
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());
}
执行结果为:
txt
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 赋值表达式
一个左值表达式、赋值运算符(=)和右值表达式,可以构成一个赋值表达式。示例如下:
rust
// 声明局部变量,带 mut 修饰
let mut x : i32 = 1;
// x 是 mut 绑定,所以可以为它重新赋值
x = 2;
上例中,x=2 是一个赋值表达式,它末尾加上分号,才能组成一个语句。赋值表达式具有“副作用”:当它执行的时候,会把右边表达式的值“复制或者移动”(copy or move)到左边的表达式中。关于复制和移动的语义区别,请参见第 11 章的内容。赋值号左右两边表达式的类型必须一致,否则是编译错误。
赋值表达式也有对应的类型和值。这里不是说赋值表达式左操作数或右操作数的类型和值,而是说整个表达式的类型和值。Rust 规定,赋值表达式的类型为 unit,即空的 tuple ()
。示例如下:
rust
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 语言允许连续赋值,但这个设计没有带来任何性能提升,反而在某些场景下给用户带来了代码不够清晰直观的麻烦。举个例子:
rust
#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 也支持组合赋值表达式,+
、-
、*
、/
、%
、&
、|
、^
、<<
、>>
这几个运算符可以和赋值运算符组合成赋值表达式。示例如下:
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 中,语句块也可以是表达式的一部分。语句和表达式的区分方式是后面带不带分号(;)。如果带了分号,意味着这是一条语句,它的类型是();如果不带分号,它的类型就是表达式的类型。示例如下:
rust
// 语句块可以是表达式,注意后面有分号结尾,x 的类型是 ()
let x : () = { println!("Hello."); };
// Rust 将按顺序执行语句块内的语句,并将最后一个表达式类型返回,y 的类型是 i32
let y : i32 = { println!("Hello."); 5 };
同理,在函数中,我们也可以利用这样的特点来写返回值:
rust
fn my_func() -> i32 {
// ... blablabla 各种语句
100
}
注意,最后一条表达式没有加分号,因此整个语句块的类型就变成了 i32,刚好与函数的返回类型匹配。这种写法与 return 100;
语句的效果是一样的,相较于 return 语句来说没有什么区别,但是更加简洁。特别是用在后面讲到的闭包 closure 中,这样写就方便轻量得多。