第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

相关推荐
minDuck2 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
小政爱学习!22 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。27 分钟前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼34 分钟前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k093337 分钟前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang13581 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning1 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人1 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
假装我不帅2 小时前
asp.net framework从webform开始创建mvc项目
后端·asp.net·mvc