前言
本章介绍了几乎所有编程语言中出现的概念以及它们在 Rust 中的工作方式。许多编程语言的核心有很多共同点。本章中介绍的概念都不是 Rust 独有的,但我们将在 Rust 的背景中讨论它们,并解释使用这些概念的约定。
具体来说,您将了解变量、基本类型、函数、注释和控制流。这些基础将出现在每个 Rust 程序中,尽早学习它们将为您提供一个强大的核心。关于Rust命名规范,大家可访问 rust rfcs 查看。
ust 语言有一组关键字,这些关键字仅供该语言使用,就像在其他语言中一样。请记住,您不能将这些词用作变量或函数的名称。大多数关键字都有特殊的含义,您将使用它们来执行 Rust 程序中的各种任务;有些当前没有与之相关的功能,但已保留用于将来可能添加到 Rust 中的功能。您可以在附录 A 中找到关键字列表。
内容
接下来我们将一起学习具体的内容,主要有以下模块:
- 变量和可变性
- 数据类型
- 函数
- 注释
- 控制流
变量和可变性
默认情况下变量是不可变的(immutable)。这是 Rust 众多精妙之处的其中一个,这些特性让您充分利用 Rust 提供的安全性和简单并发性的方式来编写代码。不过您也可以选择让变量是可变的(mutable)。让我们探讨一下 Rust 如何及为什么鼓励您选用不可变性,以及为什么有时您可能不选用。
当变量不可变时,一旦值绑定到变量,就无法更改该值。为了说明这一点,请使用 cargo new variables
在项目目录中生成一个名为 variables 的新项目。
然后,在新的变量目录中,打开 src/main.rs 并将其代码替换为以下代码,该代码还不会编译:
代码语言:rust复制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 更改为以下内容:
代码语言:rust复制fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}")
}
当我们现在运行程序时,我们得到以下结果:
代码语言:shell复制$ 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
关键字声明常量,并且必须对值的类型进行批注。我们将在下一节“数据类型”中介绍类型和类型注释,因此现在不用担心细节。只要知道您必须始终对类型进行批注。
常量可以在任何作用域(包括全局作用域)中声明,这使得它们对于代码的许多部分需要了解的值很有用。
最后一个区别是,常量只能设置为常量表达式,而不能设置为只能在运行时计算的值的结果。
下面是常量声明的示例:
代码语言:rust复制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
声明的变量,修改的是同一个内存地址上的值,并不会发生内存对象的再分配。
例如,假设我们的程序要求用户通过输入空格字符来显示他们想要在一些文本之间有多少个空格,然后我们希望将该输入存储为一个数字:
代码语言:rust复制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}")
}
结果如下,我们得到了一个编译期的错误,该错误表明我们不允许更改变量的类型:
代码语言:shell复制$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 |
|
|
16-bit |
|
|
32-bit |
|
|
64-bit |
|
|
128-bit |
|
|
arch |
|
|
个定义形式要么是有符号类型要么是无符号类型,且带有一个显式的大小。有符号和无符号表示数字能否取负数——也就是说,这个数是否可能是负数(有符号类型),或一直为正而不需要带上符号(无符号类型)。就像在纸上写数字一样:当要强调符号时,数字前面可以带上正号或负号;然而,当很明显确定数字为正数时,就不需要加上正号了。有符号的数字使用二进制补码表示进行存储。
每个有符号类型规定的数字范围是 -(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
是相同的。
数字字面量 | 示例 |
---|---|
十进制 |
|
十六进制 |
|
八进制 |
|
二进制 |
|
字节 (仅限于 |
|
那么该使用哪种类型的整型呢?如果不确定,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}")
}
这些语句中的每个表达式都使用了数学运算符,并且计算结果为一个值,然后绑定到一个变量上,现在来运行一下:
代码语言:shell复制$ 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 = '