哥哥艾斯和弟弟路飞
LISP 程序员知道一切的价值(value),但不了解其代价。
------Alan Perlis
本章将介绍 Rust 表达式,它是函数体的组成部分,因而也是大部分 Rust 代码的组成部分。Rust 中的大多数内容是表达式。本章将探索表达式的强大功能以及如何克服它的局限性。我们将介绍在 Rust 中完全面向表达式的控制流,以及 Rust 的基础运算符如何独立工作和组合工作。
某些概念(比如闭包和迭代器)严格来说也属于这一类,但略显深奥,我们稍后将用单独的一章来介绍它们。目前,我们的目标是在这"区区几页"中涵盖尽可能多的语法。
6.1 表达式语言
Rust 乍看起来和 C 家族的语言相似,但这只是假象。在 C 语言中,表达式和语句之间有明显的区别,表达式看起来是这样的:
scss
5 * (fahr-32) / 9
而语句看起来更像这样:
arduino
for (; begin != end; ++begin) {
if (*begin == target)
break;
}
表达式有值,而语句没有。
Rust 是所谓的表达式语言。这意味着它遵循更古老的传统,可以追溯到 Lisp,在 Lisp 中,表达式能完成所有工作。
在 C 中,if
和 switch
是语句,它们不生成值,也不能在表达式中间使用。而在 Rust 中,if
和 match
可以 生成值。第 2 章介绍过一个生成数值的 match
表达式:
scss
pixels[r * bounds.0 + c] =
match escapes(Complex { re: point.0, im: point.1 }, 255) {
None => 0,
Some(count) => 255 - count as u8
};
if
表达式可用于初始化变量:
rust
let status =
if cpu.temperature <= MAX_TEMP {
HttpStatus::Ok
} else {
HttpStatus::ServerError // 服务程序出错了
};
match
表达式可以作为参数传给函数或宏:
rust
println!("Inside the vat, you see {}.",
match vat.contents {
Some(brain) => brain.desc(),
None => "nothing of interest"
});
这解释了为什么 Rust 没有 C 那样的三元运算符(expr1 ? expr2 : expr3
)。在 C 语言中,三元运算符是一个表达式级别的类似 if
语句的东西。这在 Rust 中是多余的:if
表达式足以处理这两种情况。
笔记
在JavaScript中三元运算符很实用也很常见,明显在Rust中用if即可
C 中的大多数控制流工具是语句。而在 Rust 中,它们都是表达式。
6.2 优先级与结合性
表 6-1 总结了 Rust 的表达式语法。本章将讨论所有这些类型的表达式。这里的运算符已按优先级顺序列出,从最高到最低。(与大多数编程语言一样,当一个表达式包含多个相邻的运算符时,Rust 会根据运算符优先级 来确定运算顺序。例如,在 limit < 2 * broom.size + 1
中,.
运算符具有最高优先级,因此会最先访问字段。)
表 6-1:表达式
表达式类型 | 示例 | 相关特型 |
---|---|---|
数组字面量 | [1, 2, 3] |
|
数组重复表达式 | [0; 50] |
|
元组 | (6, "crullers") |
|
分组 | (2 + 2) |
|
块 | { f(); g() } |
|
控制流表达式 | if ok { f() } if ok { 1 } else { 0 } if let Some(x) = f() { x } else { 0 } match x { None => 0, _ => 1 } for v in e { f(v); } while ok { ok = f(); } while let Some(x) = it.next() { f(x); } loop { next_event(); } break continue return 0 |
std::iter::IntoIterator |
宏调用 | println!("ok") |
|
路径 | std::f64::consts::PI |
|
结构体字面量 | Point {x: 0, y: 0} |
|
元组字段访问 | pair.0 |
Deref 、DerefMut |
结构体字段访问 | point.x |
Deref 、DerefMut |
方法调用 | point.translate(50, 50) |
Deref 、DerefMut |
函数调用 | stdin() |
Fn(Arg0, ...) -> T 、FnMut(Arg0, ...) -> T 、FnOnce(Arg0, ...) -> T |
索引 | arr[0] |
Index 、IndexMutDeref 、DerefMut |
错误检查 | create_dir("tmp")? |
|
逻辑非 / 按位非 | !ok |
Not |
取负 | -num |
Neg |
解引用 | *ptr |
Deref 、DerefMut |
借用 | &val |
|
类型转换 | x as u32 |
|
乘 | n * 2 |
Mul |
除 | n / 2 |
Div |
取余(取模) | n % 2 |
Rem |
加 | n + 1 |
Add |
减 | n - 1 |
Sub |
左移 | n << 1 |
Shl |
右移 | n >> 1 |
Shr |
按位与 | n & 1 |
BitAnd |
按位异或 | n ^ 1 |
BitXor |
按位或 | n 竖线 1 |
BitOr |
小于 | n < 1 |
std::cmp::PartialOrd |
小于等于 | n <= 1 |
std::cmp::PartialOrd |
大于 | n > 1 |
std::cmp::PartialOrd |
大于等于 | n >= 1 |
std::cmp::PartialOrd |
等于 | n == 1 |
std::cmp::PartialEq |
不等于 | n != 1 |
std::cmp::PartialEq |
逻辑与 | x.ok && y.ok |
|
逻辑或 | x.ok 双竖线 backup.ok |
|
右开区间范围 | start .. stop |
|
右闭区间范围 | start ..= stop |
|
赋值 | x = val |
|
复合赋值 | x *= 1 x /= 1 x %= 1 x += 1 x -= 1 x <<= 1 x >>= 1 x &= 1 x ^= 1 x 竖线= 1 |
DivAssign RemAssign AddAssign SubAssign ShlAssign ShrAssign BitAndAssign BitXorAssign BitOrAssign MulAssign |
闭包 | 竖线x, y竖线 x + y |
注意!
上面内容由于在表格模式中
|
符号会默解析为表格符号,导致布局异常,因此表达式中的|
使用中文替代
所有可以链式书写的运算符都是左结合的。也就是说,诸如 a - b - c
之类的操作链会分组为 (a - b) - c
,而不是 a - (b - c)
。所有可以这样链式书写的运算符都遵循左结合规则:
csharp
* / % + - << >> & ^ | && || as
比较运算符、赋值运算符和范围运算符(..
和 ..=
)则根本无法链式书写。
6.3 块与分号
块是一种最通用的表达式。一个块生成一个值,并且可以在任何需要值的地方使用:
ini
let display_name = match post.author() {
Some(author) => author.name(),
None => {
let network_info = post.get_network_metadata()?;
let ip = network_info.client_address();
ip.to_string()
}
};
Some(author) =>
之后的代码是简单表达式 author.name()
,None =>
之后的代码是一个块表达式,它们对 Rust 来说没什么不同。块表达式的值是最后一个表达式 ip.to_string()
的值。
请注意,ip.to_string()
方法调用后面没有分号。大多数 Rust 代码行以分号或花括号结尾,就像 C 或 Java 一样。如果一个块看起来很像 C 代码,在你熟悉的每个地方都有分号,那么它就会像 C 的块一样运行,并且其值为 ()
。正如第 2 章提到的,当块的最后一行不带分号时,就以最后这个表达式的值而不是通常的 ()
作为块的值。
在某些语言,尤其是 JavaScript 中,可以省略分号,并且该语言会简单地替你填充分号------这是一个小小的便捷特性。但 Rust 不一样。在 Rust 中,分号是有实际意义的:
ini
let msg = {
// let声明:分号总是必需的
let dandelion_control = puffball.open();
// 带分号的表达式:调用方法,丢弃返回值
dandelion_control.release_all_seeds(launch_codes);
// 无分号的表达式:调用方法,返回值将存入`msg`
dandelion_control.get_status()
};
块可以包含多个声明并在末尾生成值,这是一个很好的特性,你很快就会适应它。但它的一个缺点是,如果你不小心遗漏了分号,则会导致奇怪的错误消息:
javascript
...
if preferences.changed() {
page.compute_size() // 糟糕,丢了分号
}
...
如果在 C 或 Java 程序中犯了同样的错误,那么编译器会直接指出你漏了一个分号。但 Rust 会这么说:
go
error: mismatched types
22 | page.compute_size() // 糟糕,丢了分号
| ^^^^^^^^^^^^^^^^^^^- help: try adding a semicolon: `;`
| |
| expected (), found tuple
|
= note: expected unit type `()`
found tuple `(u32, u32)`
由于缺少分号,块的值将是 page.compute_size()
返回的任何值,但是没有 else
的 if
必定返回 ()
。幸好,Rust 已经针对这类错误做出改进,并会建议添加分号。
6.4 声明
除了表达式和分号,块还可以包含任意数量的声明。最常见的是 let
声明,它会声明局部变量:
bash
let name: type = expr;
类型和初始化代码是可选的,分号则是必需的。与 Rust 中的所有标识符一样,变量名必须以字母或下划线开头,并且只能在第一个字符之后包含数字。Rust 中的"字母"是广义的,包括希腊字母、带重音的拉丁字符和更多符号------符合 Unicode 标准中附件 #31 要求的一切字符(也包括中文)。不允许使用表情符号。
let
声明可以在不初始化变量的情况下声明变量,然后再用赋值语句来初始化变量。这在某些情况下很有用,因为有时确实应该在某种控制流结构的中间初始化变量:
ini
let name;
if user.has_nickname() {
name = user.nickname();
} else {
name = generate_unique_name();
user.register(&name);
}
这里有两种初始化局部变量 name
的方式,但无论采用哪种方式,都只会初始化一次,所以 name
不需要声明为 mut
。
在初始化之前就使用变量是错误的。(这与"移动后又使用值"的错误紧密相关。Rust 确实非常希望你只使用存在的值。)
你可能偶尔会看到似乎在重新声明现有变量的代码,如下所示:
scss
for line in file.lines() {
let line = line?;
...
}
这个 let
声明会创建一个不同类型的、新的(第二个)变量。第一个 line
变量的类型是 Result<String, io::Error>
。第二个 line
变量则是 String
。第二个定义会在所处代码块的其余部分代替第一个定义。这叫作遮蔽(shadowing),在 Rust 程序中很常见。该代码等效于如下内容:
ini
for line_result in file.lines() {
let line = line_result?;
...
}
本书会坚持在这种情况下使用 _result
后缀,以便让不同变量具有不同的名称。
块还可以包含语法项声明 (item declaration)。语法项是指可以在程序或模块中的任意地方出现的声明,比如 fn
、struct
或 use
。
后面的章节会详细介绍这些语法项。现阶段,用 fn
这个例子就足够了。任何块都可能包含一个 fn
:
rust
use std::io;
use std::cmp::Ordering;
fn show_files() -> io::Result<()> {
let mut v = vec![];
...
fn cmp_by_timestamp_then_name(a: &FileInfo, b: &FileInfo) -> Ordering {
a.timestamp.cmp(&b.timestamp) // 首先,比较时间戳
.reverse() // 最新的文件优先
.then(a.path.cmp(&b.path)) // 通过路径做二级比较
}
v.sort_by(cmp_by_timestamp_then_name);
...
}
当在块内声明一个 fn
时,它的作用域是整个块,也就是说,它可以在整个封闭块内部使用 。但是嵌套的 fn
无法访问恰好在同一作用域内的局部变量或参数。例如,函数 cmp_by_timestamp_then_name
不能直接使用 v
。(封闭块与闭包不同。Rust 也有闭包,闭包可以看到封闭块作用域内的变量。请参阅第 14 章。)
块甚至可以包含完整的模块。这可能看起来有点儿过分(真的需要把语言的每一部分都嵌进任何其他部分吗?),但是程序员(特别是使用宏的程序员)总是有办法为语言提供的每一种独立语法找到用武之地。
6.5 if
与 match
if
表达式的形式我们很眼熟:
arduino
if condition1 {
block1
} else if condition2 {
block2
} else {
block_n
}
每个 condition
都必须是 bool
类型的表达式,依照 Rust 的风格,不会隐式地将数值或指针转换为布尔值。
与 C 不同,条件周围不需要圆括号。事实上,如果出现了不必要的圆括号,那么 rustc
会给出警告。但花括号是必需的。
else if
块以及最后的 else
是可选的。没有 else
块的 if
表达式的行为与具有空的 else
块完全相同。
match
表达式类似于 C 语言中的 switch
语句,但更灵活。下面是一个简单的例子:
ini
match code {
0 => println!("OK"),
1 => println!("Wires Tangled"),
2 => println!("User Asleep"),
_ => println!("Unrecognized Error {}", code)
}
这类似于 switch
语句的用途。它将执行此 match
表达式的四个分支之一,具体执行哪个分支取决于 code
的值。通配符模式 _
会匹配所有内容。这类似于 switch
语句中的 default:
语句,不过它必须排在最后。将 _
模式放在其他模式之前意味着它会优先于其他模式。这样一来,其他模式将永远没机会匹配到(编译器会发出警告)。
编译器可以使用跳转表来优化这种 match
,就像 C++ 中的 switch
语句一样。当 match
的每个分支都生成一个常量值时,也会应用与 C++ 类似的优化。在这种情况下,编译器会构建出这些值的数组,并将各个 match
项编译为数组访问。除了边界检查,编译后的代码中根本不存在任何分支。
match
的多功能性源于每个分支 =>
左侧支持的多种模式 (pattern)。在上面的例子中,每个模式只是一个常量整数。我们还展示了用以区分两种 Option
值的 match
表达式:
rust
match params.get("name") {
Some(name) => println!("Hello, {}!", name),
None => println!("Greetings, stranger.")
}
对模式的强大能力来说,这还只是"冰山一角"。模式可以匹配一系列值,它可以解构元组、可以匹配结构体的各个字段、可以追踪引用、可以借用部分值,等等。甚至可以说,Rust 的模式定义了自己的迷你语言。第 10 章会用一些篇幅来介绍模式。
match
表达式的一般形式如下所示:
ini
match value {
pattern => expr,
...
}
如果 expr
是一个块,则可以省略此分支之后的逗号。
Rust 会从第一项开始依次根据每个模式检查给定的 value
。当模式能够匹配时,对应的 expr
会被求值,而当这个 match
表达式结束时,不会再检查别的模式。至少要有一个模式能够匹配。Rust 禁止执行未覆盖所有可能值的 match
表达式:
ini
let score = match card.rank {
Jack => 10,
Queen => 10,
Ace => 11
}; // 错误:未穷举所有模式
if
表达式的所有块都必须生成相同类型的值:
javascript
let suggested_pet =
if with_wings { Pet::Buzzard } else { Pet::Hyena }; // 正确
let favorite_number =
if user.is_hobbit() { "eleventy-one" } else { 9 }; // 错误
let best_sports_team =
if is_hockey_season() { "Predators" }; // 错误
(最后一个示例之所以是错的,是因为在 7 月份结果将是 ()
。)1
1因为 7 月份不是冰球赛季 hockey_season
,所以会"走"入隐藏的 else
分支,返回 ()
。------译者注
类似地,match
表达式的所有分支都必须具有相同的类型。
ini
let suggested_pet =
match favorites.element {
Fire => Pet::RedPanda,
Air => Pet::Buffalo,
Water => Pet::Orca,
_ => None // 错误:不兼容的类型
};
6.5.1 if let
还有一种 if
形式,即 if let
表达式:
bash
if let pattern = expr {
block1
} else {
block2
}
给定的 expr
要么匹配 pattern
,这时会运行 block1
;要么无法匹配,这时会运行 block2
。有时这是从 Option
或 Result
中获取数据的好办法:
scss
if let Some(cookie) = request.session_cookie {
return restore_session(cookie);
}
if let Err(err) = show_cheesy_anti_robot_task() {
log_robot_attempt(err);
politely_accuse_user_of_being_a_robot();
} else {
session.mark_as_human();
}
if let
不是必需 的,因为凡是 if let
可以做到的,match
同样可以做到。if let
表达式其实是只有一个模式的 match
表达式的简写形式。
ini
match expr {
pattern => { block1 }
_ => { block2 }
}
笔记
由于日常只写JavaScript代码,这部分代码在练习时总感觉怪怪的,写着写着就出错了
6.5.2 循环
有 4 种循环表达式:
rust
while condition {
block
}
while let pattern = expr {
block
}
loop {
block
}
for pattern in iterable {
block
}
各种循环都是 Rust 中的表达式,但 while
循环或 for
循环的值总是 ()
,因此它们的值通常没什么用。如果指定了一个值,那么 loop
表达式就能生成一个值。
while
循环的行为与 C 中的等效循环完全一样,只不过其 condition
必须是 bool
类型。
while let
循环类似于 if let
。在每次循环迭代开始时,expr
的值要么匹配给定的 pattern
,这时会运行循环体(block
);要么不匹配,这时会退出循环。
可以用 loop
来编写无限循环。它会永远重复执行循环体(直到遇上 break
或 return
,或者直到线程崩溃)。
for
循环会对可迭代(iterable
)表达式求值,然后为结果迭代器中的每个值运行一次循环体。许多类型可以迭代,包括所有标准库集合,比如 Vec
和 HashMap
。标准的 C 语言的 for
循环如下所示:
css
for (int i = 0; i < 20; i++) {
printf("%d\n", i);
}
在 Rust 中则是这样的:
rust
for i in 0..20 {
println!("{}", i);
}
与 C 一样,最后打印出的数值是 19
。
..
运算符会生成一个范围 (range),即具有两个字段(start
和 end
)的简单结构体。0..20
与 std::ops::Range { start: 0, end: 20 }
相同。各种范围都可以与 for
循环一起使用,因为 Range
是一种可迭代类型,它实现了 std::iter::IntoIterator
特型(参见第 15 章)。标准集合都是可迭代的,数组和切片也是如此。
为了与 Rust 的移动语义保持一致,把值用于 for
循环会消耗该值:
rust
let strings: Vec<String> = error_messages();
for s in strings { // 在这里,每个String都会转移给s......
println!("{}", s);
} // ......并在此丢弃
println!("{} error(s)", strings.len()); // 错误:使用了已移动出去的值
这可能很不方便。简单的补救措施是在循环中访问此集合的引用。然后,循环变量也会变成对集合中每个条目的引用:
css
for rs in &strings {
println!("String {:?} is at address {:p}.", *rs, rs);
}
这里 &strings
的类型是 &Vec<String>
,rs
的类型是 &String
。
遍历(可迭代对象的)可变引用会为每个元素提供一个可变引用:
rust
for rs in &mut strings { // rs的类型是&mut String
rs.push('\n'); // 为每个字符串添加一个换行
}
欢迎大家讨论交流 Rust,如果喜欢本文章或感觉文章有用,动动你那发财的小手点个赞再走呗
^_^
微信公众号:草帽Lufei