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::Result。Result 类型是 枚举(enumerations),通常也写作 enums。
这些 Result 类型的作用是编码错误处理信息(encode error-handling information)。
Result 的成员是 Ok 和 Err,Ok 成员表示操作成功,内部包含成功时产生的值。Err 成员则意味着操作失败,并且包含失败的前因后果。
io::Result 的实例拥有 expect 方法。如果 io::Result 实例的值是 Err,expect 会导致程序崩溃,并显示当做参数传递给 expect 的信息。如果 read_line 方法返回 Err,则可能是来源于底层操作系统错误的结果。如果 io::Result 实例的值是 Ok,expect 会获取 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::Rng。Rng 是一个 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 新概念:let、match、方法(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-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| arch | isize | usize |
有符号 和 无符号 代表数字能否为负值
每一个有符号的变体可以储存包含从 -(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 有三种循环:loop、while 和 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!!!