2026最新Rust入门教程[03]:变量、类型、函数与控制流

70 阅读27分钟

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 位i8u8
16 位i16u16
32 位i32u32
64 位i64u64
128 位i128u128
视架构而定isizeusize

有符号(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_add
  • checked_*:溢出时返回 None
  • overflowing_*:返回值和一个指示是否溢出的布尔值
  • 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 真正的开始,本章所有概念都将在所有权面前重新洗牌。