前言
本章介绍了几乎所有编程语言中出现的概念以及它们在 Rust 中的工作方式。许多编程语言的核心有很多共同点。本章中介绍的概念都不是 Rust 独有的,但我们将在 Rust 的背景中讨论它们,并解释使用这些概念的约定。
具体来说,您将了解变量、基本类型、函数、注释和控制流。这些基础将出现在每个 Rust 程序中,尽早学习它们将为您提供一个强大的核心。关于Rust命名规范,大家可访问 rust rfcs 查看。
ust 语言有一组关键字,这些关键字仅供该语言使用,就像在其他语言中一样。请记住,您不能将这些词用作变量或函数的名称。大多数关键字都有特殊的含义,您将使用它们来执行 Rust 程序中的各种任务;有些当前没有与之相关的功能,但已保留用于将来可能添加到 Rust 中的功能。您可以在附录 A 中找到关键字列表。
内容
接下来我们将一起学习具体的内容,主要有以下模块:
- 变量和可变性
- 数据类型
- 函数
- 注释
- 控制流
变量和可变性
默认情况下变量是不可变的(immutable)。这是 Rust 众多精妙之处的其中一个,这些特性让您充分利用 Rust 提供的安全性和简单并发性的方式来编写代码。不过您也可以选择让变量是可变的(mutable)。让我们探讨一下 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 run
Compiling variables v0.1.0 (/Users/wangyang/Documents/project/rust-learn/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 1 previous error
此示例演示编译器如何帮助您查找程序中的错误。编译器错误可能令人沮丧,但实际上它们只意味着您的程序尚未安全地执行您希望它执行的操作;这并不意味着您不是一个好的程序员!有经验的 Rustaceans 仍然会遇到编译器错误。
您收到错误消息 cannot assign twice to immutable variable
x` 是因为您尝试将第二个值分配给不可
x` 变变量。
当我们尝试更改指定为不可变的值时,遇到编译时错误非常重要,因为这种情况可能会导致错误。如果代码的一部分基于一个值永远不会改变的假设来操作,而代码的另一部分更改了该值,那么代码的第一部分可能无法执行其设计要执行的操作。这种错误的原因在事后可能很难追踪,尤其是当第二段代码只是偶尔更改值时。Rust 编译器保证,当您声明一个值不会改变时,它真的不会改变,所以您不必自己跟踪它。因此,您的代码更容易推理。
但是可变性可能非常有用,并且可以使代码编写更方便。尽管变量在默认情况下是不可变的,但您可以通过在变量名称前面添加 mut
它们来使它们可变。添加 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 (/Users/wangyang/Documents/project/rust-learn/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
加上 mut
后,我们就可以将 x
绑定的值从 5
改成 6
。归根结底,决定是否使用可变性取决于您,并取决于您知道自己在做什么。
常数
与不可变变量一样,常量是绑定到变量且不允许更改的值,但常量和变量之间存在一些差异。
首先,不允许与常量一起使用 mut
。默认情况下,常量不仅是不可变的,而且始终是不可变的。使用 const
关键字而不是 let
关键字声明常量,并且必须对值的类型进行批注。我们将在下一节“数据类型”中介绍类型和类型注释,因此现在不用担心细节。只要知道您必须始终对类型进行批注。
常量可以在任何作用域(包括全局作用域)中声明,这使得它们对于代码的许多部分需要了解的值很有用。
最后一个区别是,常量只能设置为常量表达式,而不能设置为只能在运行时计算的值的结果。
下面是常量声明的示例:
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
常量的名称是 THREE_HOURS_IN_SECONDS
,其值设置为将 60(一分钟中的秒数)乘以 60(一小时中的分钟数)乘以 3(我们要在此程序中计算的小时数)的结果。Rust 对常量的命名约定是使用全部大写字母,单词之间带有下划线。编译器能够在编译时评估一组有限的操作,这使我们能够选择以更易于理解和验证的方式写出此值,而不是将此常量设置为值 10,800。请参阅 Rust 参考中关于常量计算)的部分,以获取有关声明常量时可以使用哪些操作的更多信息。
常量在程序运行的整个程序内有效,在声明常量的范围内。此属性使常量对于应用程序域中的值非常有用,程序的多个部分可能需要了解这些值,例如允许游戏的任何玩家获得的最大点数或光速。
将整个程序中使用的硬编码值命名为常量,有助于将该值的含义传达给代码的未来维护者。如果将来需要更新硬编码值,则代码中只有一个位置需要更改,这也有所帮助。
遮蔽
我们可以通过使用相同的变量名并重复使用 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}");
}
此程序首先绑 x
定到值 5
。然后 x
它通过重复 let x =
创建一个新变量,取原始值并相加 1
,因此 的 x
值为 6
。然后,在用大括号创建的内部作用域内,第三个 let
语句也会遮蔽 x
并创建一个新变量,将前一个值乘以 2
得到 x
值 12
。当该范围结束时,内部遮蔽结束并 x
恢复为存在 6
。当我们运行这个程序时,它将输出以下内容:
$ cargo run
Compiling variables v0.1.0 (/Users/wangyang/Documents/project/rust-learn/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
遮蔽和 mut
变量的使用是不同的,因为除非我们再次使用 let
关键字,否则若是我们不小心尝试重新赋值给这个变量,我们将得到一个编译错误。通过使用 let
,我们可以对一个值进行一些转换,但在这些转换完成后,变量将是不可变的。
mut
和遮蔽 另一个区别是,由于当我们再次使用 let
关键字时,我们实际上是在创建一个新变量,因此我们可以更改值的类型,并重复使用相同的名称, 由此可得mut
性能要更好,因为mut
声明的变量,修改的是同一个内存地址上的值,并不会发生内存对象的再分配。
例如,假设我们的程序要求用户通过输入空格字符来显示他们想要在一些文本之间有多少个空格,然后我们希望将该输入存储为一个数字:
fn main() {
let spaces = " ";
let spaces = spaces.len();
println!("spaces: {spaces}")
}
第一个 spaces
变量是字符串类型,第二个 spaces
变量是数字类型。因此,变量遮蔽使我们不必想出不同的名称,例如 spaces_str
和 spaces_num
;
如果我们尝试使用 mut
此操作,又会是什么样呢?
fn main() {
let mut spaces = " ";
spaces = spaces.len();
println!("spaces: {spaces}")
}
结果如下,我们得到了一个编译期的错误,该错误表明我们不允许更改变量的类型:
$cargo run
Compiling variables v0.1.0 (/Users/wangyang/Documents/project/rust-learn/variables)
error[E0308]: mismatched types
--> src/main.rs:39:14
|
38 | let mut spaces = " ";
| ---- expected due to this value
39 | 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
现在我们已经探讨了变量是如何工作的,接下来我们学习更多的数据类型。
数据类型
Rust 中的每个值都具有特定的数据类型,它告诉 Rust 指定了哪种数据,以便它知道如何处理这些数据。我们将研究两个数据类型子集:标量和复合。
请记住,Rust 是一种静态类型语言,这意味着它必须在编译时知道所有变量的类型。编译器通常可以根据值和使用方式推断我们想要使用的类型。在可能有许多类型的情况下,例如当我们在“猜秘密数字”部分中使用 parse
将 String
转换为数字类型时,我们必须添加一个类型注释,如下所示:
let guess: u32 = "42".parse().expect("Not a number!");
如果我们不添加前面代码中显示的 : u32
类型注解,Rust 将显示以下错误,这意味着编译器需要我们提供更多信息才能知道我们想要使用哪种类型:
$ cargo run
Compiling variables v0.1.0 (/Users/wangyang/Documents/project/rust-learn/variables)
error[E0284]: type annotations needed
--> src/main.rs:46:9
|
46 | 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
|
46 | 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 1 previous error
标量类型
标量类型表示单个值。Rust 有四种主要标量类型:整数、浮点数、布尔值和字符。您可以从其他编程语言中识别出这些。让我们来看看它们在 Rust 中是如何工作的。
整型
整数是没有小数分量的数字。我们在猜谜游戏中使用了一种整数类型,即 u32
type。此类型声明指示与其关联的值应为占用 32 位空间的无符号整数(有符号整数类型以 i
而不是 u
开头)。下表为 Rust 中的内置整数类型。我们可以使用这些变体中的任何一个来声明整数值的类型。
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 |
u8 |
16-bit | i16 |
u16 |
32-bit | i32 |
u32 |
64-bit | i64 |
u64 |
128-bit | i128 |
u128 |
arch | isize |
usize |
个定义形式要么是有符号类型要么是无符号类型,且带有一个显式的大小。有符号和无符号表示数字能否取负数——也就是说,这个数是否可能是负数(有符号类型),或一直为正而不需要带上符号(无符号类型)。就像在纸上写数字一样:当要强调符号时,数字前面可以带上正号或负号;然而,当很明显确定数字为正数时,就不需要加上正号了。有符号的数字使用二进制补码表示进行存储。
每个有符号类型规定的数字范围是
$$ -(2^{n-1}) ~ 2^{n-1}-1(含) $$
其中 n
是该定义形式的位长度。所以 i8
可存储数字范围是
$$ -(2^7) ~ 2^7-1 $$
,即 -128 ~ 127。无符号类型可以存储的数字范围是
$$ 0 ~ 2^n - 1 $$
,所以 u8
能够存储的数字为
$$ 2^8-1 $$
,即 0 ~ 255。
此外, isize
和 usize
类型取决于运行程序的计算机的体系结构,在表中表示为“arch”:如果采用 64 位体系结构,则为 64 位,如果采用 32 位体系结构,则为 32 位。
可按下表所示的任意形式来编写整型的字面量。注意,可能属于多种数字类型的数字字面量允许使用类型后缀来指定类型,例如 57u8
。数字字面量还可以使用 _
作为可视分隔符以方便读数,如 1_000
和 1000
是相同的。
数字字面量 | 示例 |
---|---|
十进制 | 98_222 |
十六进制 | 0xff |
八进制 | 0o77 |
二进制 | 0b1111_0000 |
字节 (仅限于 u8 ) |
b'A' |
那么该使用哪种类型的整型呢?如果不确定,Rust 的默认形式通常是个不错的选择,整型默认是 i32
。isize
和 usize
的主要应用场景是用作某些集合的索引。
整型溢出
比方说有一个
u8
,它可以存放从 0 到 255 的值。那么当您将其修改为范围之外的值,比如 256,则会发生整型溢出(integer overflow),这会导致两种行为的其中一种。当在调试(debug)模式编译时,Rust 会检查整型溢出,若存在这些问题则使程序在编译时 panic。Rust 使用 panic 这个术语来表明程序因错误而退出。在当使用
--release
参数进行 release 模式构建时,Rust 不检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出(two’s complement wrapping)的规则处理。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在u8
的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是您期望的值。依赖这种默认行为的代码都应该被认为是错误的代码。要显式处理可能的溢出,可以使用标准库针对原始数字类型提供的这些方法:
- 使用
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
println!("x: {x}, y: {y}")
}
浮点数根据 IEEE-754 标准表示。该 f32
类型是单精度浮点数,并 f64
具有双精度,但是切记不要对浮点数进行比较,因为存在精度缺失的问题,比如0.1+0.2 == 0.3
$ cargo run
Compiling variables v0.1.0 (/Users/wangyang/Documents/project/rust-learn/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.67s
Running `target/debug/variables`
x: 2, y: 3
数值运算
Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、除法和余数。整数除法会向下取整。下面的代码演示如何在 let
语句中使用每个数值运算:
fn main() {
let sum = 5 + 10;
let difference = 95.5 - 4.3;
let product = 4 * 30;
let quotient = 56.7 / 32.2;
let floored = 2 / 3;
let remainder = 43 % 5;
println!("sum: {sum}, difference: {difference}, product: {product}, quotient: {quotient}, floored: {floored}, remainder: {remainder}")
}
这些语句中的每个表达式都使用了数学运算符,并且计算结果为一个值,然后绑定到一个变量上,现在来运行一下:
$ cargo run
Compiling variables v0.1.0 (/Users/wangyang/Documents/project/rust-learn/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/variables`
sum: 15, difference: 91.2, product: 120, quotient: 1.7608695652173911, floored: 0, remainder: 3
布尔类型
与大多数其他编程语言一样,Rust 中的布尔类型有两个可能的值: true
和 false
。布尔值大小为一个字节。Rust 中的布尔类型是使用 bool
指定。例如:
fn main() {
let t = true;
let f: bool = false; // 显式类型标注
println!("t: {t}, f: {f}")
}
使用布尔值的主要方法是通过条件,例如 if
表达式。我们将在 “控制流” 部分介绍表达式在 Rust 中的工作原理 if
。
字符类型
Rust char
的类型是该语言最原始的字母类型。下面是声明 char
值的一些示例:
fn main() {
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻';
println!("c: {c}, z: {z}, heart_eyed_cat: {heart_eyed_cat}")
}
注意,我们声明的 char
字面量采用单引号括起来,这与字符串字面量不同,字符串字面量是用双引号括起来。Rust 的字符类型大小为 4 个字节,表示的是一个 Unicode 标量值,这意味着它可以表示的远远不止是 ASCII。标音字母,中文/日文/韩文的文字,emoji,还有零宽空格(zero width space)在 Rust 中都是合法的字符类型。Unicode 值的范围为 U+0000
~ U+D7FF
和 U+E000
~U+10FFFF
。不过“字符”并不是 Unicode 中的一个概念,所以人在直觉上对“字符”的理解和 Rust 的字符概念并不一致。
复合类型
复合类型可以将多个值组合成一个类型。Rust 有两种基本的复合类型:元组(tuple)和数组(array)。
元组类型
元组是将多种类型的多个值组合到一个复合类型中的一种基本方式。元组的长度是固定的:声明后,它们就无法增长或缩小。
我们通过在小括号内写入以逗号分隔的值列表来创建一个元组。元组中的每个位置都有一个类型,并且元组中不同值的类型不要求是相同的。我们在下面示例中添加了可选的类型标注:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
变量 tup
绑定到整个元组,因为元组被认作是单个复合元素。 想从元组中获取个别值,我们可以使用模式匹配来解构(destructure)元组的一个值,如下所示:
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("x:{x}, y: {y}, z: {z}");
}
该程序首先创建一个元组并将其绑定到变量 tup
上。 然后它借助 let
来使用一个模式匹配 tup
,并将它分解成三个单独的变量 x
、y
和 z
。 这过程称为解构(destructuring),因为它将单个元组分为三部分。
现在让我们一起来运行下:
$ cargo run
Compiling variables v0.1.0 (/Users/wangyang/Documents/project/rust-learn/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/variables`
x:500, y: 6.4, z: 1
除了通过模式匹配进行解构外,我们还可以使用一个句点(.
)连上要访问的值的索引来直接访问元组元素。例如:
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;
println!("five_hundred: {five_hundred}, six_point_four: {six_point_four}, one: {one}")
}
运行结果如下:
$ cargo run
Compiling variables v0.1.0 (/Users/wangyang/Documents/project/rust-learn/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/variables`
five_hundred: 500, six_point_four: 6.4, one: 1
该程序创建元组 x
,然后使用它们各自的索引访问元组的每个元素。与大多数编程语言一样,元组中的第一个索引是 0。
没有任何值的元组 ()
是一种特殊的类型,只有一个值,也写成 ()
。该类型被称为单元类型(unit type),该值被称为单元值(unit value)。如果表达式不返回任何其他值,就隐式地返回单元值。
数组类型
拥有多个值集合的另一种方法是使用数组。与元组不同,数组的每个元素都必须具有相同的类型。与其他一些语言中的数组不同,Rust 中的数组具有固定的长度。
fn main() {
let a = [1, 2, 3, 4, 5];
println!("a:{:?}", a);
}
运行如下:
$ cargo run
Compiling variables v0.1.0 (/Users/wangyang/Documents/project/rust-learn/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s
Running `target/debug/variables`
a:[1, 2, 3, 4, 5]
当您希望将数据分配到栈(stack)而不是堆(heap)时,或者当您希望确保始终具有固定数量的元素时,数组非常有用。但是,数组并不像向量(vector)类型那样灵活。向量是标准库提供的类似集合类型,允许增大或缩小大小。如果您不确定是使用数组还是向量,则很可能应该使用向量。
但是,当您知道不需要更改元素的数量时,数组会更有用。例如,如果您在程序中使用月份的名称,您可能会使用数组而不是向量,因为您知道它总是包含 12 个元素:
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
您可以使用方括号编写数组的类型,其中包含每个元素的类型、分号,然后是数组中的元素数,如下所示:
fn main() {
let a:[i32;6] = [1, 2, 3, 4, 5, 6];
println!("a:{:?}", a);
}
这里,i32
是每个元素的类型。分号之后,数字 5
表明该数组包含 5 个元素。
您还可以通过指定初始值,后跟分号,然后在方括号中指定数组的长度,将数组初始化为包含每个元素的相同值,如下所示:
fn main() {
let a = [3; 5];
println!("a:{:?}", a);
}
命名 a
的数组将包含 5
最初将全部设置为该值 3
的元素。这与写作 let a = [3, 3, 3, 3, 3];
相同,但更简洁。
访问数组元素
数组是可以在堆栈上分配的已知固定大小的单个内存块。您可以使用索引访问数组的元素,如下所示:
fn main() {
let a:[i32;6] = [1, 2, 3, 4, 5, 6];
let first = a[0];
let second = a[1];
println!("a:{:?}", a);
println!("first: {first}, second: {second}")
}
在此示例中,命名 first
的变量将获取该值 1
,因为它是数组中 index [0]
处的值。命名 second
的变量将从数组中的 index [1]
中获取值 2
。
无效的数组元素访问
让我们看看,如果尝试访问数组末尾的数组元素,会发生什么。假设您运行以下代码,类似于第 2 章中的猜谜游戏,从用户那里获取数组索引:
use std::io;
// 无效的数组访问
fn main() {
let a = [1,2,3,4,5,6];
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}");
}
此代码编译成功。如果使用 using cargo run
并输入 0
、 1
、 2
、 或 3
4
运行此代码,则程序将在数组中该索引处打印出相应的值。如果改为输入数组末尾的数字,例如 10
,则会看到如下所示的输出:
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/variables`
Please enter an array index:
10
thread 'main' panicked at src/main.rs:121:19:
index out of bounds: the len is 6 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
在索引操作中使用无效值时,程序会导致运行时错误。程序退出并显示错误消息,并且未执行最终 println!
语句。当您尝试使用索引访问元素时,Rust 将检查您指定的索引是否小于数组长度。如果索引大于或等于长度,Rust 将崩溃。此检查必须在运行时进行,尤其是在这种情况中,因为编译器不可能知道用户稍后运行代码时将输入什么值。
这是 Rust 内存安全原则的一个例子。在许多低级语言中,这种检查是不做的,当您提供不正确的索引时,可以访问无效的内存。Rust 通过立即退出而不是允许内存访问并继续来保护您免受此类错误的影响。
函数
函数在 Rust 代码中很普遍。您已经看到了该语言中最重要的函数之一: main
函数,它是许多程序的入口点。您还看到了 fn
关键字,它允许您声明新函数。
Rust 代码中的函数和变量名使用下划线命名法(snake case,直译为蛇形命名法)规范风格。在下划线命名法中,所有字母都是小写并使用下划线分隔单词。这是一个包含函数定义示例的程序:
fn main() {
println!("Hello, world!");
author_function();
}
fn author_function() {
println!("author function")
}
我们在 Rust 中定义一个函数,方法是输入 fn
后跟一个函数名称和一组括号。大括号告诉编译器函数体的开始和结束位置。
我们可以通过输入其名称后跟一组括号来调用我们定义的任何函数。因为它是 another_function
在程序中定义的,所以可以从 main
函数内部调用它。请注意,我们在源代码 another_function
中定义了 main
函数之后;我们之前也可以定义它。Rust 并不关心您在哪里定义函数,只关心它们在调用者可以看到的作用域中的某个位置定义。
main
函数中的代码会按顺序执行。首先,打印 “Hello, world!” 信息,然后调用 another_function
函数并打印它的信息。
$ cargo run
Compiling function v0.1.0 (/Users/wangyang/Documents/project/rust-learn/function)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s
Running `target/debug/function`
Hello, world!
author function
参数
函数也可以被定义为拥有参数(parameter),参数是特殊变量,是函数签名的一部分。当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)。技术上讲,这些具体值被称为实参(argument),但是在日常交流中,人们倾向于不区分使用 parameter 和 argument 来表示函数定义中的变量或调用函数时传入的具体值。
在这个版本的 another_function
中,我们添加了一个参数:
fn main() {
author_function("Tim");
}
fn author_function(name: &str) {
println!("author function: {}", name)
}
运行结果如下:
$ cargo run
Compiling function v0.1.0 (/Users/wangyang/Documents/project/rust-learn/function)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/function`
author function: Tim
在函数签名中,必须声明每个参数的类型。这是一个 Rust 设计中经过慎重考虑的决定:要求在函数定义中提供类型标注,意味着编译器几乎从不需要您在代码的其他地方注明类型来指出您的意图。
当一个函数有多个参数时,使用逗号分隔,像这样:
fn main() {
author_function("Tim", 10);
}
fn author_function(name: &str, age: i32) {
println!("author function name: {}, age:{}", name, age)
}
运行结果如下:
$ cargo run
Compiling function v0.1.0 (/Users/wangyang/Documents/project/rust-learn/function)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/function`
author function name: Tim, age:10
语句和表达式
函数体由一系列语句组成,也可选择以表达式结尾。目前为止,我们介绍的函数还没有包含结尾表达式,不过您已经看到了表达式作为语句的一部分。因为 Rust 是一门基于表达式(expression-based)的语言,所以这是一个需要理解的重要区别。其他语言没有这样的区别,所以让我们看看语句和表达式分别是什么,以及它们的区别如何影响函数体。
语句(Statements)是执行一些操作但不返回值的指令。
表达式(Expressions)计算并产生一个值。
让我们看一些例子:
实际上,我们已经使用过语句和表达式。使用 let
关键字创建变量并绑定一个值是一个语句。
fn main() {
let y = 6;
}
函数定义也是语句,上面的整个示例本身就是一个语句。语句不返回值。因此,不能把 let
语句赋值给另一个变量,就像下面的代码尝试做的那样,会产生一个错误:
fn main() {
let x = (let y = 6);
}
错误如下:
$ cargo run
Compiling function v0.1.0 (/Users/wangyang/Documents/project/rust-learn/function)
error: expected expression, found `let` statement
--> src/main.rs:15:14
|
15 | 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:15:13
|
15 | let x = (let y = 6);
| ^ ^
|
= note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
|
15 - let x = (let y = 6);
15 + let x = let y = 6;
|
warning: `function` (bin "function") generated 1 warning
error: could not compile `function` (bin "function") due to 1 previous error; 1 warning emitted
let y = 6
语句不返回值,因此没有任何要 x
绑定的内容。这与其他语言(如 C 和 Ruby)中发生的情况不同,在 C 和 Ruby 中,赋值返回赋值。在这些语言中,您可以同时编写 x = y = 6
并拥有两者 x
并 y
具有值 6
;在 Rust 中并非如此。
表达式会计算出一个值,并且您接下来要用 Rust 编写的大部分代码都由表达式组成。考虑一个数学运算,比如 5 + 6
,这是一个表达式并计算出值 11
。表达式可以是语句的一部分:在示例中,语句 let y = 6;
中的 6
是一个表达式,它计算出的值是 6
。函数调用是一个表达式。宏调用是一个表达式。我们用来创建新作用域的大括号(代码块) {}
也是一个表达式,例如:
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
行的末尾没有分号,这与您目前见过的大部分代码行不同。表达式的结尾没有分号。如果在表达式的末尾加上分号,那么它就转换为语句,而语句不会返回值。在接下来探讨函数返回值和表达式时,请记住这一点。
带有返回值的函数
函数可以向调用它的代码返回值。我们并不对返回值命名,但要在箭头(->
)后声明它的类型。在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。使用 return
关键字和指定值,可以从函数中提前返回;但大部分函数隐式返回最后一个表达式。这是一个有返回值函数的例子:
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {}", x);
}
在 five
函数中没有函数调用、宏,甚至没有 let
语句——只有数字 5
本身。这在 Rust 中是一个完全有效的函数。注意,函数返回值的类型也被指定好,即 -> i32
。尝试运行代码;输出应如下所示:
$ cargo run
Compiling function v0.1.0 (/Users/wangyang/Documents/project/rust-learn/function)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/function`
The value of x is: 5
five
函数的返回值是 5
,所以返回值类型是 i32
。让我们仔细检查一下这段代码。有两个重要的部分:首先,let x = five();
这一行表明我们使用函数的返回值初始化一个变量。因为 five
函数返回 5
,这一行与如下代码相同:
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
}
运行如下:
$ cargo run
Compiling function v0.1.0 (/Users/wangyang/Documents/project/rust-learn/function)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/function`
The value of x is: 6
运行代码会打印出 The value of x is: 6
。但如果在包含 x + 1
的行尾加上一个分号,把它从表达式变成语句,我们将得到一个错误。
fn main() {
let x = plus_one(5);
println!("The value of x is: {}", x);
}
fn plus_one(x: i32) -> i32 {
x + 1;
}
运行代码会产生一个错误,如下:
Compiling function v0.1.0 (/Users/wangyang/Documents/project/rust-learn/function)
error[E0308]: mismatched types
--> src/main.rs:43:24
|
43 | fn plus_one(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
44 | x + 1;
| - help: remove this semicolon to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `function` (bin "function") due to 1 previous error
主要错误消息 mismatched types
揭示了此代码的核心问题。该函数 plus_one
的定义说它将返回一个 i32
,但语句的计算结果不为一个值,该值由 ()
单位类型 表示。因此,不返回任何内容,这与函数定义相矛盾并导致错误。在此输出中,Rust 提供了一条可能有助于纠正此问题的消息:它建议删除分号,这将修复错误。
注释
所有程序员都努力使他们的代码易于理解,但有时需要额外的解释。在这些情况下,程序员在他们的源代码中留下注释,编译器会忽略这些注释,但阅读源代码的人可能会发现这些注释很有用。
这里有一个简单的注释:
// 这是一个注释
在 Rust 中,惯用的注释样式以两个斜杠开始注释,注释一直持续到行尾。对于超出单行的注释,您需要在每一行中包含 //
注释,如下所示:
// 注释一
// 注释二
注释也可以放在代码行的末尾:
fn main() {
let lucky_number = 7; // I’m feeling lucky today
}
但您更经常看到它们以这种格式使用,注释在它所注释的代码上方的单独一行上:
fn main() {
// I’m feeling lucky today
let lucky_number = 7;
}
Rust 还有另一种注释,即文档注释,我们将在后续的章节去学习。
控制流
在大多数编程语言中,能够根据条件是否为 true
条件来运行某些代码,以及在条件为 true
条件时重复运行某些代码的能力。让您控制 Rust 代码执行流程的最常见结构是 if
表达式和循环。
if 表达式
if
表达式允许您根据条件对代码进行分支。您提供一个条件,然后声明:“如果满足此条件,请运行此代码块。如果不满足条件,请不要运行此代码块。
现在让我们一起来创建一个名为 branches 的新项目来一起学习:
fn main() {
let number = 6;
if number > 5 {
println!("Number is greater than 5");
} else {
println!("Number is less than or equal to 5");
}
}
所有的 if
表达式都以 if
关键字开头,其后跟一个条件。在这个例子中,条件检查变量 number
的值是否大于 5。在条件为真时希望执行的代码块位于紧跟条件之后的大括号中。if
表达式中与条件关联的代码块有时被叫做分支(arm)。
或者,我们还可以包含一个 else
表达式,我们选择在这里这样做,以便为程序提供一个替代代码块,以便在条件计算结果为 false
时执行。如果您不提供 else
表达式,并且条件为 false
,则程序将跳过该 if
块并继续下一段代码。
现在让我们尝试运行这段代码:
$ cargo run
Compiling branches v0.1.0 (/Users/wangyang/Documents/project/rust-learn/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.38s
Running `target/debug/branches`
Number is greater than 5
让我们尝试将 的 number
值更改为一个值,该值使条件 false
成为条件,看看会发生什么:
let number = 4
现在让我再次运行代码:
$ cargon run
Compiling branches v0.1.0 (/Users/wangyang/Documents/project/rust-learn/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/branches`
Number is less than or equal to 5
还值得注意的是,此代码中的条件必须是 bool
.如果条件不是 bool
,我们将收到错误。例如,尝试运行以下代码:
fn main() {
let number = 4;
if number {
println!("Number is greater than 5")
}
}
很明显,我们收到了一个报错,该错误表明 Rust 需要 bool
,但得到一个整数。与 Ruby 和 JavaScript 等语言不同,Rust 不会自动尝试将非布尔类型转换为布尔类型。您必须显式,并始终提供 if
布尔值作为其条件。
$ cargo run
Compiling branches v0.1.0 (/Users/wangyang/Documents/project/rust-learn/branches)
error[E0308]: mismatched types
--> src/main.rs:9:8
|
9 | 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
例如,如果我们希望 if
代码块仅在数字不等于 0
时运行,我们可以将 if
表达式更改为以下内容:
fn main() {
let number = 4;
if number != 0 {
println!("number was something other than zero");
}
}
运行如下:
$ cargo run
Compiling branches v0.1.0 (/Users/wangyang/Documents/project/rust-learn/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.51s
Running `target/debug/branches`
number was something other than zero
else if 表达式
您可以通过在 else if
表达式中组合 if
和 else
来使用多个条件。例如:
fn main() {
let number = 4;
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 (/Users/wangyang/Documents/project/rust-learn/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/branches`
number is divisible by 4
当执行这个程序时,它按顺序检查每个 if
表达式并执行第一个条件为真的代码块。Rust 只会执行第一个条件为真的代码块,并且一旦它找到一个以后,甚至都不会检查剩下的条件了。
使用过多 else if
的表达式会使代码混乱,因此,如果有多个表达式,则可能需要重构代码。针对这些情况,您可以选择更强大 Rust 分支结构 match
。
在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
表达式结果的值上。运行这段代码看看会出现什么:
/Users/wangyang/.cargo/bin/cargo run --color=always --package branches --bin branches
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/branches`
The value of number is: 5
Process finished with exit code 0
记住,代码块的值是其最后一个表达式的值,而数字本身就是一个表达式。在这个例子中,整个 if
表达式的值取决于哪个代码块被执行。这意味着 if
的每个分支的可能的返回值都必须是相同类型;if
分支和 else
分支的结果都是 i32
整型。如果它们的类型不匹配,如下面这个例子,则会产生一个错误:
当然了编辑器其实也明显的提示了我们错误所在
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {}", number);
}
当我们尝试编译此代码时,我们会得到一个错误。 if
和 else
分支具有不兼容的值类型,Rust 准确地指出了在程序中查找问题的位置:
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:44:44
|
44 | let number = if condition { 5 } else { "six" };
| - ^^^^^ expected integer, found `&str`
| |
| expected because of this
f
代码块中的表达式返回一个整数,而 else
代码块中的表达式返回一个字符串。这不可行,因为变量必须只有一个类型。Rust 需要在编译时就确切地知道 number
变量的类型,这样它就可以在编译时验证在每处使用的 number
变量的类型是有效的。若 number
的类型仅在运行时确定,则 Rust 将无法做到这一点;而且若编译器必须跟踪任意变量的多种假设类型,则编译器会变得更复杂,并且对代码的保证也会减少。
loop表达式
多次执行代码块通常很有用。对于此任务,Rust 提供了多种循环,这些循环将贯穿循环体中的代码到末尾,然后立即从头开始。为了试验循环,让我们做一个名为 loops 的新项目。
Rust 有三种循环: loop
、 while
和 for
。让我们尝试每一个。
loop
该 loop
关键字告诉 Rust 一遍又一遍地执行代码块,或者直到您明确告诉它停止。
fn main() {
loop {
println!("Hello, world!");
}
}
当我们运行这个程序时,我们会看到 again!
一遍又一遍地打印,直到我们手动停止程序。大多数终端都支持键盘快捷键 ctrl-c 来中断卡在连续循环中的程序
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
He^C
Process finished with exit code 130 (interrupted by signal 2:SIGINT)
该符号 ^C
表示您按下 ctrl-c 的位置。您可能会也可能不会看到 ^C
前面打印的单词 He
,具体取决于代码在接收中断信号时在循环中的位置。
幸运的是,Rust 还提供了一种使用代码打破循环的方法。您可以将 break
关键字放在循环中,以告诉程序何时停止执行循环。
循环中的 continue
关键字告诉程序跳过这个循环迭代中的任何剩余代码,并转到下一个迭代。
这两个在我们前面的猜谜游戏中也有体现哦~
循环中返回值
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 等于 10 。 counter 如果满足条件,我们使用 break 返回值 counter * 2,在循环之后,我们使用分号结束将值赋给 result 的语句。最后,我们打印 result 值:
/Users/wangyang/.cargo/bin/cargo run --color=always --package loops --bin loops
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/loops`
The result is 20
Process finished with exit code 0
您也可以从循环内部 return。虽然 break 只退出当前循环,但 return 始终退出当前函数。
循环标签
在 Rust 中,循环可以包含标签,以允许对循环进行更精细的控制。标签用于标识循环,然后可以在循环中引用标签。
如果循环中有循环, 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; 语句将退出外循环。
/Users/wangyang/.cargo/bin/cargo run --color=always --package loops --bin loops
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
Process finished with exit code 0
while 表达式
程序通常需要评估循环中的条件。当条件为 true 时,循环将运行。当条件不再是 true 时,程序将调用 break ,停止循环。可以使用 loop 、 if 、 else 和 break 的组合来实现这样的行为;如果您愿意,您现在可以在一个程序中尝试一下。然而,这种模式非常普遍,以至于 Rust 有一个内置的语言结构,称为 while 循环。
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
运行如下:
/Users/wangyang/.cargo/bin/cargo run --color=always --package loops --bin loops
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/loops`
3!
2!
1!
LIFTOFF!!!
Process finished with exit code 0
这种构造消除了大量嵌套,如果使用 loop 、 if 、 else 和 break ,则需要这些嵌套,并且更清晰。当条件的计算结果为 true 时,代码将运行;否则,它将退出循环。
for 表达式
for 循环在 Rust 中非常常见,因为它们允许您遍历集合中的每个元素。对比使用while 循环,for 循环更简洁且避免了数组越界的问题。
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {}", element);
}
}
for 循环的安全性和简洁性使它们成为 Rust 中最常用的循环结构。即使在您想要运行某些代码一定次数的情况下, 大多数 Rustaceans 也会使用循环 for 。这样做的方法是使用标准库提供的 Range ,它按顺序生成所有数字,从一个数字开始,到另一个数字结束。
以下是使用 for 循环和我们尚未讨论 rev 的另一种方法来反转范围的倒计时的样子:
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
运行如下:
/Users/wangyang/.cargo/bin/cargo run --color=always --package loops --bin loops
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/loops`
3!
2!
1!
LIFTOFF!!!
Process finished with exit code 0
总结
您成功了!这是一个相当大的章节:您了解了变量、标量和复合数据类型、函数、注释、 if 表达式和循环!若要练习本章中讨论的概念,请尝试构建程序以执行以下操作:
- 在华氏度和摄氏度之间转换温度。
- 生成第 n 个斐波那契数。
- 打印圣诞颂歌“The Twelve Days of Christmas”的歌词,利用歌曲中的重复。
当您准备好继续前进时,我们将讨论 Rust 中一个在其他编程语言中通常不存在的概念:所有权。