《Rust 程序设计语言》学习笔记:1. 入门、2. 猜猜看、3. 编程概念

199 阅读19分钟

Rust 程序设计语言

介绍

在 Rust 中,编译器充当了守门员的角色,它拒绝编译存在这些难以捕获的 bug 的代码,这其中包括并发 bug。

Rust 也为系统编程世界带来了现代化的开发工具:

  • Cargo,内置的依赖管理器和构建工具,它能轻松增加、编译和管理依赖,并使其在 Rust 生态系统中保持一致。
  • Rustfmt 确保开发者遵循一致的代码风格。
  • Rust Language Server 为集成开发环境(IDE)提供了强大的代码补全和内联错误信息功能。

Rust 的编译器检查确保了增加功能和重构代码时的稳定性。

力求零开销抽象(zero-cost abstractions),把高级的特性编译成底层的代码,这样写起来很快,运行起来也很快,Rust 致力于使安全的代码也同样快速。

1. 入门指南

安装

第一步是安装 Rust。我们会通过 rustup 下载 Rust,这是一个管理 Rust 版本和相关工具的命令行工具。

更新和卸载

$ rustup update
$ rustup self uninstall

查看版本

$ rustc --version

Hello, World!

main.rs

fn main() {
    println!("Hello, world!");
}
$ rustc main.rs
$ ./main # Windows 是 .\main.exe
Hello, world!

Rust 的缩进风格使用 4 个空格,而不是 1 个制表符(tab)。

println! 调用了一个 Rust 宏(macro)。如果是调用函数,则应输入 println(没有!)。

当看到符号 ! 的时候,就意味着调用的是宏而不是普通函数,并且宏并不总是遵循与函数相同的规则。

编译和运行是彼此独立的步骤,编译成功后,Rust 会输出一个二进制的可执行文件。

如果你更熟悉动态语言,如 Ruby、Python 或 JavaScript,则可能不习惯将编译和运行分为两个单独的步骤。Rust 是一种 预编译静态类型ahead-of-time compiled)语言,这意味着你可以编译程序,并将可执行文件送给其他人,他们甚至不需要安装 Rust 就可以运行。如果你给他人一个  .rb.py 或  .js 文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM)。不过在这些语言中,只需要一句命令就可以编译和运行程序。这一切都是语言设计上的权衡取舍。

Hello, Cargo!

Cargo 是 Rust 的构建系统和包管理器。

所以如果使用 Cargo 来构建 “Hello, world!” 项目,将只会用到 Cargo 构建代码的那部分功能。在编写更复杂的 Rust 程序时,你将添加依赖项,如果使用 Cargo 启动项目,则添加依赖项将更容易。

$ cargo --version

使用 Cargo 创建项目

$ cargo new hello_cargo
$ cd hello_cargo
hello_cargo
├── Cargo.toml
└── src
    └── main.rs

Cargo.toml

[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"

[dependencies]

在 Rust 中,代码包被称为 crates

构建并运行 Cargo 项目

我们刚刚使用 cargo build 构建了项目,并使用 ./target/debug/hello_cargo 运行了程序,也可以使用 cargo run 在一个命令中同时编译并运行生成的可执行文件:

$ cargo run
   Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs
     Running `target/debug/hello_cargo`
Hello, world!

cargo check 的命令:快速检查代码确保其可以编译,但并不产生可执行文件。

我们回顾下已学习的 Cargo 内容:

  • 可以使用 cargo build 构建项目。
  • 可以使用 cargo run 一步构建并运行项目。
  • 可以使用 cargo check 在不生成二进制文件的情况下构建项目来检查错误。

发布(release)构建

当项目最终准备好发布时,可以使用 cargo build --release 来优化编译项目。这会在 target/release 而不是 target/debug 下生成可执行文件。这些优化可以让 Rust 代码运行的更快,不过启用这些优化也需要消耗更长的编译时间。

2. 编写 猜猜看 游戏

$ cargo new guessing_game
     Created binary (application) `guessing_game` package

请求和处理用户输入

src/main.rs

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {}", guess);
}

将 io输入/输出库引入当前作用域。io 库来自于标准库,也被称为 std

use std::io;

默认情况下,Rust 将 prelude 模块中少量的类型引入到每个程序的作用域中。

如果你需要的类型不在 prelude 中,你必须使用 use 语句显式地将其引入作用域。

使用变量储存值

let mut guess = String::new();

我们使用 let 语句来创建变量。

let apples = 5;

