Rust Book 编译版 03 · 通用编程概念:变量、类型、函数与控制流
编译自 The Rust Programming Language(2024 edition),在原文基础上做了删减与补充。© The Rust Project Developers,依据 MIT OR Apache-2.0 许可编译转载。
开始之前
这一章大概是全系列对「有其他语言经验的人」最快的一章:变量、数据类型、函数、注释、控制流,哪门语言都有,闭着眼也能猜个八九不离十。但 Rust 在每一个熟悉的概念上都有所改变:变量默认不可变、同名变量可以遮蔽(还能换类型)、if 条件必须是 bool、loop 能带返回值。原书章首说得直白:本章没有一个概念是 Rust 独有的,但每一个都要放到 Rust 的语境里重新校准使用惯例。顺带一提,Rust 保留了一组关键字(keyword) 专供语言本身使用,不能拿来做变量名或函数名,完整清单见附录 A。
建议拿你最熟的语言当对照系来读——C 系背景尤其合适。这也是第 4 章所有权之前最后的铺垫:本章这些看似拧巴的设计,到所有权一章都会给出答案。
前置知识:第 1 章入门(第 2 章可选;猜数字游戏已经预演过本章不少概念)。
你会学到:
- 分清
let不可变绑定、mut、const常量与遮蔽四种变量机制各自的边界 - 认全 Rust 的标量与复合类型,以及整数溢出在 debug / release 下的两种行为
- 用「语句 和 表达式」理解函数返回值——为什么一个分号能改变返回类型
- 掌握
if表达式与loop/while/for三种循环,包括循环标签和从loop带值返回
变量与可变性
第 2 章已经剧透过:Rust 的变量默认是不可变的(immutable)。这是 Rust 引导你写出更安全、更轻松并发代码的众多设计之一——不可变是默认,可变要显式申请。先看不可变时会发生什么。
在 projects 目录里用 cargo new variables 建个新项目,把 src/main.rs 换成下面这段(暂时编译不过):
文件名:src/main.rs
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
保存后 cargo run,你会收到一条关于不可变性的错误:
cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| - first assignment to `x`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error
编译报错不等于你是糟糕的程序员——经验丰富的 Rustacean 照样天天收到编译错误,它只说明程序还不能安全地做到你想让它做的事。这里的错误原因很简单:x 不可变,你却给它赋了第二个值。
当然可变性也很有用。像第 2 章那样在变量名前加 mut:
文件名:src/main.rs
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
运行:
cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
用不用 mut 由你决定,标准只有一个:哪种写法意图更清晰。mut 同时也是给读代码的人的信号——这个值后面会被程序的其他部分改变。
编译注: C/C++ 把可变当默认、
const当额外修饰;Rust 却反过来:不可变默认,可变要申请。默认值的方向决定了落笔心态——每个变量都得先想一句「要不要变」。颗粒度也不同:C++ 同一作用域根本不允许重声明同名变量,而 Rust 的不可变绑定可以被下一节的「遮蔽」合法替换成新值。
声明常量
常量(constant) 与不可变变量形似,实则有三点不同:
- 不允许
mut——常量不是默认不可变,是永远不可变; - 声明用
const而非let,且值的类型必须显式标注(类型下一节讲,先记住「必须标」); - 只能绑定常量表达式,不能是运行时才能算出的值。
常量可以声明在任何作用域(scope)里,包括全局作用域;它在程序运行的整个期间都有效(限其作用域内)。一个例子:
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
命名约定是全大写、下划线分隔。60 * 60 * 3 由编译器在编译期求值,所以不必直接写 10,800——让读者一眼看懂「三小时折成多少秒」远比省几个字符重要。可用运算的范围见 Rust Reference 的常量求值一节。
适合做常量的,是程序多个部分都需要知道的应用领域值,比如游戏允许玩家赚取的最高分,或者光速。把通篇使用的硬编码值提成常量,既向未来维护者传达含义,也把将来要改的地方收敛到一处。
遮蔽
第 2 章出现过:可以再用 let 声明一个同名变量。如此第一个变量被第二个遮蔽(shadowing) 了——用这个名字时编译器看到的是第二个变量,对名字的所有使用都落到它身上,直到它自己也被遮蔽或作用域结束:
文件名:src/main.rs
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}");
}
程序先把 5 绑定到 x;let x = x + 1 创建新变量,x 变成 6;花括号里的第三条 let 再遮蔽一次,x 变成 12;内层作用域结束,遮蔽随之结束,x 回到 6:
cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
遮蔽与 mut 的第一处不同:不写 let 直接赋值仍会得到编译期错误;用 let 遮蔽则可以对一个值做多次变换,且每一步之后变量都不可变。第二处不同更实用:let 遮蔽创建的是新变量,因此可以换类型、复用同一个名字。比如把「用户输入的空格串」转成「空格数」:
let spaces = " ";
let spaces = spaces.len();
第一个 spaces 是字符串类型,第二个是数字类型——不必再发明 spaces_str、spaces_num 这样的名字。但如果用 mut:
let mut spaces = " ";
spaces = spaces.len();
就过不了编译,错误信息直说类型不能变:
cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to 1 previous error
编译注: C++ 里同一作用域重声明同名变量是 redefinition 错误,只有内层作用域的声明才叫「隐藏」;Rust 的遮蔽发生在同一作用域内,是货真价实的新绑定,还允许换类型。实战里最趁手的场景是处理输入——第 2 章的
let guess: u32 = guess.trim().parse()已经用过这招,一路同名变换,免去被迫起x_str、x_num的尴尬。
数据类型
Rust 的每个值都有确切的数据类型(data type)。Rust 是静态类型(statically typed) 语言,编译期必须知道所有变量的类型;编译器通常能推断出来,推断不出时(比如第 2 章用 parse 把 String 转数值,目标类型有多种可能)就要你补类型标注:
let guess: u32 = "42".parse().expect("Not a number!");
不标的话:
cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ ----- type must be known at this point
|
= note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
|
2 | let guess: /* Type */ = "42".parse().expect("Not a number!");
| ++++++++++++
For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error
本章考察两类:标量类型(scalar type)与复合类型(compound type)。先给一张全景图,后面逐个展开:
Rust 基本数据类型一览
├── 标量(一个值)
│ ├── 整数 i8/i16/i32/i64/i128、u8/u16/u32/u64/u128、isize/usize(默认 i32)
│ ├── 浮点 f32 / f64(默认 f64,IEEE-754,均有符号)
│ ├── 布尔 bool(true / false,占 1 字节)
│ └── 字符 char(4 字节,Unicode 标量值)
└── 复合(多个值)
├── 元组 (T1, T2, ...):定长,元素类型可不同;空元组叫单元 ()
└── 数组 [T; N]:定长,元素同类型,整块放栈上
标量类型
标量表示单个值。Rust 有四种:整数、浮点数、布尔值和字符。
整型
整数(integer) 没有小数部分,i 前缀有符号、u 前缀无符号。表 3-1 列出全部内置整数类型:
表 3-1:Rust 中的整数类型
| 长度 | 有符号 | 无符号 |
|---|---|---|
| 8 位 | i8 | u8 |
| 16 位 | i16 | u16 |
| 32 位 | i32 | u32 |
| 64 位 | i64 | u64 |
| 128 位 | i128 | u128 |
| 视架构而定 | isize | usize |
有符号(signed) 数以二进制补码形式存储,能存 −(2^(n − 1)) 到 2^(n − 1) − 1(含两端);无符号(unsigned) 存 0 到 2^n − 1。于是 i8 是 −128 到 127,u8 是 0 到 255。isize/usize 视架构而定:64 位机器上是 64 位,32 位机器上是 32 位。
字面量写法见表 3-2;类型后缀(如 57u8)可指明类型,_ 可做视觉分隔符让数字更易读(1_000 与 1000 等值)。
表 3-2:Rust 中的整数字面量
| 数字字面量 | 示例 |
|---|---|
| 十进制 | 98_222 |
| 十六进制 | 0xff |
| 八进制 | 0o77 |
| 二进制 | 0b1111_0000 |
字节(仅限 u8) | b'A' |
拿不准用哪个?Rust 的默认选择 i32 通常是好起点;isize/usize 的主战场是对集合做索引。
整数溢出
u8 能存 0–255,写入 256 就发生整数溢出(integer overflow),两种行为:debug 模式编译时 Rust 加入溢出检查,一旦溢出程序在运行时 panic(程序因错误退出,第 9 章详谈);用 --release 编译时不加这套检查,溢出按二进制补码环绕(two's complement wrapping) 处理——256 绕回 0,257 绕回 1。程序不会 panic,但拿到的值多半不是你想要的;依赖环绕行为被视为一种错误。
要显式处理溢出,标准库为基本数值类型提供了四族方法:
wrapping_*:所有编译模式下都环绕,例如wrapping_addchecked_*:溢出时返回Noneoverflowing_*:返回值和一个指示是否溢出的布尔值saturating_*:让值在最小值或最大值处饱和
这部分读者感兴趣可以去了解,此处不多做介绍。
编译注: 这一节值得多停一分钟:C++ 的有符号整数溢出是未定义行为(UB),无符号溢出虽定义为环绕,但两条规则的界线全靠你自己记——记错一次,代价往往是一笔查不出原因的错误结果。Rust 把这件事显式化了:debug 下溢出当场 panic 崩给你看,release 下是定义好的补码环绕。
浮点类型
Rust 的浮点数(floating-point number) 类型是 f32 和 f64,默认 f64——现代 CPU 上它与 f32 速度相当、精度更高。所有浮点类型都有符号,按 IEEE-754 标准表示:
文件名:src/main.rs
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
数值运算
加、减、乘、除、取余俱全;整数除法向零截断到最接近的整数:
文件名:src/main.rs
fn main() {
// 加法
let sum = 5 + 10;
// 减法
let difference = 95.5 - 4.3;
// 乘法
let product = 4 * 30;
// 除法
let quotient = 56.7 / 32.2;
let truncated = -5 / 3; // 结果为 -1
// 取余
let remainder = 43 % 5;
}
附录 B 列出了 Rust 提供的全部运算符。
布尔类型
bool 只有两个值 true 和 false,占 1 字节。主要用途是条件判断,比如 if 表达式:
文件名:src/main.rs
fn main() {
let t = true;
let f: bool = false; // 显式类型标注
}
字符类型
char 是 Rust 最基本的字母类型,用单引号声明(字符串字面量(string literal)用双引号),大小 4 字节,表示一个 Unicode 标量值——远不止 ASCII,中文、韩文、emoji、零宽空格都合法:
文件名:src/main.rs
fn main() {
let c = 'z';
let z: char = 'ℤ'; // 显式类型标注
let heart_eyed_cat = '😻';
}
Unicode 标量值的范围是 U+0000 到 U+D7FF 以及 U+E000 到 U+10FFFF(含两端)。不过「字符」并不是 Unicode 中的概念,你对「字符」的直觉未必与 char 的含义吻合,第 8 章谈字符串时细说。
复合类型
复合类型把多个值组合成一个类型。Rust 有两种原生复合类型:元组(tuple)和数组(array)。
元组类型
元组把多个不同类型的值打包成一个复合类型,长度固定:一旦声明,大小不能增减。圆括号内写逗号分隔的值列表:
文件名:src/main.rs
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
取出值有两条路。其一,模式匹配解构,但是什么是模式你先别管,先记住下面的写法:
文件名:src/main.rs
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}
用带模式的 let 把 tup 拆成三个变量,这叫解构(destructuring)。其二,句点后跟索引直接访问:
文件名:src/main.rs
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;
}
索引从 0 开始。不含任何值的元组有个特殊名字:单元(unit),值和类型都写作 (),表示空值或空的返回类型;表达式若不返回其他值,就隐式返回单元——马上会在函数返回值处再遇到它。
数组类型
数组的每个元素必须同类型,且长度固定,整块分配在栈上(栈与堆的区别第 4 章讲):
文件名:src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
}
数组不如 vector 灵活——vector 是标准库提供的集合类型,内容放在堆上、可增可缩;拿不准就用 vector(第 8 章细讲)。但确知元素数量不变时,数组更合适,比如月份永远只有 12 个:
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
数组的类型写作 [元素类型; 长度]:
let a: [i32; 5] = [1, 2, 3, 4, 5];
也可以「初始值; 长度」把每个元素初始化为同一个值:
let a = [3; 5];
这等价于 let a = [3, 3, 3, 3, 3];,但更简洁。
访问数组元素
数组是一整块大小已知且固定的内存,用索引访问:
文件名:src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
first 得到 1(索引 [0]),second 得到 2(索引 [1])。
无效的数组元素访问
越界访问会怎样?写一段和第 2 章类似的程序,从用户读一个数组索引:
文件名:src/main.rs
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println!("Please enter an array index.");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");
let element = a[index];
println!("The value of the element at index {index} is: {element}");
}
代码编译通过。输入 0 到 4 会打印对应值;输入超出末尾的 10:
thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
程序在使用无效索引的那一刻带着错误信息退出,最后那条 println! 没有执行。边界检查只能发生在运行时——编译器无法预知用户日后会输入什么。这是 Rust 内存安全原则:许多底层语言里,这个错误会变成一次对无效内存的访问;Rust 选择立即退出来保护你。第 9 章会讲怎么写出既可读又安全、既不 panic 也不允许无效内存访问的代码。
函数
函数在 Rust 代码里无处不在:main 是许多程序的入口,fn 关键字声明新函数。命名惯用蛇形命名(snake case):全小写、下划线分隔。
文件名:src/main.rs
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
定义函数就是 fn + 函数名 + 圆括号 + 花括号体;调用则写函数名加圆括号。注意 another_function 定义在 main 之后——Rust 不关心函数定义位置,只要求定义在调用者可见的作用域里。放进新的 functions 项目运行:
cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/functions`
Hello, world!
Another function.
参数
参数(parameter)是函数签名中的特殊变量;调用时传入的具体值严格说叫实参(argument),日常交流中两个词通常混用。给 another_function 加一个参数:
文件名:src/main.rs
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {x}");
}
运行:
cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
The value of x is: 5
函数签名里每个参数的类型必须标注。
文件名:src/main.rs
fn main() {
print_labeled_measurement(5, 'h');
}
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}
运行:
cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h
语句与表达式
这一节是本章所有概念里最关键的。Rust 是基于表达式(expression-based)的语言,而很多语言不做这个区分:
- 语句(statement) 执行某种操作,但不返回值,或者说返回单元类型
(); - 表达式(expression) 求值并得到一个结果值。
let 创建变量就是语句。示例 3-1 中 let y = 6; 是一条语句:
fn main() {
let y = 6;
}
示例 3-1:包含一条语句的 main 函数声明
函数定义也是语句。语句没有值,所以不能把 let 语句赋给另一个变量:
文件名:src/main.rs
fn main() {
let x = (let y = 6);
}
运行会得到:
cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressions
warning: unnecessary parentheses around assigned value
--> src/main.rs:2:13
|
2 | let x = (let y = 6);
| ^ ^
|
= note: `#[warn(unused_parens)]` (part of `#[warn(unused)]`) on by default
help: remove these parentheses
|
2 - let x = (let y = 6);
2 + let x = let y = 6 ;
|
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted
let y = 6 不返回值,x 没什么可绑定的。这和 C、Ruby 不同——那些语言里赋值返回所赋的值,可以写 x = y = 6 让两个变量都等于 6;Rust 里不行。
表达式构成其余大部分代码:数学运算如 5 + 6 求值为 11;表达式可以是语句的一部分(let y = 6; 里的 6 就是表达式);调用函数、调用宏是表达式,花括号块也是表达式:
文件名:src/main.rs
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {y}");
}
其中的表达式:
{
let x = 3;
x + 1
}
是一个代码块,求值为 4,绑定到 y。注意 x + 1 结尾没有分号——表达式结尾不带分号;一旦加上分号,它就变成语句、不再返回值。请把这条记住,马上见效。
带返回值的函数
函数可以返回值:返回值不命名,但必须在箭头(->)之后声明类型。Rust 函数的返回值就是函数体最后一个表达式的值;可以用 return 关键字提前返回,但大多数函数隐式返回尾表达式:
文件名:src/main.rs
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");
}
five 的函数体只有孤零零的数字 5——没有调用、没有宏、没有 let,在 Rust 里完全合法。运行:
cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/functions`
The value of x is: 5
let x = five(); 因此等价于:
let x = 5;
再看一个例子:
文件名:src/main.rs
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1
}
输出 The value of x is: 6。那如果给 x + 1 加上分号、把它从表达式变成语句呢?
文件名:src/main.rs
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1;
}
编译就挂了:
cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
--> src/main.rs:7:24
|
7 | fn plus_one(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
8 | x + 1;
| - help: remove this semicolon to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` (bin "functions") due to 1 previous error
mismatched types 点破一切:函数声称返回 i32,但语句不求值,函数实际返回的是单元 (),与定义矛盾。Rust 连修法都替你想好了:删掉那个分号。
注释
惯用风格:// 开始、延续到行尾,多行注释每行都加:
// hello, world
// 我们在这里做的事情很复杂,长到需要用好
// 几行注释才说得清楚!呼!希望这条注释能
// 解释清楚这里到底发生了什么。
注释也可以放行尾,但更常见的是单独放在所说明代码的上一行:
文件名:src/main.rs
fn main() {
let lucky_number = 7; // 我今天感觉挺走运
}
文件名:src/main.rs
fn main() {
// 我今天感觉挺走运
let lucky_number = 7;
}
Rust 还有另一种注释——文档注释,第 14 章谈把 crate 发布到 Crates.io 时再讲。
控制流
按条件运行代码、按条件反复运行代码,是大多数语言最基本的构件。Rust 里控制执行流的主力是 if 表达式和循环。
if 表达式
if 允许你根据条件分支:条件为真跑一块代码,否则(有 else 时)跑另一块。新建一个 branches 项目:
文件名:src/main.rs
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
所有 if 表达式都以 if 关键字开头、后跟条件,条件为 true 时执行的代码块放在花括号里;这个块有时也叫分支(arm),与第 2 章 match 表达式的分支同名。else 可选——不写且条件为 false 时,程序跳过 if 块继续往后走。运行:
cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was true
把 number 换成让条件为 false 的值:
let number = 7;
再运行:
cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was false
注意,条件必须是 bool。if number 这种写法在别的语言司空见惯,Rust 直接拒绝:
文件名:src/main.rs
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
编译错误:
cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected `bool`, found integer
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
Rust 期望 bool、得到整数。与 Ruby、JavaScript 不同,Rust 不会自动把非布尔值转成布尔值,必须写得明确——比如要「数字非零才跑」,就写 if number != 0:
文件名:src/main.rs
fn main() {
let number = 3;
if number != 0 {
println!("number was something other than zero");
}
}
编译注: C/C++ 对条件做「非零即真」的隐式转换,
if (x = 5)这种把==手滑写成=的代码不但合法、还永远为真,是 C 系代码的经典事故来源;Rust 强制bool,这一整类 bug 在编译期清零。JS 的 Truthy/Falsy 规则在这里同样失效。
使用 else if 处理多个条件
if 和 else 可以组合成 else if 处理多个条件:
文件名:src/main.rs
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
运行:
cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
number is divisible by 3
条件自上而下依次检查,只执行第一个为 true 的块——6 虽然也能被 2 整除,后面的分支连检查都不会发生。else if 一多代码就乱,超过一个就要考虑重构;第 6 章的 match 是为这类场景准备的更强构造。
在 let 语句中使用 if
if 是表达式,于是可以直接放在 let 右侧把结果赋给变量,如示例 3-2:
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}
示例 3-2:把 if 表达式的结果赋值给变量
运行:
cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/branches`
The value of number is: 5
代码块求值为其中最后一个表达式,数字本身也是表达式,所以整个 if 的值取决于执行哪个块——这意味着两个分支必须是同一类型(示例 3-2 里都是 i32)。类型不一致就过不了编译:
文件名:src/main.rs
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {number}");
}
cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:4:44
|
4 | let number = if condition { 5 } else { "six" };
| - ^^^^^ expected integer, found `&str`
| |
| expected because of this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
if 块求值为整数、else 块求值为字符串,而变量必须有单一类型,且编译期就得定下来——否则编译器要为每个变量追踪多种假想类型,它自身更复杂,对代码的保证也更少。
用循环重复执行
Rust 提供三种循环(loop):loop、while 和 for。新建一个 loops 项目,挨个试。
使用 loop 重复执行代码
loop 关键字告诉 Rust 反复执行一块代码,永远执行下去,直到你明确叫停:
文件名:src/main.rs
fn main() {
loop {
println!("again!");
}
}
again! 会一直刷屏,直到用 ctrl-C 中断(^C 就是按键位置;之后再打一行与否,取决于中断信号到达时代码在循环里的位置):
cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
用代码叫停的两种方式,第 2 章都用过:break 告诉程序何时退出循环(猜数字游戏里猜对即退出);continue 跳过本次迭代剩余的代码,直接进入下一轮。
从循环返回值
loop 的一个高频用途是重试一个可能失败的操作(比如检查线程是否完成任务),并把结果交给循环外的代码。做法是在 break 后面带上值——它就是整个 loop 表达式的返回值:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
counter 数到 10 时 break counter * 2,result 得到 20。循环里也可以 return:break 只退出当前循环,return 总是退出当前函数。
编译注:
loop能带返回值,是「一切皆表达式」的延伸:C/C++ 的循环是语句,想把结果带出来得在循环外先声明变量、循环里赋值、break后再读,变量可见范围被迫放大一整层;Rust 的break value把重试逻辑的数据流收进let result = loop { ... };一行。第 9 章的错误处理会反复用到这个形状。
使用循环标签消除歧义
循环里嵌套循环时,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}");
}
外层 'counting_up 从 0 数到 2,内层无标签循环从 10 倒数到 9。裸 break 只退内层,break 'counting_up; 退外层:
cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
使用 while 简化条件循环
「条件为 true 就一直转」的循环,用 loop、if、else、break 也能拼出来,但这个模式太常见,Rust 直接内置了 while。示例 3-3 用它做三次倒计时:
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
示例 3-3:当条件求值为 true 时,用 while 循环运行代码
条件为 true 时代码运行,否则退出循环,省掉了手动组合的大量嵌套。
使用 for 遍历集合
用 while 也能遍历集合元素,示例 3-4:
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index += 1;
}
}
示例 3-4:用 while 循环遍历集合中的每个元素
运行会打印全部五个元素:
cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50
但这是绕远路:索引或条件写错就可能 panic——比如数组改成 4 个元素、条件忘了从 index < 5 同步改成 index < 4;而且编译器要在每轮迭代里加入运行时代码检查索引是否越界,更慢。更简洁的替代是 for 循环,直接对集合中每个元素执行代码:
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
示例 3-5:用 for 循环遍历集合中的每个元素
输出与示例 3-4 相同,但不可能越界或漏掉元素,生成的机器码也更高效;数组长度变了,也不需要记得去改别处的代码。
for 的安全与简洁使它成为 Rust 最常用的循环构造——连「按固定次数执行」这种事(比如示例 3-3 的倒计时),多数 Rustacean 也用 for 配合标准库的 Range(生成从一个数字开始、到另一个数字之前结束的序列)。加上还没讲过的 rev 方法反转范围,倒计时长这样:
文件名:src/main.rs
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
是不是清爽了一些?
小结与下一步
本章覆盖五块内容:变量、数据类型、函数、注释、控制流。核心结论:
- 变量默认不可变;
mut是显式申请,const是编译期常量,遮蔽是同名新绑定、还能换类型 - Rust 静态强类型:标量四种(整数默认
i32、浮点默认f64、bool、char)加元组/数组两种复合;数组越界在运行时 panic,而不是踩内存 - 整数溢出 debug 下 panic、release 下补码环绕,
wrapping_*等四族方法可显式处理 - Rust 基于表达式:块、
if、loop都有值,分号把表达式变成语句——一个分号能改变函数的返回类型 if条件必须是bool、两臂类型必须一致;for是最常用循环,配Range吃下固定次数场景
原书留了三个练习,值得一做:华氏度与摄氏度互转(练类型与函数)、生成第 n 个斐波那契数(练循环或递归)、打印圣诞颂歌「The Twelve Days of Christmas」歌词(练 for 与重复结构)。
下一篇:第 4 章「理解所有权」——Rust 真正的开始,本章所有概念都将在所有权面前重新洗牌。