第6章 | 表达式 | 优先级,块与分号,生命,if match

哥哥艾斯和弟弟路飞

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 中,ifswitch 是语句,它们不生成值,也不能在表达式中间使用。而在 Rust 中,ifmatch 可以 生成值。第 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 DerefDerefMut
结构体字段访问 point.x DerefDerefMut
方法调用 point.translate(50, 50) DerefDerefMut
函数调用 stdin() Fn(Arg0, ...) -> TFnMut(Arg0, ...) -> TFnOnce(Arg0, ...) -> T
索引 arr[0] IndexIndexMutDerefDerefMut
错误检查 create_dir("tmp")?
逻辑非 / 按位非 !ok Not
取负 -num Neg
解引用 *ptr DerefDerefMut
借用 &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() 返回的任何值,但是没有 elseif 必定返回 ()。幸好,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)。语法项是指可以在程序或模块中的任意地方出现的声明,比如 fnstructuse

后面的章节会详细介绍这些语法项。现阶段,用 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 ifmatch

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 。有时这是从 OptionResult 中获取数据的好办法:

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 来编写无限循环。它会永远重复执行循环体(直到遇上 breakreturn,或者直到线程崩溃)。

for 循环会对可迭代(iterable)表达式求值,然后为结果迭代器中的每个值运行一次循环体。许多类型可以迭代,包括所有标准库集合,比如 VecHashMap。标准的 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),即具有两个字段(startend)的简单结构体。0..20std::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

相关推荐
PanZonghui10 分钟前
Centos项目部署之安装数据库MySQL8
linux·后端·mysql
Victor35614 分钟前
MySQL(119)如何加密存储敏感数据?
后端
zhanshuo14 分钟前
不依赖框架,如何用 JS 实现一个完整的前端路由系统
前端·javascript·html
火柴盒zhang15 分钟前
websheet在线电子表格(spreadsheet)在集团型企业财务报表中的应用
前端·html·报表·合并·spreadsheet·websheet·集团财务
khalil17 分钟前
基于 Vue3实现一款简历生成工具
前端·vue.js
拾光拾趣录24 分钟前
浏览器对队头阻塞问题的深度优化策略
前端·浏览器
用户39661446871924 分钟前
TypeScript 系统入门到项目实战-慕课网
后端
用户81221993672224 分钟前
[已完结]后端开发必备高阶技能--自研企业级网关组件(Netty+Nacos+Disruptor)
前端
万少29 分钟前
2025中了 聊一聊程序员为什么都要做自己的产品
前端·harmonyos