在 Rust 中,变量默认是不可变的。

下面的例子展示了如何在变量名前使用 mut 来使一个变量可变:

let apples = 5; // 不可变
let mut bananas = 5; // 可变

String::new 这个函数会返回一个 String 的新实例。String 是一个标准库提供的字符串类型,它是 UTF-8 编码的可增长文本块。

::new 那一行的 :: 语法表明 new 是 String 类型的一个 关联函数associated function)。关联函数是针对类型实现的,在这个例子中是 String,而不是 String 的某个特定实例。一些语言中把它称为 静态方法static method)。

new 函数创建了一个新的空字符串,你会发现很多类型上有 new 函数,因为它是创建类型实例的惯用函数名。

总的来说,let mut guess = String::new(); 这一行创建了一个可变变量,当前它绑定到一个新的 String 空实例上。

接收用户输入

io::stdin()
    .read_line(&mut guess)

如果程序的开头没有使用 use std::io 引入 io 库,我们仍可以通过把函数调用写成 std::io::stdin 来使用函数。stdin 函数返回一个 std::io::Stdin 的实例,这代表终端标准输入句柄的类型。

调用 read_line 方法从标准输入句柄获取用户输入。我们还将 &mut guess 作为参数传递给 read_line() 函数,让其将用户输入储存到这个字符串中。read_line 的工作是,无论用户在标准输入中键入什么内容,都将其追加(不会覆盖其原有内容)到一个字符串中,因此它需要字符串作为参数。这个字符串参数应该是可变的,以便 read_line 将用户输入附加上去。

& 表示这个参数是一个 引用reference),它允许多处代码访问同一处数据,而无需在内存中多次拷贝。引用是一个复杂的特性,Rust 的一个主要优势就是安全而简单的操纵引用。现在,我们只需知道它像变量一样,默认是不可变的。因此,需要写成 &mut guess 来使其可变,而不是 &guess

使用 Result 类型来处理潜在的错误

.expect("Failed to read line");

我们也可以将代码这样写:

io::stdin().read_line(&mut guess).expect("Failed to read line");

read_line 返回一个 io::Result。Rust 标准库中有很多叫做 Result 的类型:一个泛型 Result 以及在子模块中的特化版本,比如 io::ResultResult 类型是 枚举enumerations,通常也写作 enums

这些 Result 类型的作用是编码错误处理信息(encode error-handling information)。

Result 的成员是 Ok 和 ErrOk 成员表示操作成功,内部包含成功时产生的值。Err 成员则意味着操作失败,并且包含失败的前因后果。

io::Result 的实例拥有 expect 方法。如果 io::Result 实例的值是 Errexpect 会导致程序崩溃,并显示当做参数传递给 expect 的信息。如果 read_line 方法返回 Err,则可能是来源于底层操作系统错误的结果。如果 io::Result 实例的值是 Okexpect 会获取 Ok 中的值并原样返回。在本例中,这个值是用户输入到标准输入中的字节数。

如果不调用 expect,程序也能编译,不过会出现一个警告:

Rust 警告我们没有使用 read_line 的返回值 Result,说明有一个可能的错误没有处理。

消除警告的正确做法是实际去编写错误处理代码,不过由于我们就是希望程序在出现问题时立即崩溃,所以直接使用 expect

生成一个秘密数字

使用 crate 来增加更多功能

记住,crate 是一个 Rust 代码包。我们正在构建的项目是一个 二进制 crate,它生成一个可执行文件。

rand crate 是一个 库 crate,库 crate 可以包含任意能被其他程序使用的代码,但是不能自执行。

[dependencies]
rand = "0.8.3"

0.8.3 事实上是 ^0.8.3 的简写,它表示任何至少是 0.8.3 但小于 0.9.0 的版本。

然后,构建项目

$ cargo build

现在我们有了一个外部依赖,Cargo 从 registry 上获取所有包的最新版本信息,这是一份来自 Crates.io 的数据拷贝。

配置国内镜像:C:\Users\myname\.cargo\config

