数据类型
Rust 中的每个值都有特定的数据类型,它会告诉 Rust 当前指定的是什么数据,以便 Rust 知道如何处理它。我们将介绍两类数据类型:标量类型和复合类型。
请记住,Rust 是一种静态类型语言,也就是说,它必须在编译时知道所有变量的类型。通常,编译器可以根据值及其用法推断我们想使用的类型。如果存在多种可能的类型,例如第 2 章“比较猜测与秘密数字”一节中使用 parse 将 String 转换为数字类型时,我们就必须添加类型标注,如下所示:
#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}
如果不添加前面代码中的 : u32 类型标注,Rust 会显示以下错误。这意味着编译器需要我们提供更多信息,才能知道要使用哪种类型:
$ cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
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 `no_type_annotations` (bin "no_type_annotations") due to 1 previous error
其他数据类型也会使用不同的类型标注。
标量类型
标量类型表示单个值。Rust 有四种主要的标量类型:整数、浮点数、布尔值和字符。你可能在其他编程语言中见过这些类型。让我们看看它们在 Rust 中的用法。
整数类型
整数是不带小数部分的数字。我们在第 2 章使用过一种整数类型:u32。这个类型声明表示其关联值是一个占用 32 位空间的无符号整数(有符号整数类型以 i 开头,而不是 u)。表 3-1 展示了 Rust 内置的整数类型。我们可以使用其中任何一种来声明整数值的类型。
表 3-1:Rust 中的整数类型
| 长度 | 有符号 | 无符号 |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| 依赖架构 | isize | usize |
每种整数类型都可以是有符号或无符号,并具有明确的位数。有符号和无符号指的是数字是否可能为负数;换句话说,数字是否需要带符号(有符号),还是始终为正数、因此可以不带符号表示(无符号)。这就像在纸上写数字:符号重要时,数字会带正号或负号;如果可以确定数字为正数,就可以不写符号。有符号数字使用二进制补码表示。
Each signed variant can store numbers from −(2n − 1) to 2n −
1 − 1 inclusive, where n is the number of bits that variant uses. So, an
i8 can store numbers from −(27) to 27 − 1, which equals
−128 to 127. Unsigned variants can store numbers from 0 to 2n − 1,
so a u8 can store numbers from 0 to 28 − 1, which equals 0 to 255.
此外,isize 和 usize 类型取决于程序运行所在计算机的架构:64 位架构上是 64 位,32 位架构上是 32 位。
整数可以写成表 3-2 中的任意形式。可以属于多种数值类型的数字字面量允许添加类型后缀(例如 57u8)来指定类型。数字字面量也可以使用 _ 作为视觉分隔符,让数字更易阅读;例如 1_000 与 1000 的值相同。
表 3-2:Rust 中的整数字面量
| 数字字面量 | 示例 |
|---|---|
| 十进制 | 98_222 |
| 十六进制 | 0xff |
| 八进制 | 0o77 |
| 二进制 | 0b1111_0000 |
字节(仅限 u8) | b'A' |
那么,如何判断应该使用哪种整数类型?如果不确定,通常可以从 Rust 的默认值开始:整数类型默认为 i32。主要在对集合进行索引时,才会使用 isize 或 usize。
整数溢出
假设你有一个 u8 类型的变量,它能保存 0 到 255 之间的值。如果试图将变量改为范围之外的值(例如 256),就会发生整数溢出,并出现两种行为之一。以调试模式编译时,Rust 会检查整数溢出;如果发生溢出,程序会在运行时触发 panic。Rust 使用触发 panic一词描述程序因错误退出;我们会在第 9 章“不可恢复的错误与 panic!”一节中更深入地讨论 panic。
使用 --release 标志以发布模式编译时,Rust 不会加入会因整数溢出而触发 panic 的检查。相反,发生溢出时,Rust 会执行二进制补码环绕。简单来说,超过类型最大值的数会“环绕”到该类型可表示的最小值。对于 u8,256 会变成 0,257 会变成 1,以此类推。程序不会触发 panic,但变量的值可能与你预期不同。依赖整数溢出的环绕行为通常被视为错误。
若要显式处理溢出的可能性,可以使用标准库为基本数值类型提供的以下几组方法:
- 使用
wrapping_*方法(例如wrapping_add),在所有编译模式下执行环绕运算。 - 使用
checked_*方法,在发生溢出时返回None。 - 使用
overflowing_*方法,返回数值以及一个表示是否发生溢出的布尔值。 - 使用
saturating_*方法,将结果限制在该类型可表示的最小值或最大值。
浮点类型
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 标准表示。
数值运算
Rust 支持所有数字类型常见的基本数学运算:加法、减法、乘法、除法和取余。整数除法会向零截断到最接近的整数。下面的代码展示了如何在 let 语句中使用各种数值运算:
文件名: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;
}
这些语句中的每个表达式都会使用一个数学运算符,并计算出单个值,然后将其绑定到变量。附录 B列出了 Rust 提供的所有运算符。
布尔类型
与大多数编程语言一样,Rust 的布尔类型有两个可能的值:true 和 false。布尔值占一个字节。Rust 使用 bool 表示布尔类型,例如:
文件名:src/main.rs
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
布尔值主要用于条件判断,例如 if 表达式。我们会在“控制流”一节介绍 if 表达式在 Rust 中的工作方式。
字符类型
Rust 的 char 类型是该语言最基本的字母类型。下面是声明 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 标量值,因此它可以表示远不止 ASCII 字符。重音字母、中日韩字符、表情符号和零宽空格,在 Rust 中都可以作为有效的 char 值。Unicode 标量值的范围包括 U+0000 到 U+D7FF,以及 U+E000 到 U+10FFFF。不过,在 Unicode 中,“字符”并不是一个严格的概念,因此你对“字符”的直觉可能与 Rust 中的 char 不完全相符。我们会在第 8 章“使用字符串存储 UTF-8 编码文本”一节中详细讨论这个主题。
复合类型
复合类型可以将多个值组合为一种类型。Rust 有两种基本复合类型:元组和数组。
元组类型
元组是一种通用方式,可以将多个不同类型的值组合成一个复合类型。元组的长度固定:声明后,大小不会增加或缩小。
我们可以将以逗号分隔的值列表写在圆括号中来创建元组。元组中的每个位置都有一种类型,而不同位置的类型不必相同。下面的示例中添加了可选的类型标注:
文件名:src/main.rs
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
变量 tup 绑定到整个元组,因为元组被视为单个复合元素。要取出元组中的各个值,可以使用模式匹配解构元组值,如下所示:
文件名:src/main.rs
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。
也可以使用句点(.)后跟索引,直接访问元组元素。例如:
文件名: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;
}
这个程序创建元组 x,然后使用各元素对应的索引访问它们。与大多数编程语言一样,元组的第一个索引是 0。
不包含任何值的元组有一个特殊名称:单元。这个值及其对应类型都写作 (),表示空值或空的返回类型。如果表达式没有返回其他值,就会隐式返回单元值。
数组类型
另一种存放多个值的集合是数组。与元组不同,数组中的每个元素必须具有相同类型。与某些语言中的数组不同,Rust 数组的长度固定。
我们可以将以逗号分隔的值列表写在方括号中来创建数组:
文件名:src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
}
如果你希望数据像目前看到的其他类型一样分配在栈上,而不是堆上(第 4 章会进一步介绍栈和堆),或者希望确保元素数量始终固定,那么数组就很有用。不过,数组没有向量类型灵活。向量是标准库提供的一种类似集合的类型,由于内容存放在堆上,大小可以增加或缩小。如果不确定该用数组还是向量,很可能应该选择向量。第 8 章会更详细地讨论向量。
不过,如果你知道元素数量不会改变,数组会更实用。例如,程序要使用月份名称时,你可能会选择数组而不是向量,因为你知道它始终包含 12 个元素:
#![allow(unused)]
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
}
数组类型写在方括号中,包括每个元素的类型、一个分号以及数组元素的数量,如下所示:
#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}
这里,i32 是每个元素的类型。分号后的数字 5 表示数组包含五个元素。
你也可以指定初始值、分号和数组长度,让数组中的每个元素都使用相同的值,如下所示:
#![allow(unused)]
fn main() {
let a = [3; 5];
}
名为 a 的数组会包含 5 个元素,初始值全部为 3。这与写出 let a = [3, 3, 3, 3, 3]; 相同,只是更加简洁。
访问数组元素
数组是一块大小已知且固定的连续内存,可以分配在栈上。你可以像下面这样通过索引访问数组元素:
文件名:src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
在这个例子中,名为 first 的变量会获得值 1,因为它是数组索引 [0] 处的值。名为 second 的变量会从数组索引 [1] 处获得值 2。
无效的数组元素访问
如果尝试访问超出数组末尾的元素,会发生什么?假设你运行下面这段代码,从用户那里获取数组索引;它与第 2 章的猜数字游戏类似:
文件名: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}");
}
这段代码可以成功编译。如果使用 cargo run 运行并输入 0、1、2、3 或 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 会检查指定的索引是否小于数组长度。如果索引大于或等于长度,Rust 就会触发 panic。这个检查必须在运行时进行,尤其是在本例中,因为编译器不可能提前知道用户稍后运行代码时会输入什么值。
这是 Rust 内存安全原则的一个实际例子。在许多底层语言中,不会执行此类检查;如果提供了错误的索引,就可能访问无效内存。Rust 会立即退出,而不是允许访问该内存并继续运行,从而保护你免受这类错误影响。第 9 章会进一步讨论 Rust 的错误处理,以及如何编写既易读又安全、既不触发 panic 也不允许访问无效内存的代码。