- 教程
- 文档
【免费下载链接】book
The Rust Programming Language
本指南以 The Rust Programming Language 仓库中第三章(nostarch/chapter03.md,源码版见 src/ch03-00-common-programming-concepts.md)为核心脉络,系统讲解 Rust 中最基础的编程概念:变量与可变性、标量与复合数据类型、函数、注释以及if/循环等控制流结构。读完本篇,你将掌握 Rust 静态类型系统的使用习惯、表达式导向的函数设计,以及如何用loop、while、for写出既安全又高效的控制流程,为后续学习所有权(ownership)打下扎实基础。
这些概念几乎出现在每一门编程语言中,它们本身并非 Rust 独有,但本章会在 Rust 的语境下逐一展开,并说明其背后的使用约定。Rust 保留了一批关键字(keywords),它们只能被语言本身使用,不能用作变量名或函数名,完整列表见 appendix-01-keywords.md(即书中附录 A)。
变量与可变性
默认不可变:编译器帮你兜底
默认情况下,Rust 中的变量是**不可变(immutable)**的,这是 Rust 引导你写出更安全、更易并发代码的方式之一。当变量不可变时,一旦值绑定到名字上,就不能再修改。
下面用一个新项目来演示。在 projects 目录下执行cargo new variables创建项目,然后打开src/main.rs,替换为以下代码(仓库中的实际文件为 listings/ch03-common-programming-concepts/no-listing-01-variables-are-immutable/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,会得到一个不可变性错误(完整输出见 listings/ch03-common-programming-concepts/no-listing-01-variables-are-immutable/output.txt):
$ 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; | +++错误信息本身还给出了修复建议:把绑定改成let mut x = 5;。这个例子展示了编译器如何帮助你提前发现问题。遇到编译错误并不意味着你水平不行——经验丰富的 Rustacean 一样会遇到编译错误。编译器报错只说明程序还没有安全地实现你想要的行为。
为什么在编译期就拦截对不可变值的修改如此重要?因为这种场景极易滋生 bug:如果代码的一部分假设某个值永远不会变化,而另一部分悄悄改了它,程序就可能无法按预期工作,尤其是当第二处代码只是偶尔修改值时,问题会变得极难追踪。Rust 编译器保证:你说它不变,它就真的不变,你不需要自己操心,代码也因此更容易推理。
用mut显式声明可变
可变性有时能让代码更简洁方便。虽然变量默认不可变,但可以在变量名前加mut关键字使其可变(参考第二章猜数字游戏的用法)。mut同时也在向未来阅读代码的人传达意图:这个变量的值会被其他部分的代码修改。
修改 listings/ch03-common-programming-concepts/no-listing-02-adding-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 The value of x is: 5 The value of x is: 6是否使用可变性完全取决于你,以及你认为在特定场景下哪种写法最清晰。
声明常量(const)
常量(constants)与不可变变量类似,都是绑定到名字上且不允许改变的值,但有三点关键差异:
- 常量不允许使用
mut——常量不只是默认不可变,而是始终不可变。声明使用const关键字而非let,并且必须标注类型(类型与类型注解在下一节“数据类型”中讲解)。 - 常量可以在任意作用域声明,包括全局作用域,因此适合存放多处代码都需要知道的值。
- 常量只能被设置为常量表达式(constant expression),不能是运行时才能计算出来的结果。
示例:
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;常量名为THREE_HOURS_IN_SECONDS,其值是 60(每分钟秒数)× 60(每小时分钟数)× 3(小时数)的结果。Rust 对常量的命名约定是全大写、单词间用下划线分隔。编译器能在编译期求值有限的一组运算,因此可以写成60 * 60 * 3这种易读可校验的形式,而不必直接写死10800。
常量的生命周期覆盖整个程序运行期间(在其声明的作用域内),因此很适合表示应用领域中的通用值,比如游戏玩家可获得的最高分,或光速。把散落在程序各处的硬编码值命名为常量,既向维护者传达了值的含义,也让未来需要更新该值时只需修改一处。
遮蔽(Shadowing)
正如第二章猜数字游戏中看到的,你可以声明一个与之前变量同名的新变量。Rust 社区称第一个变量被第二个*遮蔽(shadowed)*了:当你使用该名字时,编译器看到的是第二个变量。实际上,第二个变量覆盖了第一个,把对该名字的所有使用都占为己有,直到它自己被遮蔽或作用域结束。
通过重复使用let关键字即可遮蔽变量(对应代码见 listings/ch03-common-programming-concepts/no-listing-03-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}"); }程序先把x绑定为5,然后用let x =创建新变量,取原值加1得6;接着在花括号创建的内部作用域中,第三个let再次遮蔽x,把上一值乘以2得12。该作用域结束后,内部遮蔽失效,x回到6。运行输出:
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是数字类型。遮蔽让我们免于绞尽脑汁起spaces_str、spaces_num之类的名字。但如果用mut这样做(见 listings/ch03-common-programming-concepts/no-listing-05-mut-cant-change-types/src/main.rs),会得到类型不匹配的编译错误:
let mut spaces = " "; spaces = spaces.len();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`错误说明:不允许改变变量的类型。
数据类型
Rust 中每个值都属于某种数据类型(data type),它告诉 Rust 正在处理什么数据、如何处理。数据类型分为两个子集:标量(scalar)和复合(compound)。
Rust 是*静态类型(statically typed)*语言,编译时必须知道所有变量的类型。编译器通常能根据值及其用法推断出类型;但当多种类型都可能时(例如第二章中把String用parse转成数字),就必须加上类型注解:
let guess: u32 = "42".parse().expect("Not a number!");如果不加: u32注解,Rust 会报E0284 type annotations needed错误,提示编译器需要更多信息来确定类型(仓库中对应的示例工程为 listings/ch03-common-programming-concepts/output-only-01-no-type-annotations)。
标量类型
标量类型表示单个值。Rust 有四种基本标量类型:整数、浮点数、布尔和字符。
整数类型
*整数(integer)*是没有小数部分的数字。第二章用到的u32表示无符号整数(signed 有符号类型以i开头,unsigned 无符号类型以u开头),占用 32 位空间。Rust 内置的整数类型如下表(表 3-1):
| 长度 | 有符号 | 无符号 |
|---|---|---|
| 8 位 | i8 | u8 |
| 16 位 | i16 | u16 |
| 32 位 | i32 | u32 |
| 64 位 | i64 | u64 |
| 128 位 | i128 | u128 |
| 依架构而定 | isize | usize |
有符号与无符号的区别在于数字是否可能为负:有符号数字需要携带符号(正号或负号),无符号数字只可能为正,因此无需符号位。有符号数字以二进制补码(two's complement)表示。每个有符号变体可存储 −(2ⁿ⁻¹) 到 2ⁿ⁻¹ − 1 的值(n 为该变体位数),例如i8可存 −128 到 127;无符号变体可存 0 到 2ⁿ − 1,例如u8可存 0 到 255。isize和usize的位数取决于运行程序的机器架构:64 位架构上是 64 位,32 位架构上是 32 位。
整数字面量可以写成下表(表 3-2)中任意形式。可以被多种数值类型解释的数字字面量允许带类型后缀,如57u8;数字字面量也允许用_作为视觉分隔符,如1_000与1000值相同:
| 数字字面量 | 示例 |
|---|---|
| 十进制 | 98_222 |
| 十六进制 | 0xff |
| 八进制 | 0o77 |
| 二进制 | 0b1111_0000 |
字节(仅u8) | b'A' |
该选哪个整数类型?如果不确定,Rust 的默认值通常是不错的起点:整数默认是i32。需要isize或usize的主要场景是索引某种集合。
关于整数溢出
假设有一个取值 0 到 255 的
u8变量,如果尝试改成超出范围的值(如 256),就会发生整数溢出(integer overflow),可能触发两种行为之一。调试模式编译时,Rust 会包含整数溢出检查,一旦发生溢出程序会在运行时panic(程序以错误退出即称为 panicking,详见 src/ch09-01-unrecoverable-errors-with-panic.md)。发布模式(--release标志)编译时,Rust不包含导致 panic 的溢出检查;溢出时 Rust 执行二进制补码环绕(two's complement wrapping)——超过类型最大值会“绕回”到最小值,u8的 256 变成 0、257 变成 1,以此类推。程序不会 panic,但变量值很可能不是你预期的。依赖这种环绕行为被视为错误。要显式处理溢出的可能性,标准库为原始数值类型提供了四组方法:
wrapping_*方法(如wrapping_add):在所有模式下都环绕;checked_*方法:溢出时返回None;overflowing_*方法:返回值并附一个表示是否溢出的布尔值;saturating_*方法:在最小值或最大值处饱和。
浮点类型
Rust 有两种原始浮点类型:f32(32 位)和f64(64 位)。默认类型是f64,因为在现代 CPU 上它和f32速度大致相同,但精度更高。所有浮点类型都是有符号的,遵循 IEEE-754 标准表示:
fn main() { let x = 2.0; // f64 let y: f32 = 3.0; // f32 }数值运算
Rust 支持所有数值类型的基本数学运算:加、减、乘、除、取余。整数除法向零截断。对应代码见 listings/ch03-common-programming-concepts/no-listing-07-numeric-operations/src/main.rs:
fn main() { // addition let sum = 5 + 10; // subtraction let difference = 95.5 - 4.3; // multiplication let product = 4 * 30; // division let quotient = 56.7 / 32.2; let truncated = -5 / 3; // Results in -1 // remainder let remainder = 43 % 5; }其中每个表达式都使用数学运算符求值为单个值,并绑定给一个变量。Rust 提供的全部运算符列表见 appendix-02-operators.md(附录 B)。
布尔类型
布尔类型有两个可能的值:true和false,占用 1 字节,类型名是bool(代码见 listings/ch03-common-programming-concepts/no-listing-08-boolean/src/main.rs):
fn main() { let t = true; let f: bool = false; // with explicit type annotation }布尔值主要通过条件(如if表达式)使用,详见后文“控制流”。
字符类型
Rust 的char类型是最基本的字母类型(代码见 listings/ch03-common-programming-concepts/no-listing-09-char/src/main.rs):
fn main() { let c = 'z'; let z: char = 'ℤ'; // with explicit type annotation let heart_eyed_cat = '😻'; }注意char字面量用单引号,字符串字面量用双引号。Rust 的char类型占 4 字节,表示一个 Unicode 标量值(Unicode scalar value),因此远不止 ASCII:带重音字母、中日韩文字、emoji、零宽空格都是合法的char值。Unicode 标量值范围为U+0000到U+D7FF及U+E000到U+10FFFF。不过“字符”在 Unicode 中并非一个真正的概念,你直觉上理解的“字符”可能与 Rust 的char并不一致,详见 src/ch08-02-strings.md 中“用字符串存储 UTF-8 编码文本”一节。
复合类型
复合类型可以把多个值组合成一个类型。Rust 有两种原始复合类型:元组和数组。
元组(Tuple)
元组是把多个不同类型的值组合成一个复合类型的通用方式。元组长度固定:声明后既不能增长也不能缩小。元组写法是用括号写出逗号分隔的值列表,每个位置有自己的类型,各位置类型不必相同:
fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); }变量tup绑定的是整个元组,因为元组被视为单个复合元素。取单个值有两种方式:
方式一:用模式匹配解构(destructure)(对应 listings/ch03-common-programming-concepts/no-listing-11-destructuring-tuples/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拆成x、y、z三个独立变量(即解构),再打印y的值6.4。
方式二:用句点加索引直接访问(对应 listings/ch03-common-programming-concepts/no-listing-12-tuple-indexing/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)。该值和对应类型都写作(),表示空值或空返回类型。表达式若没有返回其他值,就隐式返回单元值。
数组(Array)
另一种容纳多个值的方式是数组。与元组不同,数组的每个元素必须是同一类型;与某些其他语言的数组不同,Rust 数组长度固定:
fn main() { let a = [1, 2, 3, 4, 5]; }当你希望数据分配在栈上(栈与堆的讨论见 src/ch04-01-what-is-ownership.md),或希望确保元素数量固定时,数组很有用。但数组不如标准库的vector灵活——vector 的内容在堆上,允许增长或缩小。如果不确定该用数组还是 vector,大概率应该用 vector(详见 src/ch08-01-vectors.md)。
但当元素数量确定不变时,数组更合适,例如月份名永远有 12 个:
let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];数组类型用方括号书写:元素类型、分号、元素个数:
let a: [i32; 5] = [1, 2, 3, 4, 5];这里i32是元素类型,分号后的5表示数组含 5 个元素。还可以用[初始值; 长度]的简写把每个元素都初始化为同一个值:
let a = [3; 5];这等价于let a = [3, 3, 3, 3, 3];,但更简洁。
访问数组元素
数组是一块已知、固定大小的连续内存,可以分配在栈上。用索引访问元素:
fn main() { let a = [1, 2, 3, 4, 5]; let first = a[0]; let second = a[1]; }first得到索引[0]处的值1,second得到索引[1]处的值2。
无效的数组访问:Rust 内存安全的体现
看看如果访问越界会发生什么。运行下面的代码(对应 listings/ch03-common-programming-concepts/no-listing-15-invalid-array-access/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 会检查索引是否小于数组长度,索引大于等于长度就会 panic。这个检查必须在运行时进行——编译器不可能知道用户以后运行时会输入什么值。
这正是 Rust 内存安全原则的体现:许多底层语言不做这种检查,错误的索引会访问到无效内存;Rust 则通过立即退出而非允许内存访问继续执行来保护你。更深入的错误处理见 src/ch09-00-error-handling.md。
函数
函数在 Rust 代码中无处不在。你已经见过语言中最重要的函数之一:main函数,它是很多程序的入口;也见过声明新函数的fn关键字。
Rust 代码使用snake case作为函数和变量名的惯例风格:全小写、单词间用下划线分隔。示例:
fn main() { println!("Hello, world!"); another_function(); } fn another_function() { println!("Another function."); }定义函数:fn后跟函数名和一对括号,花括号告诉编译器函数体的起止。调用函数:函数名后跟一对括号。another_function定义在main之后,但这完全没关系——Rust 不关心函数定义在哪里,只关心它定义在调用者可见的某个作用域内。
新建一个名为functions的二进制项目(cargo new functions),把上面的例子放进src/main.rs运行:
$ cargo run Hello, world! Another function.行按main中出现的顺序执行:先打印 “Hello, world!”,再调用another_function打印它的消息。
参数(Parameters)
函数可以定义参数,即函数签名中的特殊变量。调用时传入的具体值称为实参(arguments),但日常交流中人们常把 parameter 和 argument 混用。
带参数的版本(对应 listings/ch03-common-programming-concepts/no-listing-17-functions-with-parameters/src/main.rs):
fn main() { another_function(5); } fn another_function(x: i32) { println!("The value of x is: {x}"); }输出:
The value of x is: 5another_function声明了一个名为x、类型为i32的参数。传入5后,println!宏把5填进格式字符串中{x}的位置。
函数签名中必须声明每个参数的类型,这是 Rust 设计上的刻意决定:函数定义要求类型注解,意味着编译器几乎不需要你在其他地方标注类型,而且编译器在知道函数期望类型后,能给出更有用的错误信息。
多个参数用逗号分隔:
fn main() { print_labeled_measurement(5, 'h'); } fn print_labeled_measurement(value: i32, unit_label: char) { println!("The measurement is: {value}{unit_label}"); }运行输出The measurement is: 5h。
语句与表达式
函数体由一系列语句组成,可选地以表达式结尾。Rust 是基于表达式的语言(expression-based),区分两者非常重要:
- 语句(statements):执行某种操作、不返回值;
- 表达式(expressions):求值为一个结果值。
let y = 6;就是一条语句(见 listings/ch03-common-programming-concepts/no-listing-19-statements-vs-expressions/src/main.rs 对应的输出工程)。函数定义本身也是语句。
语句不返回值,因此不能把let语句赋值给另一个变量:
fn main() { let x = (let y = 6); }运行会得到error: expected expression, found 'let' statement。这不同于 C、Ruby 等语言(它们允许x = y = 6让两个变量都等于 6),Rust 中做不到。
表达式求值为值,构成你写的其余大部分代码。5 + 6是求值为11的表达式;let y = 6;中的6是表达式;调用函数是表达式;调用宏是表达式;用花括号创建的新作用域块也是表达式:
fn main() { let y = { let x = 3; x + 1 }; println!("The value of y is: {y}"); }这个块表达式求值为4,作为let语句的一部分绑定给y。注意x + 1这一行末尾没有分号——表达式不以分号结尾,一旦加上分号,表达式就变成语句,不再返回值。这一点在理解函数返回值时至关重要。
带返回值的函数
函数可以向调用它的代码返回值。我们不命名返回值,但必须在箭头->后声明返回类型。在 Rust 中,函数返回值等同于函数体最后一个表达式的值。也可以用return关键字提前返回并指定值,但大多数函数隐式返回最后一个表达式。
示例(对应 listings/ch03-common-programming-concepts/no-listing-21-function-return-values/src/main.rs):
fn five() -> i32 { 5 } fn main() { let x = five(); println!("The value of x is: {x}"); }five函数体里没有函数调用、宏甚至let语句,只有一个孤零零的5——这在 Rust 中是完全合法的函数。返回类型是-> i32。let x = five();等价于let x = 5;。five函数体中的5没有分号,因为它是一个我们想返回其值的表达式。
再看一个例子:
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后面加上分号,把它从表达式变成语句呢?
fn plus_one(x: i32) -> i32 { x + 1; }编译会报错:
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核心错误是mismatched types:plus_one声明返回i32,但语句不求值,表现为单元类型(),因此什么都没返回,与函数定义矛盾。错误输出中还给出了修复建议:去掉分号。
注释
程序员都努力让代码易于理解,但有时确实需要额外解释。这时在源码中留下注释——编译器会忽略、但阅读源码的人可能受益:
// hello, worldRust 惯用的注释风格是两个斜杠开头,注释延续到行尾。多行注释需要每行都写//:
// So we're doing something complicated here, long enough that we need // multiple lines of comments to do it! Whew! Hopefully, this comment will // explain what's going on.注释也可以放在含代码的行尾(对应 listings/ch03-common-programming-concepts/no-listing-24-comments-end-of-line/src/main.rs):
fn main() { let lucky_number = 7; // I'm feeling lucky today }但更常见的写法是把注释放在它所注解的代码上方单独一行(对应 listings/ch03-common-programming-concepts/no-listing-25-comments-above-line/src/main.rs):
fn main() { // I'm feeling lucky today let lucky_number = 7; }Rust 还有另一种注释——文档注释(documentation comments),详见 src/ch14-02-publishing-to-crates-io.md 中“发布 crate 到 crates.io”一节。
控制流
根据条件是否为true执行某些代码,以及在条件为true时重复执行某些代码,是大多数编程语言的基本构件。Rust 中最常见的控制执行流的结构是if表达式和循环。
if 表达式
if表达式让你根据条件分支代码。新建名为branches的项目,在src/main.rs中输入:
fn main() { let number = 3; if number < 5 { println!("condition was true"); } else { println!("condition was false"); } }所有if表达式都以if关键字开头,后跟条件。此处条件检查number是否小于 5。条件为true时要执行的代码块紧跟在条件之后、放在花括号内。与if表达式条件关联的代码块有时称为arm(分支),类似第二章match表达式的 arm。可选地可以加else表达式,提供条件为false时执行的备选代码块;不提供else时,条件为假则跳过if块继续执行。
运行输出condition was true;把number改成7,输出变为condition was false。
条件必须是bool类型。如果条件不是bool,会报错。例如:
fn main() { let number = 3; if number { println!("number was three"); } }error[E0308]: mismatched types --> src/main.rs:4:8 | 4 | if number { | ^^^^^^ expected `bool`, found integer与 Ruby、JavaScript 不同,Rust不会自动把非布尔类型转换为布尔值,你必须显式给if提供布尔条件。想让if代码块在数字不等于 0 时执行,要写成:
fn main() { let number = 3; if number != 0 { println!("number was something other than zero"); } }用 else if 处理多个条件
把if和else组合成else if即可处理多个条件:
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"); } }这个程序有四个可能的路径,运行输出number is divisible by 3。程序按顺序检查每个if,执行第一个条件为true的分支体。注意虽然 6 能被 2 整除,却看不到number is divisible by 2,也看不到else块的输出——Rust 只执行第一个true条件的块,找到后就不再检查其余条件。
else if用多了会使代码臃肿,如果超过一个,建议考虑重构。第六章将介绍 Rust 更强大的分支结构match(见 src/ch06-02-match.md)。
在 let 语句中使用 if
因为if是表达式,可以放在let语句右侧,把结果赋给变量:
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {number}"); }number会根据if表达式的结果绑定值,运行输出The value of number is: 5。代码块求值为其中的最后一个表达式,数字本身也是表达式。整个if表达式的值取决于执行哪个分支块,因此每个分支可能产生的结果必须是同一类型——上例中if分支和else分支都是i32。类型不匹配会报错:
fn main() { let condition = true; let number = if condition { 5 } else { "six" }; println!("The value of number is: {number}"); }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`if块求值为整数、else块求值为字符串,这行不通,因为变量必须只有一个类型,Rust 需要在编译期确定number的类型。如果类型只能到运行时才知道,编译器就不得不跟踪变量的多种假设类型,既更复杂,也无法对代码做出足够多的保证。
用循环重复执行
Rust 提供几种循环:执行完循环体后立即回到开头。新建名为loops的项目来实验。Rust 有三种循环:loop、while和for。
用 loop 重复代码
loop关键字让 Rust 反复执行代码块,要么永远执行,要么直到你显式叫停。例如(对应 listings/ch03-common-programming-concepts/no-listing-32-loop/src/main.rs):
fn main() { loop { println!("again!"); } }运行后again!会不停打印,直到手动停止。大多数终端支持ctrl-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!^C代表你按下ctrl-C的位置。^C之后是否还出现again!,取决于收到中断信号时循环执行到哪里。
Rust 也提供了用代码跳出循环的方式:在循环内使用break关键字告诉程序何时停止执行循环。第二章猜数字游戏中就在用户猜对后用它退出程序。continue则跳过本次迭代剩余代码,直接进入下一次迭代。
从循环返回值
loop的一个用途是重试你可能知道会失败的操作,比如检查线程是否完成任务,并把操作结果带出循环交给后续代码。为此,可以在用来停止循环的break表达式后面加上想返回的值;该值会从循环中返回出来:
fn main() { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; println!("The result is {result}"); }循环前声明counter并初始化为0,再声明result存放循环返回值。每次迭代counter加 1,检查是否等于 10,等于时用break携带counter * 2退出。循环结束后用分号结束赋值语句,最后打印result,本例为20。
也可以从循环内部return:break只退出当前循环,return则始终退出当前函数。
用循环标签消歧
循环嵌套时,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;退出外层循环。运行输出:
count = 0 remaining = 10 remaining = 9 count = 1 remaining = 10 remaining = 9 count = 2 remaining = 10 End count = 2用 while 精简条件循环
程序经常需要在循环内求值条件:条件为true时循环运行,变为false时调用break停止。用loop、if、else、break组合可以实现这种模式,但该模式太常见,Rust 为此内置了while循环构造。下面的例子用while让程序循环三次,每次倒计时,循环结束后打印消息并退出:
fn main() { let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; } println!("LIFTOFF!!!"); }这个构造消除了用loop/if/else/break组合时的大量嵌套,更清晰:条件为true时运行代码,否则退出循环。
用 for 遍历集合
也可以用while遍历集合元素,比如数组。下面的循环打印数组a的每个元素:
fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { println!("the value is: {}", a[index]); index += 1; } }代码从索引 0 开始,一直循环到index < 5不再为 true(即到达数组最后索引)。运行会打印全部五个元素。
但这种方式容易出错:如果索引值或测试条件不正确,程序可能 panic。例如把a改成四个元素却忘了把条件改成while index < 4,代码就会 panic。它也比较慢,因为编译器会加入运行时代码,在每次迭代执行索引是否在数组边界内的条件检查。
更简洁的替代方案是for循环——对集合中的每个元素执行某些代码:
fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); } }输出与while版本相同,但安全性提高,彻底消除了越界或漏掉元素的 bug 隐患;而且for循环生成的机器码可能更高效,因为索引无需在每次迭代与数组长度比较。for循环下,即使改变数组元素数量,也不需要像 Listing 3-4 那样去改其他代码。
for循环的安全与简洁使其成为 Rust 中最常用的循环构造。即使想执行固定次数(像上面while倒计时那样),多数 Rustacean 也会用for配合标准库提供的Range(范围)——它从起始数字开始、在另一个数字之前结束,生成一列数字。用for和rev(反转范围)实现倒计时:
fn main() { for number in (1..4).rev() { println!("{number}!"); } println!("LIFTOFF!!!"); }这段代码是不是清爽多了?
总结
本章内容相当可观:你学习了变量、标量与复合数据类型、函数、注释、if表达式以及三种循环。要练习本章概念,可以试着编写以下程序:
- 在华氏温度与摄氏温度之间互相转换;
- 生成第n个斐波那契数;
- 打印圣诞颂歌《The Twelve Days of Christmas》的歌词,充分利用歌曲中的重复。
完成这些练习后,就可以进入 Rust 中一个不常见于其他语言的独特概念:所有权(ownership),见 src/ch04-00-understanding-ownership.md。
- 教程
- 文档
【免费下载链接】book
The Rust Programming Language
相关推荐
3 行配置,用 kkFileView CAD 批注在浏览器里直接上手画图纸
3 行配置,用 kkFileView CAD 批注在浏览器里直接上手画图纸 看 DWG 总得先打开 CAD?用 kkFileView CAD 批注,浏览器里就能
后端SQLModel 数据库入门:从数据库概念到 SQL 关系模型的完整指南
SQLModel 数据库入门:从数据库概念到 SQL 关系模型的完整指南 本指南基于 SQLModel 官方文档《Intro to Databases》整理,面
ORM数据库后端90DaysOfDevOps 实战:用 Go 编写变量、常量与数据类型入门程序(Day 11)
90DaysOfDevOps 实战:用 Go 编写变量、常量与数据类型入门程序(Day 11) 本篇技术指南是 90DaysOfDevOps 学习路径中 Go
文档/教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考