[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = 'ustc'

[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"

在更新完 registry 后,Cargo 检查 [dependencies] 片段并下载列表中包含但还未下载的 crates 。

下载完成后,Rust 编译依赖,然后使用这些依赖编译项目。

Cargo.lock 文件确保构建是可重现的

当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并写入 Cargo.lock 文件。当将来构建项目时,Cargo 会发现 Cargo.lock 已存在并使用其中指定的版本,而不是再次计算所有的版本。这使得你拥有了一个自动化的可重现的构建。换句话说,项目会持续使用 0.8.3 直到你显式升级,多亏有了 Cargo.lock 文件。

更新 crate 到一个新版本

$ cargo update

cargo update 会忽略 Cargo.lock 文件,并计算出所有符合 Cargo.toml 声明的最新版本。Cargo 接下来会把这些版本写入 Cargo.lock 文件。不过,Cargo 默认只会寻找大于 0.8.3 而小于 0.9.0 的版本。

生成一个随机数

src/main.rs

use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..101);

    println!("The secret number is: {}", secret_number);

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {}", guess);
}

首先,我们新增了一行 use rand::RngRng 是一个 trait,它定义了随机数生成器应实现的方法,想使用这些方法的话,此 trait 必须在作用域中。

rand::thread_rng 函数提供实际使用的随机数生成器:它位于当前执行线程的本地环境中,并从操作系统获取 seed。接着调用随机数生成器的 gen_range 方法。这个方法由 use rand::Rng 语句引入到作用域的 Rng trait 定义。gen_range 方法获取一个范围表达式(range expression)作为参数,并生成一个在此范围之间的随机数。这里使用的这类范围表达式的在 start..end 之间取值且包含下限但不包含上限,所以需要指定 1 和 101 来请求一个 1 和 100 之间的数。另外也可以使用范围 1..=100,这两者是等价的。

运行 cargo doc --open 命令来构建所有本地依赖提供的文档,并在浏览器中打开。点击左侧导航栏中的 rand 查看使用说明文档。

比较猜测的数字和秘密数字

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    // --snip--

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

一个 match 表达式由 分支(arms)  构成。一个分支包含一个 模式pattern)和表达式开头的值与分支模式相匹配时应该执行的代码。

上述代码并不能编译。

错误的核心表明这里有 不匹配的类型mismatched types)。Rust 有一个静态强类型系统,同时也有类型推断。guess 是 String 类型。secret_number 是数字类型,Rust 默认使用 i32,除非增加类型信息,或任何能让 Rust 推断出不同数值类型的信息。Rust 不会比较字符串类型和数字类型。

    // --snip--

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }

Rust 允许用一个新值来 隐藏 (shadow) guess 之前的值。这个功能常用在需要转换值类型之类的场景。它允许我们复用 guess 变量的名字,而不是被迫创建两个不同变量。

trim 方法消除 read_line 输入 enter 键带来的换行符。

字符串的 parse 方法 将字符串解析成数字。因为这个方法可以解析多种数字类型,因此需要告诉 Rust 具体的数字类型,这里通过 let guess: u32 指定。

程序中的 u32 注解以及与 secret_number 的比较,意味着 Rust 会推断出 secret_number 也是 u32 类型。

parse 方法返回一个 Result 类型。像之前 使用 Result 类型来处理潜在的错误 read_line 方法那样,再次按部就班的用 expect 方法处理即可。如果 parse 不能从字符串生成一个数字,返回一个 Result 的 Err 成员时,expect 会使游戏崩溃并打印附带的信息。如果 parse 成功地将字符串转换为一个数字,它会返回 Result 的 Ok 成员,然后 expect 会返回 Ok 值中的数字。

