常见概念
变量与可变性
变量
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
当我们写下这段代码并运行,会发现有如下错误:
我们使用let
关键字来声明rust中的变量,而这类变量默认是不可变的,当我们需要拥有一个可变变量时,就可以通过mut
来实现变量的可变性,如下:
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
运行成功,打印了x变化前后的两个值。
常量
我们使用const
关键字来声明常量,通常常量是用全大写+下划线分隔:
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
常量与不可变变量很相似,不过常量与变量还是有一些区别。
- 首先,不允许对常量使用
mut
。常量不光默认不可变,它总是不可变。声明常量使用const
关键字而不是let
,并且 必须 注明值的类型。在下一部分,“数据类型” 中会介绍类型和类型注解,现在无需关心这些细节,记住总是标注类型即可。 - 常量可以在任何作用域中声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。
- 最后一个区别是,常量只能被设置为常量表达式,而不可以是其他任何只能在运行时计算出的值。
隐藏
当我们定义一个已经存在的变量的 同名变量 时,编译器会以后来的变量为准,而第一个变量就不再会被(编译器)看到,这样的性质叫 隐藏(Shadowing) 。先前的变量相当于被后面的同名变量给隐藏了,此时不管怎么样使用这个变量名,都视为对第二个变量操作,直到第二个变量也被隐藏或者超出了其作用域。
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}");
}
结果:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [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,我们可以用这个值进行一些计算,不过计算完之后变量仍然是不可变的。
mut 与隐藏的另一个区别是,当再次使用 let 时,实际上创建了一个新变量,我们可以改变值的类型,并且复用这个名字。例如,假设程序请求用户输入空格字符来说明希望在文本之间显示多少个空格,接下来我们想将输入存储成数字(多少个空格):
fn main() {
let spaces = " ";
let spaces = spaces.len();
}
第一个 spaces 变量是字符串类型,第二个 spaces 变量是数字类型。隐藏使我们不必使用不同的名字,如 spaces_str 和 spaces_num;相反,我们可以复用 spaces 这个更简单的名字。然而,如果尝试使用 mut,将会得到一个编译时错误,如下所示:
fn main() {
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` due to previous error
所以我们想要使用“隐藏”这一特性时,就要避免使用mut
,以免出现更改变量类型的差错。
数据类型
Rust 是 静态类型(statically typed)语言,也就是说在编译时就必须知道所有变量的类型。
标量类型
标量(scalar)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过它们。让我们深入了解它们在 Rust 中是如何工作的。
整型
长度 | 有符号 | 无符号 |
---|---|---|
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
可以储存从 0
到 2^8 - 1
的数字,也就是从0
到 255
。
另外,isize
和 usize
类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的,32 位架构上它们是 32 位的。
浮点型
Rust 的浮点数类型是 f32 和 f64,分别占 32 位和 64 位。默认类型是 f64,因为在现代 CPU 中,它与 f32 速度几乎一样,不过精度更高。所有的浮点型都是有符号的。
文件名:src/main.rs
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
浮点数采用 IEEE-754 标准表示。f32 是单精度浮点数,f64 是双精度浮点数。
布尔型
正如其他大部分编程语言一样,Rust 中的布尔类型有两个可能的值:true 和 false。Rust 中的布尔类型使用 bool 表示。例如:
文件名:src/main.rs
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
字符类型
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
}
注意,我们用单引号声明 char 字面量,而与之相反的是,使用双引号声明字符串字面量。Rust 的 char 类型的大小为四个字节 (four bytes),并代表了一个 Unicode 标量值(Unicode Scalar Value),这意味着它可以比 ASCII 表示更多内容。
在 Rust 中,带变音符号的字母(Accented letters),中文、日文、韩文等字符,emoji(绘文字)以及零长度的空白字符都是有效的 char 值。Unicode 标量值包含从 U+0000 到 U+D7FF 和 U+E000 到 U+10FFFF 在内的值。不过,“字符” 并不是一个 Unicode 中的概念,所以人直觉上的 “字符” 可能与 Rust 中的 char 并不符合。第八章的 “使用字符串存储 UTF-8 编码的文本” 中将详细讨论这个主题。
复合类型
复合类型(Compound types)可以将多个值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。
元组
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}
The value of y is: 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;
}
不带任何值的元组有个特殊的名称,叫做 单元(unit) 元组。这种值以及对应的类型都写作 (),表示空值或空的返回类型。如果表达式不返回任何其他值,则会隐式返回单元值。
数组
另一个包含多个值的方式是 数组(array)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,Rust 中的数组长度是固定的。
fn main() {
let a = [1, 2, 3, 4, 5];
}
当你想要在栈(stack)而不是在堆(heap)上为数据分配空间(第四章将讨论栈与堆的更多内容),或者是想要确保总是有固定数量的元素时,数组非常有用。但是数组并不如 vector 类型灵活。vector 类型是标准库提供的一个 允许 增长和缩小长度的类似数组的集合类型。
然而,当你确定元素个数不会改变时,数组会更有用。例如,当你在一个程序中使用月份名字时,你更应趋向于使用数组而不是 vector,因为你确定只会有 12 个元素。
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July","August", "September", "October", "November", "December"];
}
可以像这样编写数组的类型:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量:
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}
这里,i32 是每个元素的类型。分号之后,数字 5 表明该数组包含五个元素。
你还可以通过在方括号中指定初始值加分号再加元素个数的方式来创建一个每个元素都为相同值的数组:
fn main() {
let a = [3; 5];
}
数组是可以在栈 (stack) 上分配的已知固定大小的单个内存块。可以使用索引来访问数组的元素,像这样:
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
文件名: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}");
}
当然,rust的数组也是不允许越界的。
当输入5之后:
函数
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
我们在 Rust 中通过输入 fn 后面跟着函数名和一对圆括号来定义函数。大括号告诉编译器哪里是函数体的开始和结尾。
可以使用函数名后跟圆括号来调用我们定义过的任意函数。因为程序中已定义 another_function 函数,所以可以在 main 函数中调用它。注意,源码中 another_function 定义在 main 函数 之后;也可以定义在之前。Rust 不关心函数定义所在的位置,只要函数被调用时出现在调用之处可见的作用域内就行。
参数
在这版 another_function 中,我们增加了一个参数:
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 [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
The value of x is: 5
在函数签名中,必须 声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解,意味着编译器再也不需要你在代码的其他地方注明类型来指出你的意图。而且,在知道函数需要什么类型后,编译器就能够给出更有用的错误消息。
当定义多个参数时,使用逗号分隔,像这样:
fn main() {
print_labeled_measurement(5, 'h');
}
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}
语句和表达式
函数体由一系列的语句和一个可选的结尾表达式构成。目前为止,我们提到的函数还不包含结尾表达式,不过你已经见过作为语句一部分的表达式。因为 Rust 是一门基于表达式(expression-based)的语言,这是一个需要理解的(不同于其他语言)重要区别。其他语言并没有这样的区别,所以让我们看看语句与表达式有什么区别以及这些区别是如何影响函数体的。
语句(Statements)是执行一些操作但不返回值的指令。 表达式(Expressions)计算并产生一个值。让我们看一些例子。
实际上,我们已经使用过语句和表达式。使用 let 关键字创建变量并绑定一个值是一个语句。在列表 3-1 中,let y = 6; 是一个语句。
fn main() {
let y = 6;
}
函数定义也是语句,上面整个例子本身就是一个语句。
语句不返回值。因此,不能把 let 语句赋值给另一个变量,比如下面的例子尝试做的,会产生一个错误:
fn main() {
let x = (let y = 6);
}
let y = 6 语句并不返回值,所以没有可以绑定到 x 上的值。这与其他语言不同,例如 C 和 Ruby,它们的赋值语句会返回所赋的值。在这些语言中,可以这么写 x = y = 6,这样 x 和 y 的值都是 6;Rust 中不能这样写。
表达式会计算出一个值,并且你将编写的大部分 Rust 代码是由表达式组成的。考虑一个数学运算,比如 5 + 6,这是一个表达式并计算出值 11。表达式可以是语句的一部分:在示例 3-1 中,语句 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 关键字和指定值,可从函数中提前返回;但大部分函数隐式的返回最后的表达式。这是一个有返回值的函数的例子:
文件名:src/main.rs
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");
}
在 five 函数中没有函数调用、宏、甚至没有 let 语句 —— 只有数字 5。这在 Rust 中是一个完全有效的函数。注意,也指定了函数返回值的类型,就是 -> i32。
让我们看看另一个例子:
文件名: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 的行尾加上一个分号,把它从表达式变成语句,我们将看到一个错误。
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
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` 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 number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
所有的 if 表达式都以 if 关键字开头,其后跟一个条件。在这个例子中,条件检查变量 number 的值是否小于 5。在条件为 true 时希望执行的代码块位于紧跟条件之后的大括号中。if 表达式中与条件关联的代码块有时被叫做 arms
也可以包含一个可选的 else 表达式来提供一个在条件为 false 时应当执行的代码块,这里我们就这么做了。如果不提供 else 表达式并且条件为 false 时,程序会直接忽略 if 代码块并继续执行下面的代码。
另外值得注意的是代码中的条件 必须 是 bool 值。如果条件不是 bool 值,我们将得到一个错误。例如,尝试运行以下代码:
文件名:src/main.rs
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
这里 if 条件的值是 3,Rust 抛出了一个错误:
$ 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` due to previous error
这个错误表明 Rust 期望一个 bool 却得到了一个整数。不像 Ruby 或 JavaScript 这样的语言,Rust 并不会尝试自动地将非布尔值转换为布尔值。必须总是显式地使用布尔值作为 if 的条件。例如,如果想要 if 代码块只在一个数字不等于 0 时执行,可以把 if 表达式修改成下面这样:
fn main() {
let number = 3;
if number != 0 {
println!("number was something other than zero");
}
}
使用 else if 处理多重条件
可以将 else if 表达式与 if 和 else 组合来实现多重条件。例如:
文件名: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");
}
}
和C/C++类似,当执行这个程序时,它按顺序检查每个 if 表达式并执行第一个条件为 true 的代码块。
在 let 语句中使用 if
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}
number 变量将会绑定到表示 if 表达式结果的值上。运行这段代码看看会出现什么:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/branches`
The value of number is: 5
记住,代码块的值是其最后一个表达式的值,而数字本身就是一个表达式。在这个例子中,整个 if 表达式的值取决于哪个代码块被执行。这意味着 if 的每个分支的可能的返回值都必须是相同类型;在示例 3-2 中,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 也准确地指出在程序中的何处发现的这个问题:
$ 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` due to previous error
if 代码块中的表达式返回一个整数,而 else 代码块中的表达式返回一个字符串。这不可行,因为变量必须只有一个类型。Rust 需要在编译时就确切的知道 number 变量的类型,这样它就可以在编译时验证在每处使用的 number 变量的类型是有效的。
如果number的类型仅在运行时确定,则 Rust 无法做到这一点;且编译器必须跟踪每一个变量的多种假设类型,那么它就会变得更加复杂,对代码的保证也会减少。
使用循环重复执行
Rust 有三种循环:loop、while 和 for。我们每一个都试试。
fn main() {
loop {
println!("again!");
}
}
当运行这个程序时,我们会看到连续的反复打印 again!,直到我们手动停止程序。大部分终端都支持一个快捷键,ctrl-c,来终止一个陷入无限循环的程序。尝试一下:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
符号 ^C 代表你在这按下了ctrl-c。在 ^C 之后你可能看到也可能看不到 again! ,这取决于在接收到终止信号时代码执行到了循环的何处。
幸运的是,Rust 提供了一种从代码中跳出循环的方法。可以使用 break 关键字来告诉程序何时停止循环。回忆一下在第二章猜猜看游戏的 “猜测正确后退出” 部分使用过它来在用户猜对数字赢得游戏后退出程序。
我们在猜谜游戏中也使用了 continue。循环中的 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 来存放循环的返回值。在循环的每一次迭代中,我们将 counter 变量加 1,接着检查计数是否等于 10。当相等时,使用 break 关键字返回值 counter * 2。循环之后,我们通过分号结束赋值给 result 的语句。最后打印出 result 的值,也就是 20。
循环标签:在多个循环之间消除歧义
如果存在嵌套循环,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 [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 条件循环
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
使用 for 遍历集合
可以使用 for 循环来对一个集合的每个元素执行一些代码:
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
for 循环的安全性和简洁性使得它成为 Rust 中使用最多的循环结构。即使是在想要循环执行代码特定次数时,例如示例 3-3 中使用 while 循环的倒计时例子,大部分 Rustacean 也会使用 for 循环。这么做的方式是使用 Range,它是标准库提供的类型,用来生成从一个数字开始到另一个数字之前结束的所有数字的序列。
下面是一个使用 for 循环来倒计时的例子,它还使用了一个我们还未讲到的方法,rev,用来反转 range:
文件名:src/main.rs
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}