上一篇:02-编程猜谜游戏
本章介绍几乎所有编程语言中都会出现的概念,以及它们在 Rust 中的工作原理。许多编程语言的核心都有许多共同点。本章介绍的概念都不是 Rust 独有的,但我们会在 Rust 的上下文中讨论这些概念,并解释使用这些概念的惯例。
具体来说,将学习变量、基本类型、函数、注释和控制流。每个 Rust 程序中都会有这些基础知识,及早学习这些知识将为你的起步打下坚实的基础。
Rust 语言与其他语言一样,有一组关键字仅供该语言使用。请记住,不能将这些关键字用作变量或函数的名称。大多数关键字都有特殊含义,你将在 Rust 程序中使用它们完成各种任务;少数关键字目前没有相关功能,但被保留用于将来可能添加到 Rust 中的功能。
默认情况下,变量是不可变的。这是 Rust 给你的众多提示之一,让你在编写代码时充分利用 Rust 提供的安全性和易并发性。不过,你仍然可以选择让变量可变。让我们来探讨一下 Rust 如何以及为什么鼓励你偏爱不可变性,以及为什么有时你可能会想选择放弃。
如果变量是不可变的,那么一旦某个值与名称绑定,就无法更改该值。为了说明这一点,请使用 cargo new variables 在你的项目目录中生成一个名为 variables 的新项目。然后,在新的变量目录下,打开 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.exe run
Compiling variables v0.1.0 (D:\rustProj\variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src\main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| help: consider making this binding mutable: `mut x`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to previous error
这个示例展示了编译器如何帮助你发现程序中的错误。编译器错误可能会令人沮丧,但实际上它们只意味着你的程序还不能安全地完成你想要它做的事情;它们并不意味着你不是一个优秀的程序员!经验丰富的 Rustaceans 仍然会遇到编译器错误。
你收到错误信息 cannot assign twice to immutable variable `x` 是因为你试图给不可变的 x 变量赋第二个值。
当我们试图更改指定为不可变的值时,编译时出错是很重要的,因为这种情况可能会导致错误。如果我们代码的一部分假定某个值永远不会改变,而另一部分代码却改变了该值,那么前一部分代码就有可能无法完成其设计目标。这种错误的原因可能很难事后追踪,尤其是当第二部分代码只是偶尔改变值时。Rust 编译器保证,当您声明某个值不会改变时,它确实不会改变,因此您不必自己跟踪它。这样,您的代码就更容易推理了。
不过,可变性可能非常有用,可以让代码编写更方便。虽然变量在默认情况下是不可变的,但也可以通过在变量名前添加 mut 来使其可变。添加 mut 还可以向代码的未来读者传达意图,表明代码的其他部分将改变该变量的值。
当使用 mut 时,我们可以将绑定到 x 的值从 5 改为 6 。最终,是否使用可变性取决于您自己,取决于您认为在特定情况下什么是最明确的。
与不可变变量一样,常量也是与名称绑定且不允许更改的值,但常量和变量之间有一些区别。
首先,不允许在常量中使用 mut 。常量不仅默认不可变,而且始终不可变。您可以使用 const 关键字而不是 let 关键字来声明常量,并且必须注释值的类型。
常量可以在任何作用域(包括全局作用域)中声明,因此对于代码中许多部分都需要知道的值来说,常量非常有用。
最后一个区别是,常量只能设置为一个常量表达式,而不能设置为只能在运行时计算的值的结果。
下面是一个常量声明的示例:
const THERR_HOURS_IN_SECONDS: u32 = 60 * 60 *3;
常量的名称是 THREE_HOURS_IN_SECONDS ,其值设置为 10800。Rust 的常量命名规则是使用全大写字母,字与字之间使用下划线。编译器可以在编译时评估有限的操作集,这让我们可以选择以更容易理解和验证的方式写出这个值,而不是将这个常量设置为 10,800 值。
常量在程序运行的整个过程中,在常量声明的作用域内都是有效的。常量的这一特性对于程序中多个部分可能需要了解此值非常有用,例如游戏中允许任何玩家获得的最大点数或光速。
将整个程序中使用的硬编码值命名为常量,有助于向未来的代码维护者传达该值的含义。此外,如果将来需要更新硬编码值,只需更改代码中的一处即可。
正如在第 2 章的猜谜游戏教程中所看到的,你可以声明一个与前一个变量同名的新变量。Rustaceans 说第一个变量会被第二个变量遮挡,这意味着当你使用变量名时,编译器会看到第二个变量。实际上,第二个变量会覆盖第一个变量,将变量名的任何使用都带到自己身上,直到它自己被阴影覆盖或作用域结束。我们可以通过使用相同的变量名和重复使用 let 关键字来对变量进行阴影处理,如下所示:
fn main() {
let x: i32 = 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 = 来创建一个新变量 x ,将原来的值加上 1 ,这样 x 的值就是 6 。然后,在用大括号创建的内部作用域中,第三个 let 语句也会对 x 进行阴影处理,并创建一个新变量,将之前的值乘以 2 ,使 x 的值为 12 。当该作用域结束时,内部阴影结束, x 返回到 6;
当我们运行这个程序时,它将输出如下内容:
cargo.exe run
Compiling variables v0.1.0 (D:\rustProj\variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.58s
Running `target\debug\variables.exe`
The value of x in the inner scope is:12
The value of x is:6
Shadowing 与将变量标记为 mut 不同,因为如果我们不小心在没有使用 let 关键字的情况下将变量重新赋值,就会在编译时出错。通过使用 let ,我们可以对一个值执行一些变换,但在这些变换完成后,变量将不可变。
mut 和阴影之间的另一个区别是,当我们再次使用 let 关键字时,实际上是创建了一个新变量,因此我们可以改变值的类型,但可以重复使用相同的名称。例如,我们的程序要求用户通过输入空格字符来显示他们希望在某些文本之间输入多少个空格,然后我们希望将输入值存储为一个数字:
let spaces = " ";
let spaces = spaces.len();
第一个 spaces 变量是字符串类型,第二个 spaces 变量是数字类型。因此,我们不必使用不同的名称,如 spaces_str 和 spaces_num ;相反,我们可以重复使用更简单的 spaces 名称。但是,如果我们尝试使用 mut ,就会出现编译时错误:
fn main() {
let cat = "BlueCat";
let cat = "Muppet";
let mut space = " ";
space = space.len();
}
错误提示我们不允许更改变量的类型:
cargo.exe build
Compiling variables v0.1.0 (D:\rustProj\variables)
error[E0308]: mismatched types
--> src\main.rs:6:13
|
5 | let mut space = " ";
| ---- expected due to this value
6 | space = space.len();
| ^^^^^^^^^^^ expected `&str`, found `usize`
|
help: try removing the method call
|
6 - space = space.len();
6 + space = space;
|
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to previous error
既然我们已经了解了变量的工作原理,那么让我们来看看变量可以有哪些数据类型。
Rust 中的每个值都有特定的数据类型,这就告诉了 Rust 所指定的数据类型,以便它知道如何处理这些数据。我们将研究两个数据类型子集:标量和复合。
Rust 是一种静态类型语言,这意味着它必须在编译时知道所有变量的类型。编译器通常可以根据值和使用方式推断出我们想要使用的类型。
在可能存在多种类型的情况下,例如我们在第 2 章 "将猜测的数字与秘密数字进行比较 "一节中使用 parse 将 String 转换为数字类型时,我们必须添加一个类型注解,就像下面这样:
let guess: u32 = "42".parse().expect("Not a number!");
如果我们不添加前面代码中显示的 : u32 类型注解,Rust 将显示如下错误,这意味着编译器需要我们提供更多信息才能知道我们要使用哪种类型:
cargo.exe build
Compiling variables v0.1.0 (D:\rustProj\variables)
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 `variables` (bin "variables") due to previous error
你会看到其他数据类型有不同的类型注解。
标量类型表示单个值。Rust 有四种主要的标量类型:整数、浮点数、布尔型和字符型。您可能在其他编程语言中见过这些类型。让我们来了解一下它们在 Rust 中是如何工作的。
整数是没有小数成分的数字。我们在第 2 章中使用了一种整数类型,即 u32 类型。该类型声明表示与之关联的值应该是一个无符号整数(有符号整数类型以 i 而不是 u 开头),占用 32 位空间。表 3-1 列出了 Rust 中的内置整数类型。我们可以使用其中任何一种变量来声明整数值的类型。
每个变量都可以是有符号或无符号的,并有明确的大小。有符号和无符号指的是数字是否有可能是负数:换句话说,数字是否需要带有符号(有符号),或者数字是否只能是正数,因此可以不带符号(无符号)。这就像在纸上书写数字:当符号很重要时,数字会用加号或减号表示;然而,当可以肯定数字是正数时,数字就不带符号。有符号的数字使用二进制表示法存储。
每个带符号的变量可以存储从到 的数字,其中 n 是该变体使用的比特数。因此,一个 i8 可以存储 -() 到的数字,相当于 -128 到 127。无符号变量可以存储 0 至的数字,因此 u8 可以存储 0 至 的数字,相当于 0 至 255。
此外, isize 和 usize 类型取决于程序运行的计算机体系结构,在表中用 "arch "表示:如果是 64 位架构,则为 64 位;如果是 32 位架构,则为 32 位。
可以用下表的任何形式编写整数字面量。需要注意的是,可以是多种数值类型的数字字面量允许使用类型后缀,如 57u8 ,来指定类型。
数字字面量也可以使用_作为视觉分隔符,使数字更容易阅读,如 1_000 ,其值与指定的 1000 相同。
那么,如何知道使用哪种整数类型呢?如果你不确定,Rust 的默认值通常是个很好的开始:整数类型默认为 i32 。使用 isize 或 usize 的主要情况是索引某种集合。
整型溢出:
假设你有一个类型为 u8 的变量,它可以保存 0 到 255 之间的值。如果您尝试将该变量更改为该范围之外的值,例如 256,就会发生整数溢出,这可能导致两种行为之一。在调试模式下编译时,Rust 会对整数溢出进行检查,如果出现这种情况,程序会在运行时panic 。
当你使用 --release 标志在 release 模式下编译时,Rust 不会对导致恐慌的整数溢出进行检查。相反,如果发生溢出,Rust 会执行二的补码。简而言之,大于类型所能容纳的最大值的值会 "缠绕 "到类型所能容纳的最小值。在 u8 的情况下,值 256 会变成 0,值 257 会变成 1,以此类推。程序不会慌乱,但变量的值可能不是你期望的值。依赖整数溢出的包装行为被视为错误。
要明确处理溢出的可能性,可以使用标准库为原始数值类型提供的这些方法系列:
① 使用 wrapping_* 方法对所有模式进行包裹,如 wrapping_add ;
② 如果 checked_* 方法出现溢出,则返回 None 值。
③ 返回值和一个布尔值,表示 overflowing_* 方法是否存在溢出。
④ 使用
saturating_*
方法对数值的最小值或最大值进行饱和处理。
Rust 还为浮点数(带有小数点的数)提供了两种原始类型。Rust 的浮点类型是 f32 和 f64 ,大小分别为 32 位和 64 位。默认类型是 f64 ,因为在现代 CPU 上,它的速度与 f32 大致相同,但精度更高。所有浮点类型都是带符号的。
下面的示例展示了浮点数的实际应用:
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
浮点数根据 IEEE-754 标准表示。 f32 类型是单精度浮点数, f64 是双精度浮点数。
Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、除法和余数。整数除法向零截断到最接近的整数。以下代码展示了如何在 let 语句中使用每种数字运算:
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; // Result in -1
// remainder
let remainder = 43 % 5;
print!("sum:{sum}, difference:{difference}, product:{product}, ");
println!("quotient:{quotient}, truncated:{truncated}, remainder:{remainder}");
}
与大多数其他编程语言一样,Rust 中的布尔类型有两种可能的值: true 和 false 。布尔类型的大小为一个字节。Rust 中的布尔类型使用 bool 指定。例如
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
使用布尔值的主要方式是通过条件,例如 if 表达式;
Rust 的 char 类型是该语言最原始的字母类型。下面是一些声明 char 值的示例:
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '';
}
请注意,我们使用单引号指定 char 字面量,而字符串字面量则使用双引号。Rust 的 char 类型大小为 4 个字节,表示 Unicode 标量值,这意味着它不仅可以表示 ASCII 码,还可以表示很多其他字符。重音字母、中文、日文和韩文字符、表情符号和零宽度空格都是 Rust 中有效的 char 值。Unicode 标量值的范围从 U+0000 到 U+D7FF ,从 U+E000 到 U+10FFFF 。不过,"字符 "在 Unicode 中并不是一个真正的概念,因此你对 "字符 "的直觉可能与 Rust 中的 char 并不一致。
复合类型可以将多个值组合成一个类型。Rust 有两种原始的复合类型:元组和数组。
元组是将多种类型的数值组合成一个复合类型的通用方法。元组有固定的长度:一旦声明,其大小就不能增大或缩小。
我们通过在括号内写入一个以逗号分隔的值列表来创建一个元组。元组中的每个位置都有一个类型,元组中不同值的类型不一定相同。我们在本例中添加了可选的类型注解:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
变量 tup 与整个元组绑定,因为元组被视为一个单一的复合元素。要从元组中获取单个值,我们可以使用模式匹配来重组元组值,如下所示:
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}
有点像枚举类,但仿佛比枚举类功能更强;
该程序首先创建一个元组,并将其绑定到变量 tup 上。然后,它使用 let 的模式将 tup 变成三个独立的变量: x 、 y 和 z 。这就是所谓的 "去结构化",因为它将单个元组分解为三个部分。最后,程序打印出 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;
}
该程序创建了一个元组 x ,然后使用各自的索引访问元组中的每个元素。与大多数编程语言一样,元组中的第一个索引为 0。
没有任何值的元组有一个特殊的名称,即 unit。该值及其对应的类型都被写为 () ,代表空值或空返回类型。如果表达式不返回任何其他值,则隐式返回 unit 值。
另一种拥有多个值集合的方法是使用数组。与元组不同,数组的每个元素都必须具有相同的类型。与其他一些语言中的数组不同,Rust 中的数组有固定的长度。
我们将数组中的值写成方括号内用逗号分隔的列表:
fn main() {
let a = [1, 2, 3, 4, 5];
}
当你希望在栈而不是堆上分配数据时,或者当你希望确保总是有固定数量的元素时,数组就非常有用了。不过,数组不如vector类型灵活。vector是标准库提供的一种类似的集合类型,其大小可以增大或缩小。如果你不确定是使用数组还是vector,那么很可能应该使用vector。
不过,当你知道元素的数量不需要改变时,数组会更有用。例如,如果你要在程序中使用月份名称,你可能会使用数组而不是vector,因为你知道数组总是包含 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];
名为 a 的数组将包含 5 元素,这些元素的初始值都将设置为 3 。这与 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 的变量将得到 1 的值,因为这是数组中索引 [0] 的值。名为 second 的变量将从数组中索引 [1] 处获取值 2 。
让我们来看看如果尝试访问数组中超过数组末尾的元素会发生什么。假设运行这段代码,类似于第 2 章中的猜谜游戏,从用户处获取数组索引:
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}");
}
该代码编译成功。如果使用 cargo run 运行此代码,并输5,则会看到如下输出:
cargo.exe run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target\debug\variables.exe`
Please enter an array index.
5
thread 'main' panicked at src\main.rs:19:19:
index out of bounds: the len is 5 but the index is 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\variables.exe` (exit code: 101)
程序在索引操作中使用无效值时出现运行时错误。程序带着错误信息退出,并且没有执行最后的 println! 语句。当你尝试使用索引访问元素时,Rust 会检查你指定的索引是否小于数组长度。如果索引大于或等于长度,Rust 就会慌乱。这种检查必须在运行时进行,尤其是在这种情况下,因为编译器不可能知道用户稍后运行代码时会输入什么值。
这是 Rust 内存安全原则发挥作用的一个例子。在许多底层语言中,这种检查是不存在的,当你提供了一个不正确的索引时,就会访问到无效的内存。Rust 会立即退出,而不是允许访问内存并继续,从而防止出现这种错误。
函数在 Rust 代码中非常普遍。您已经看到了该语言中最重要的函数之一: main 函数,它是许多程序的入口点。你还见过 fn 关键字,它允许你声明新函数。
Rust 代码使用蛇形大小写作为函数和变量名的常规样式,其中:所有字母都是小写,下划线分隔单词。下面是一个包含函数定义示例的程序:
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
在 Rust 中定义函数时,我们需要输入 fn ,然后输入函数名和一组括号。大括号告诉编译器函数体的开始和结束位置。
我们可以调用我们定义的任何函数,只需输入其名称并在后面加上一组括号即可。由于 another_function 是在程序中定义的,因此可以从 main 函数内部调用它。请注意,我们在源代码中的 main 函数之后定义了 another_function ;我们也可以在之前定义它。Rust 并不在乎你在哪里定义函数,只在乎函数是否定义在调用者可以看到的作用域中。
这点与C/C++语言不同;
我们可以为函数定义参数,这些参数是特殊变量,是函数签名的一部分。当函数有参数时,可以为这些参数提供具体的值。从技术上讲,这些具体值被称为参数,但在闲聊中,人们倾向于交替使用参数(parameter)和参数(argument)这两个词来指代函数定义中的变量或调用函数时传入的具体值。
parameter:形参;
argument: 实参;
在 another_function 这个版本中,我们添加了一个参数:
fn main() {
another_function(3.14);
}
fn another_function(x: f64) {
println!("The value of x:{x}");
}
请尝试运行该程序,您将得到以下输出结果:
cargo.exe run
Compiling another_function v0.1.0 (D:\rustProj\another_function)
Finished dev [unoptimized + debuginfo] target(s) in 0.65s
Running `target\debug\another_function.exe`
The value of x:3.14
another_function 的声明有一个名为 x 的参数。 x 的类型指定为 f64。当我们将 3.14 传递给 another_function 时, println! 宏会将 3.14放在格式字符串中包含 x 的那对大括号的位置。
在函数签名中,必须声明每个参数的类型。这是 Rust 设计中的一个深思熟虑的决定:要求在函数定义中进行类型注解意味着编译器几乎不需要在代码的其他地方使用类型注解来确定你所指的是什么类型。如果编译器知道函数所期望的类型,它还能给出更有用的错误信息。
定义多个参数时,请使用逗号分隔参数声明,如下所示:
fn main() {
another_function(3.14, 'T');
}
fn another_function(x: f64, y: char) {
println!("The value of x:{x}, y:{y}");
}
让我们试着运行这段代码:
cargo.exe run
Compiling another_function v0.1.0 (D:\rustProj\another_function)
Finished dev [unoptimized + debuginfo] target(s) in 0.58s
Running `target\debug\another_function.exe`
The value of x:3.14, y:T
函数体由一系列语句组成,可选择以表达式结尾。到目前为止,我们所涉及的函数还没有包含结束表达式,但我们已经看到表达式作为语句的一部分。因为 Rust 是一种基于表达式的语言,所以理解这一点很重要。其他语言没有相同的区别,因此让我们来看看什么是语句和表达式,以及它们的区别如何影响函数的主体。
①. 语句是执行某些操作但不返回值的指令;
②. 表达式会求得一个结果值;
表达式有返回值,语句没有返回值;
实际上,我们已经使用过语句和表达式。使用 let 关键字创建变量并赋值就是语句。
fn main() {
let y = 6;
}
函数定义也是语句;上面整个例子本身就是一个语句。
语句不返回值。因此,不能像下面的代码那样将 let 语句赋值给另一个变量,否则会出错:
fn main() {
let x = (let y = 6);
}
运行此程序时,会出现如下错误:
cargo.exe build
Compiling another_function v0.1.0 (D:\rustProj\another_function)
error: expected expression, found `let` statement
--> src\main.rs:2:14
|
2 | let x = (let y = 8);
| ^^^
|
= 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 = 8);
| ^ ^
|
= note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
|
2 - let x = (let y = 8);
2 + let x = let y = 8;
|
warning: `another_function` (bin "another_function") generated 1 warning
error: could not compile `another_function` (bin "another_function") due to previous error; 1 warning emitted
表达式会求值,是 Rust 代码的主要组成部分。考虑一个数学运算,例如 5 + 6 ,它是一个求值为 11 的表达式。表达式可以是语句的一部分:上述代码中,语句 let y = 6; 中的 6 是一个表达式,其值为 6 。调用函数是一个表达式。调用宏也是一个表达式。例如,用大括号创建的新作用域块就是一个表达式:
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y:{y}");
}
这种做法:
{
let x = 3;
x + 1
}
是一个代码块,在本例中,其值为 4 。作为 let 语句的一部分,该值被绑定到 y 。请注意, x + 1 行的末尾没有分号,这与您目前看到的大多数行不同。表达式不包括结尾的分号。如果在表达式末尾添加分号,表达式就会变成语句,并且不会返回值。在接下来学习函数返回值和表达式时,请牢记这一点。
函数可以向调用它的代码返回值。我们不为返回值命名,但必须在箭头 ( -> ) 后声明其类型。在 Rust 中,函数的返回值与函数体块中最终表达式的值同义。您可以使用 return 关键字并指定一个值来提前返回函数,但大多数函数都是隐式返回最后一个表达式。下面是一个返回值的函数示例:
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x:{x}");
}
five 函数中没有函数调用、宏,甚至没有 let 语句,只有数字 5 本身。在 Rust 中,这是一个完全有效的函数。请注意,函数的返回类型也被指定为 -> i32 。试着运行这段代码,输出结果应该是这样的:
cargo.exe run
Compiling another_function v0.1.0 (D:\rustProj\another_function)
Finished dev [unoptimized + debuginfo] target(s) in 0.60s
Running `target\debug\another_function.exe`
The value of x:5
five 中的 5 是函数的返回值,这就是返回类型为 i32 的原因。让我们来详细研究一下。有两个重要的部分:首先, let x = five(); 这一行表明我们正在使用函数的返回值来初始化一个变量。因为函数 five 的返回值是 5 ,所以该行与下面的内容相同:
let x = 5;
其次, five 函数没有参数,并定义了返回值的类型,但函数体是一个没有分号的孤零零的 5 ,因为它是一个表达式,我们要返回它的值。
我们再来看一个例子:
fn plue_one(x: i32) -> i32 {
x + 1
}
fn main() {
let x = plue_one(5);
println!("The value of x:{x}");
}
运行这段代码将打印 The value of x is: 6 。但如果我们在包含 x + 1 的行尾加上分号,将其从表达式改为语句,就会出现错误:
cargo.exe run
Compiling another_function v0.1.0 (D:\rustProj\another_function)
error[E0308]: mismatched types
--> src\main.rs:1:24
|
1 | fn plue_one(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
2 | x + 1;
| - help: remove this semicolon to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `another_function` (bin "another_function") due to previous error
主要错误信息 mismatched types 揭示了这段代码的核心问题。函数 plus_one 的定义说它将返回一个 i32 ,但语句并没有求值,而值是由单元类型 () 表示的。因此,不会返回任何值,这与函数定义相矛盾,并导致错误。在该输出中,Rust 提供了一条可能有助于纠正该问题的信息:它建议删除分号,这样就可以修复该错误。
所有程序员都努力使自己的代码易于理解,但有时也需要额外的解释。在这种情况下,程序员会在源代码中留下注释,编译器会忽略这些注释,但阅读源代码的人可能会觉得有用。
这里有一个简单的评论:
// hello world!
在 Rust 中,惯用的注释样式是以两个斜线开始注释,注释一直持续到行尾。对于超出一行的注释,需要在每一行都包含 // ,就像这样:
// 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.
注释也可以放在包含代码的行尾:
fn main() {
let lucky_number = 8; // I’m feeling lucky today
}
但你更经常看到的是以这种格式使用的注释,即在注释代码的上方另起一行:
fn main() {
// I’m feeling lucky today
let lucky_number = 8;
}
Rust 还有另一种注释,即文档注释,在后面的章节中再介绍。
根据条件是否 true 运行某些代码,以及在条件 true 时重复运行某些代码的能力,是大多数编程语言的基本构件。让您控制 Rust 代码执行流的最常见结构是 if 表达式和循环。
if 表达式允许您根据条件分支代码。您可以提供一个条件,然后声明:"如果满足此条件,则运行此代码块。如果不满足条件,则不运行此代码块"。
在项目目录下新建一个名为branch的项目,以探索 if 表达式:
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
所有 if 表达式都以关键字 if 开头,后面跟一个条件。在本例中,条件检查变量 number 的值是否小于 5。我们将条件为 true 时要执行的代码块紧接在大括号内的条件之后。与 if 表达式中的条件相关的代码块有时被称为 "arms",就像我们在第 2 章 "将猜测与秘密数字进行比较 "一节中讨论的 match 表达式中的 "arms "一样。
我们还可以选择加入 else 表达式(我们在这里选择了这样做),以便在条件评估结果为 false 时,为程序提供另一个要执行的代码块。如果不提供 else 表达式,而条件是 false ,程序将跳过 if 代码块,继续执行下一段代码。
请尝试运行这段代码,您将看到以下输出:
cargo.exe run
Compiling branch v0.1.0 (D:\rustProj\branch)
Finished dev [unoptimized + debuginfo] target(s) in 0.69s
Running `target\debug\branch.exe`
condition was true
让我们试着将 number 的值改为能使条件 false 的值,看看会发生什么:
let number = 7;
再次运行程序,查看输出结果:
cargo.exe run
Compiling branch v0.1.0 (D:\rustProj\branch)
Finished dev [unoptimized + debuginfo] target(s) in 0.60s
Running `target\debug\branch.exe`
condition was false
值得注意的是,这段代码中的条件必须是 bool 。如果条件不是 bool ,我们就会出错。例如,请尝试运行以下代码:
fn main() {
let number = 3;
if number {
println!("condition was true");
} else {
println!("condition was false");
}
}
if 条件这次的求值结果是 3 ,Rust 会抛出一个错误:
cargo.exe build
Compiling branch v0.1.0 (D:\rustProj\branch)
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 `branch` (bin "branch") due to previous error
该错误表明 Rust 期望得到一个 bool ,但得到的却是一个整数。与 Ruby 和 JavaScript 等语言不同,Rust 不会自动尝试将非布尔类型转换为布尔类型。您必须明确地将布尔类型作为条件提供给 if 。例如,如果我们希望 if 代码块只在一个数字不等于 0 时运行,我们可以将 if 表达式改为如下:
fn main() {
let number = 3;
if number != 0 {
println!("condition was true");
} else {
println!("condition was false");
}
}
运行这段代码将打印:condition was true
通过在 else if 表达式中组合 if 和 else ,可以使用多个条件。例如:
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.exe run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\control_flow.exe`
number is divisible by 3
执行该程序时,它会依次检查每个 if 表达式,并执行条件求值为 true 的第一个正文。请注意,尽管 6 可以被 2 整除,但我们并没有看到输出 number is divisible by 2 ,也没有看到 else 代码块中的 number is not divisible by 4, 3, or 2 文本。这是因为 Rust 只执行了第一个 true 条件的代码块,一旦找到一个,就不会再检查其余的了。
使用过多的 else if 表达式会使代码变得杂乱无章,因此如果使用的表达式超过一个,可能需要重构代码。后面会介绍一种强大的 Rust 分支结构,称为 match ,用于处理这些情况。
因为 if 是一个表达式,所以我们可以在 let 语句的右侧使用它,将结果赋值给一个变量,如下所示:
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The vaule of number is:{number}");
}
number 变量将根据 if 表达式的结果绑定到一个值上。运行这段代码看看会发生什么:
cargo.exe run
Compiling control_flow v0.1.0 (E:\rustProj\control_flow)
Finished dev [unoptimized + debuginfo] target(s) in 0.32s
Running `target\debug\control_flow.exe`
The vaule of number is:5
请记住,代码块对其中的最后一个表达式进行求值,而数字本身也是表达式。在这种情况下,整个 if 表达式的值取决于执行哪个代码块。这意味着有可能成为 if 各分支结果的值必须是相同的类型;在上述代码中, if 分支和 else 分支的结果都是 i32 整数。如果类型不匹配,如下面的示例,我们就会出错:
fn main() {
let condition = true;
let number = if condition { 5 } else { '6' };
println!("The vaule of number is:{number}");
}
当我们尝试编译这段代码时,会出现错误。 if 和 else 两条手臂的值类型不兼容,而 Rust 则准确地指出了程序中的问题所在:
cargo.exe run
Compiling control_flow v0.1.0 (E:\rustProj\control_flow)
error[E0308]: `if` and `else` have incompatible types
--> src\main.rs:3:44
|
3 | let number = if condition { 5 } else { '6' };
| - ^^^ expected integer, found `char`
| |
| expected because of this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `control_flow` (bin "control_flow") due to previous error
if 代码块中的表达式求值为整数,而 else 代码块中的表达式求值为字符串。这样做是行不通的,因为变量必须具有单一类型,而 Rust 需要在编译时明确知道 number 变量的类型。知道了 number 的类型,编译器就可以在使用 number 的地方验证类型是否有效。如果 number 的类型只能在运行时确定,那么 Rust 就无法做到这一点;如果编译器必须跟踪任何变量的多种假设类型,那么编译器就会变得更加复杂,对代码的保证也会减少。
执行一个代码块不止一次通常很有用。为此,Rust 提供了多个循环,它们会将循环体中的代码执行到底,然后立即从头开始。为了尝试使用循环,让我们创建一个名为 loops 的新项目。
Rust 有三种循环: loop , while , 和 for 。
loop 关键字会告诉 Rust 永远重复执行一个代码块,直到你明确告诉它停止为止。
举例来说,将 loops 目录中的 src/main.rs 文件修改为如下所示:
fn main() {
loop {
println!("Hello, world!");
}
}
当我们运行这个程序时,会看到 Hello, world!不断重复打印,直到我们手动停止程序。大多数终端支持键盘快捷键 ctrl-c,用于中断陷入持续循环的程序。
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 的变量,用于保存循环返回的值。在循环的每次迭代中,我们将 1 添加到 counter 变量中,然后检查 counter 是否等于 10 。如果等于,我们就使用 break 关键字,并将其值改为 counter * 2 。循环结束后,我们使用分号结束赋值给 result 的语句。最后,我们打印 result 中的值,本例中的值为 20 。
如果在循环中存在循环, break 和 continue 适用于此时的最内层循环。您可以选择在循环上指定一个循环标签,然后与 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.exe run
Compiling loops v0.1.0 (E:\rustProj\loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target\debug\loops.exe`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
程序经常需要在循环中评估一个条件。当条件为 true 时,循环运行。当条件不再是 true 时,程序会调用 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 时,代码将运行;否则,将退出循环。
您可以选择使用 while 结构对数组等集合的元素进行循环。例如,下列代码中的循环打印数组 a 中的每个元素。
fn main() {
let a = [10, 20, 30, 40, 50];
let mut i = 0;
while i < 5 {
println!("the value is:{}", a[i]);
i += 1;
}
}
在这里,代码对数组中的元素进行计数。它从索引 0 开始,然后循环直到数组中的最终索引(即 index < 5 不再是 true 时)。运行这段代码将打印数组中的每个元素:
cargo.exe run
Compiling loops v0.1.0 (E:\rustProj\loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Running `target\debug\loops.exe`
the value is:10
the value is:20
the value is:30
the value is:40
the value is:50
所有五个数组值都如期出现在终端中。尽管 index 会在某一时刻达到 5 的值,但在尝试从数组中获取第六个值之前,循环就停止执行了。
如果我们将while循环中的i +=1修改为i++,则在编译时会报错:
cargo.exe build
Compiling loops v0.1.0 (E:\rustProj\loops)
error: Rust has no postfix increment operator
--> src\main.rs:7:10
|
7 | i++;
| ^^ not a valid postfix operator
|
help: use `+= 1` instead
|
7 | i += 1;
| ~~~~error: could not compile `loops` (bin "loops") due to previous error
===> 也就是说:Rust语言不支持类似C/C++那样的++i/i++这样的操作;
不过,这种方法容易出错;如果索引值或测试条件不正确,我们可能会导致程序宕机。例如,如果将 a 数组的定义改为包含四个元素,但忘记将条件更新为 while index < 4 ,代码就会宕机。此外,这样做的速度也很慢,因为编译器会添加运行时代码,在循环的每次迭代中执行索引是否在数组范围内的条件检查。
作为一种更简洁的替代方法,您可以使用 for 循环,为集合中的每个项目执行一些代码。 for 循环与上述的代码相似。
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is:{element}");
}
}
运行这段代码后,我们将看到与while循环有相同的输出结果。更重要的是,我们现在提高了代码的安全性,消除了因超出数组末尾或遗漏某些项所可能导致的错误。
使用 for 循环,如果改变数组中的数值个数,就不需要像while循环那样,还需要同步修改其他代码。
for 循环的安全性和简洁性使其成为 Rust 中最常用的循环结构。即使在需要运行一定次数代码的情况下,使用 while 循环的示例,大多数 Rustaceans 也会使用 for 循环。这样做的方法是:使用标准库提供的 Range ,它可以按顺序生成从一个数字开始到另一个数字之前结束的所有数字。
下面是使用 for 循环和另一种我们尚未讨论过的方法 rev 来反转范围的倒计时效果:
fn main() {
for number in (1..4).rev() {
println!("{number}");
}
println!("LIFTOFF!!!");
}
这个代码更好看一些,不是吗?
下一篇:04-了解所有权