Guess the number!
The secret number is: 90
Please input your guess.
abcd
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src\main.rs:20:43
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\guessing_game.exe` (exit code: 101)

使用循环来允许多次猜测

    // --snip--

    println!("The secret number is: {}", secret_number);

    loop {
        println!("Please input your guess.");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }
}

猜测正确后退出

Ordering::Equal => {
    println!("You win!");
    break;
}

处理无效输入

        // --snip--

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {}", guess);

        // --snip--

我们将 expect 调用换成 match 语句,以从遇到错误就崩溃转换为处理错误。

_ 是一个通配符值,本例中用来匹配所有 Err 值,不管其中有何种信息。

Err(_) => {
    println!("Invalid number!");
    continue;
}

完整代码

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..101);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

本项目通过动手实践,向你介绍了 Rust 新概念:letmatch、方法(method)、关联函数(associated function)、使用外部 crate 等。

3. 常见编程概念

变量和可变性

变量默认是不可改变的(immutable)。在变量名之前加 mut 来使其可变。

常量

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

声明常量使用 const 关键字,并且 必须 注明值的类型。

隐藏(Shadowing)

定义一个与之前变量同名的新变量。可以用相同变量名称来隐藏一个变量,以及重复使用 let 关键字来多次隐藏。

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {}", x);
    }

    println!("The value of x is: {}", x);
}
The value of x in the inner scope is: 12
The value of x is: 6
fn main() {
    let mut x = 5;

    x = x + 1;

    {
        x = x * 2;
        println!("The value of x in the inner scope is: {}", x);
    }

    println!("The value of x is: {}", x);
}
The value of x in the inner scope is: 12
The value of x is: 12

隐藏与将变量标记为 mut 是有区别的。当不小心尝试对变量重新赋值时,如果没有使用 let 关键字,就会导致编译时错误。通过使用 let,我们可以用这个值进行一些计算,不过计算完之后变量仍然是不可变的。

mut 与隐藏的另一个区别是,当再次使用 let 时,实际上创建了一个新变量,我们可以改变值的类型,并且复用这个名字。

let spaces = " ";
let spaces = spaces.len();

第一个 spaces 变量是字符串类型,第二个 spaces 变量是数字类型。

然而,如果尝试使用 mut,将会得到一个编译时错误。

let mut spaces = " ";
spaces = spaces.len(); // expected `&str`, found `usize`

数据类型

我们将看到两类数据类型子集:标量(scalar)和复合(compound)。

记住,Rust 是 静态类型statically typed)语言,也就是说在编译时就必须知道所有变量的类型。根据值及其使用方式,编译器通常可以推断出我们想要用的类型。当多种类型均有可能时,必须增加类型注解

let guess: u32 = "42".parse().expect("Not a number!");

标量类型

标量scalar)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。

整型

长度有符号无符号
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

有符号 和 无符号 代表数字能否为负值

每一个有符号的变体可以储存包含从 -(2n - 1) 到 2n - 1 - 1 在内的数字,这里 n 是变体使用的位数。所以 i8 可以储存从 -(27) 到 27 - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 2n - 1 的数字,所以 u8 可以储存从 0 到 28 - 1 的数字,也就是从 0 到 255。

另外,isize 和 usize 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。

可以使用表格 3-2 中的任何一种形式编写数字字面值。请注意可以是多种数字类型的数字字面值允许使用类型后缀,例如 57u8 来指定类型,同时也允许使用 _ 做为分隔符以方便读数,例如1_000,它的值与你指定的 1000 相同。

表格 3-2: Rust 中的整型字面值

数字字面值例子
Decimal (十进制)98_222
Hex (十六进制)0xff
Octal (八进制)0o77
Binary (二进制)0b1111_0000
Byte (单字节字符)(仅限于u8)b'A'

数字类型默认是 i32

整型溢出

当在 debug 模式编译时,Rust 检查这类问题并使程序 panic,这个术语被 Rust 用来表明程序因错误而退出。

在 release 构建中,Rust 不检测溢出,相反会进行一种被称为二进制补码包装(two’s complement wrapping)的操作。

浮点型

Rust 的浮点数类型是 f32 和 f64,分别占 32 位和 64 位。默认类型是 f64。所有的浮点型都是有符号的。

浮点数采用 IEEE-754 标准表示。f32 是单精度浮点数,f64 是双精度浮点数。

数值运算

Rust 中的所有数字类型都支持基本数学运算:加法、减法、乘法、除法和取余。

布尔型

Rust 中的布尔类型使用 bool 表示。

字符类型

Rust的 char 类型是语言中最原生的字母类型。

fn main() {
    let c = 'z';
    let z = 'ℤ';
    let heart_eyed_cat = '😻';
}

注意,我们用单引号声明 char 字面量,而与之相反的是,使用双引号声明字符串字面量。Rust 的 char 类型的大小为四个字节(four bytes),并代表了一个 Unicode 标量值(Unicode Scalar Value),这意味着它可以比 ASCII 表示更多内容。在 Rust 中,拼音字母(Accented letters),中文、日文、韩文等字符,emoji(绘文字)以及零长度的空白字符都是有效的 char 值。Unicode 标量值包含从 U+0000 到 U+D7FF 和 U+E000 到 U+10FFFF 在内的值。

复合类型

复合类型Compound types)可以将多个值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。

元组类型

元组是一个将多个其他类型的值组合进一个复合类型的主要方式。元组长度固定:一旦声明,其长度不会增大或缩小。

let tup: (i32, f64, u8) = (500, 6.4, 1);

为了从元组中获取单个值,可以使用模式匹配(pattern matching)来解构(destructure)元组值,像这样:

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {}", y); // 6.4
}

我们也可以使用点号(.)后跟值的索引来直接访问它们。

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

没有任何值的元组 () 是一种特殊的类型,只有一个值,也写成 () 。该类型被称为 单元类型unit type),而该值被称为 单元值unit value)。如果表达式不返回任何其他值,则会隐式返回单元值。

数组类型

与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,Rust中的数组长度是固定的

let a = [1, 2, 3, 4, 5];
let a = [3; 5]; // 数组将包含 `5` 个元素,这些元素的值最初都将被设置为 `3`。
let a: [i32; 5] = [1, 2, 3, 4, 5];
let first = a[0];

程序在索引操作中使用一个无效的值时导致 运行时 错误。程序带着错误信息退出,并且不会执行后面的语句。如果索引超出了数组长度,Rust 会 panic,这是 Rust 术语,它用于程序因为错误而退出的情况。通过立即退出而不是允许内存访问并继续执行,Rust 让你避开此类错误。

数组并不如 vector 类型灵活。vector 类型是标准库提供的一个 允许 增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,那么很可能应该使用 vector。当你确定元素个数不会改变时,数组会更有用。

函数

Rust 代码中的函数和变量名使用 snake case 规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词。

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

another_function 定义在 main 函数 之后;也可以定义在之前。Rust 不关心函数定义于何处,只要定义了就行。

在函数签名中,必须 声明每个参数的类型。

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {}{}", value, unit_label);
}

语句和表达式

函数体由一系列的语句和一个可选的结尾表达式构成。

Rust 是一门基于表达式(expression-based)的语言,这是一个需要理解的(不同于其他语言)重要区别。

语句Statements)是执行一些操作但不返回值的指令。表达式Expressions)计算并产生一个值。

let y = 6; 是一个语句。函数定义也是语句。语句不返回值。

5 + 6 是一个表达式并计算出值 11。函数调用是一个表达式。宏调用是一个表达式。用大括号创建的一个新的块作用域也是一个表达式,例如:

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("The value of y is: {}", y);
}

这个表达式:

{
    let x = 3;
    x + 1
}

是一个代码块,它的值是 4。这个值作为 let 语句的一部分被绑定到 y 上。注意 x+1 这一行在结尾没有分号,与你见过的大部分代码行不同。表达式的结尾没有分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。在接下来探索具有返回值的函数和表达式时要谨记这一点。

具有返回值的函数

在箭头(->)后声明它的类型。函数的返回值等同于函数体最后一个表达式的值。使用 return 关键字和指定值,可从函数中提前返回;但大部分函数隐式的返回最后的表达式。

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {}", x);
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

注释

Rust 还有另一种注释,称为文档注释,我们将在 14 章的 “将 crate 发布到 Crates.io” 部分讨论它。

控制流

if 表达式

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

条件 必须 是 bool 值。

可以将 else if 表达式与 if 和 else 组合来实现多重条件。

在 let 语句中使用 if

因为 if 是一个表达式,我们可以在 let 语句的右侧使用它

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {}", number);
}

if 的每个分支的可能的返回值都必须是相同类型,如果它们的类型不匹配,则会出现一个错误。

循环

Rust 有三种循环:loopwhile 和 for

loop 关键字告诉 Rust 一遍又一遍地执行一段代码直到你明确要求停止。使用 break 关键字来告诉程序何时停止循环。continue 关键字告诉程序跳过这个循环迭代中的任何剩余代码,并转到下一个迭代。

你可以选择在一个循环上指定一个 循环标签loop label),然后将标签与 break 或 continue 一起使用,使这些关键字应用于已标记的循环而不是最内层的循环。

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {}", count);
        let mut remaining = 10;

        loop {
            println!("remaining = {}", remaining);
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {}", count);
}

从循环返回值

loop 的一个用例是重试可能会失败的操作,比如检查线程是否完成了任务。然而你可能会需要将操作的结果传递给其它的代码。如果将返回值加入你用来停止循环的 break 表达式,它会被停止的循环返回:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {}", result);
}

while 循环

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);

        number -= 1;
    }

    println!("LIFTOFF!!!");
}

可以使用 for 循环来对一个集合的每个元素执行一些代码。

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {}", element);
    }
}

for 循环的安全性和简洁性使得它成为 Rust 中使用最多的循环结构。

fn main() {
    for number in (1..4).rev() { // `rev` 用来反转 range
        println!("{}!", number);
    }
    println!("LIFTOFF!!!");
}
3!
2!
1!
LIFTOFF!!!