# 数据类型
Rust 中的每个值都属于特定的数据类型,这告诉 Rust 指定了哪种数据,以便它知道如何处理这些数据。我们将研究两种数据类型子集:标量和复合。
请记住,Rust 是一种静态类型语言,这意味着它必须在编译时知道所有变量的类型。编译器通常可以根据值及其使用方式推断出我们想要使用的类型。在可能存在多种类型的情况下,例如当我们在第 2 章的“将猜测数字与秘密数字进行比较”部分中使用 parse
将字符串转换为数字类型时,我们必须添加类型注释,如下所示:
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 有字符和字符串两种类型,字符串将在后面介绍)。你可能在其他编程语言中见过这些类型。让我们来看看它们在 Rust 中的工作原理。
# 整数类型
整数是没有小数部分的数字。我们在第 2 章中使用了一种整数类型,即 u32 类型。此类型声明表示它所关联的值应为无符号整数(有符号整数类型以 i 而不是 u 开头),占用 32 位空间。表 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 |
arch(架构) | isize | usize |
每个变量可以是有符号的,也可以是无符号的,并且具有明确的大小。有符号和无符号指的是数字是否可能为负数,换句话说,数字是否需要带有符号(有符号)或者它是否永远都是正数,因此可以不用符号表示(无符号)。这就像在纸上写数字:当符号很重要时,数字会显示加号或减号;但是,当可以安全地假设数字为正数时,它不会显示符号。有符号数字使用二进制补码表示法 (opens new window)存储。
每个有符号变量可以存储从 -(2n - 1) 到 2n - 1 - 1(含)的数字,其中 n 是变量使用的位数。因此,i8 可以存储从 -(27) 到 27 - 1(等于 -128 到 127)的数字。无符号变量可以存储从 0 到 2n - 1 的数字,因此 u8 可以存储从 0 到 28 - 1(等于 0 到 255)的数字。
此外,isize 和 usize 类型取决于程序所运行的计算机的体系结构,在表 3-1 中表示为“arch(可以理解是架构的意思)一行”:如果使用 64 位体系结构,则为 64 位;如果使用 32 位体系结构,则为 32 位。
你可以采用表 3-2 中所示的任何一种形式来编写整数文字。请注意,可以为多种数字类型的数字文字允许使用类型后缀(例如57u8
)来指定类型。数字文字还可以使用 _
作为视觉分隔符,使数字更易于阅读,例如 1_000
,其值与你指定 1000 时的值相同。
表 3-2: rust 中的整数文字
数值文字 | 示例 |
---|---|
Decimal | 98_222 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_0000 |
Byte (u8 only) | b'A' |
那么你如何知道要使用哪种类型的整数?如果你不确定,Rust 的默认值通常是不错的起点:整数类型默认为 i32。你使用 isize 或 usize 的主要情况是索引某种集合时。
# 整数溢出
假设你有一个 u8 类型的变量,它可以保存 0 到 255 之间的值。如果你尝试将变量更改为该范围之外的值(例如 256),则会发生整数溢出,这可能导致两种行为之一。在调试模式下编译时,Rust 会检查整数溢出,如果发生此行为,则会导致程序在运行时崩溃。Rust 在程序因错误退出时使用术语“崩溃”;我们将在第 9 章的“无法恢复的崩溃错误!”部分更深入地讨论崩溃。
在使用
--release
标志在发布模式下编译时,Rust 不会检查导致崩溃的整数溢出。相反,如果发生溢出,Rust 会执行二进制补码包装。简而言之,大于类型可以保存的最大值的值“包装”到类型可以保存的最小值。对于 u8,值 256 变为 0,值 257 变为 1,依此类推。程序不会崩溃,但变量的值可能不是你预期的值。依赖整数溢出的包装行为被视为错误。要明确处理溢出的可能性,你可以使用标准库为原始数字类型提供的这些方法系列:
- 使用
wrapping_*
方法(例如wrapping_add
)在所有模式下包装。- 如果使用
checked_*
方法发生溢出,则返回 None 值。- 返回
overflowing_*
方法的值和一个布尔值,指示是否发生溢出。- 使用
saturating_*
方法在值的最小值或最大值处饱和。
# 浮点类型
Rust 还具有两种浮点数基本类型,即带有小数点的数字。Rust 的浮点类型是 f32 和 f64,大小分别为 32 位和 64 位。默认类型为 f64,因为在现代 CPU 上,它的速度与 f32 大致相同,但精度更高。所有浮点类型都是有符号的。
下面是一个展示浮点数实际作用的示例:
文件名: main.rs:
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
浮点数按照 IEEE-754 标准表示。f32 类型为单精度浮点数,f64 为双精度浮点数。
# 数值运算
Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、除法和余数。整数除法会将数字截断为最接近的整数。以下代码显示了如何在 let 语句中使用每个数字运算:
文件名: main.rs:
fn main() {
// 加法
let sum = 5 + 10;
// 减法
let difference = 95.5 - 4.3;
// 乘法
let product = 4 * 30;
// 除法
let quotient = 56.7 / 32.2;
let truncated = -5 / 3; // 结果是-1
// 余数
let remainder = 43 % 5;
}
这些语句中的每个表达式都使用数学运算符并计算出一个值,然后将其绑定到变量。附录 B 包含 Rust 提供的所有运算符的列表。
# 布尔类型
与大多数其他编程语言一样,Rust 中的布尔类型有两个可能的值:true 和 false。布尔值的大小为一个字节。Rust 中的布尔类型使用 bool 指定。例如:
文件名: main.rs:
fn main() {
let t = true;
let f: bool = false; // 显式类型注释
}
使用布尔值的主要方式是通过条件,例如 if 表达式。我们将在“控制流”部分介绍 if 表达式在 Rust 中的工作原理。
# 字符类型
Rust 的 char 类型是该语言最原始的字符类型。以下是声明 char 值的一些示例:
文件名: main.rs:
fn main() {
let c = 'z';
let z: char = 'ℤ'; // 显式类型注释
let heart_eyed_cat = '😻';
}
请注意,我们用单引号指定 char 文字,而不是使用双引号的字符串文字。Rust 的 char 类型大小为四个字节,表示 Unicode 标量值,这意味着它可以表示的不仅仅是 ASCII。重音字母;中文、日文和韩文字符;表情符号;以及零宽度空格都是 Rust 中的有效 char 值。Unicode 标量值的范围从 U+0000 到 U+D7FF 和 U+E000 到 U+10FFFF (含)。但是,“字符”在 Unicode 中并不是一个真正的概念,因此你对“字符”的人类直觉可能与 Rust 中的 char 不匹配。我们将在第 8 章“使用字符串存储 UTF-8 编码文本”中详细讨论这个主题。
# 复合类型
复合类型可以将多个值分组为一种类型。Rust 有两种原始复合类型:元组和数组。
# 元组类型
元组是一种将多个具有各种类型的值组合成一种复合类型的通用方法。元组的长度是固定的:一旦声明,它们的大小就不能增加或缩小。
我们通过在括号内写入逗号分隔的值列表来创建元组。元组中的每个位置都有一个类型,并且元组中不同值的类型不必相同。我们在此示例中添加了可选的类型注释:
文件名: main.rs:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
变量 tup 绑定到整个元组,因为元组被视为单个复合元素。要从元组中获取单个值,我们可以使用模式匹配来解构元组值,如下所示:
文件名: 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。
我们还可以使用句点 (.) 后跟要访问的值的索引来直接访问元组元素。例如:
文件名: 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。
没有任何值的元组有一个特殊名称,即 unit
。此值及其对应类型都写为 ()
,表示空值或空返回类型。如果表达式不返回任何其他值,则隐式返回 unit
值。
# 数组类型
另一种拥有多个值集合的方法是使用数组。与元组不同,数组的每个元素都必须具有相同的类型。与其他一些语言中的数组不同,Rust 中的数组具有固定长度。
我们将数组中的值写为方括号内的逗号分隔列表:
文件名: main.rs:
fn main() {
let a = [1, 2, 3, 4, 5];
}
当你希望将数据分配在堆栈而不是堆上(我们将在第 4 章中进一步讨论堆栈和堆)或者希望确保始终具有固定数量的元素时,数组非常有用。不过,数组并不像向量类型那样灵活。向量是标准库提供的类似集合类型,其大小可以增大或缩小。如果你不确定是使用数组还是向量,那么你很可能应该使用向量。第 8 章将更详细地讨论向量。
但是,当你知道元素数量不需要改变时,数组更有用。例如,如果你在程序中使用月份名称,你可能会使用数组而不是向量,因为你知道它总是包含 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 表示数组包含五个元素。
你还可以通过指定初始值、后跟分号以及方括号中的数组长度来初始化数组,使其每个元素包含相同的值,如下所示:
let a = [3; 5];
名为 a 的数组将包含 5 个元素,这些元素的初始值均为 3。这与 let a = [3, 3, 3, 3, 3];
的写法相同,但更简洁。
# 访问数组元素
数组是一块已知且固定大小的内存,可在堆栈上分配。你可以使用索引来访问数组的元素,如下所示:
文件名: 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 章中的猜谜游戏)以从用户那里获取数组索引:
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 将崩溃。此检查必须在运行时进行,尤其是在这种情况下,因为编译器不可能知道用户稍后运行代码时将输入什么值。
这是 Rust 内存安全原则的一个实际示例。在许多低级语言中,不会进行这种检查,当你提供不正确的索引时,可能会访问无效内存。Rust 通过立即退出而不是允许内存访问并继续来保护你免受此类错误的影响。第 9 章讨论了更多 Rust 的错误处理以及如何编写既不会崩溃也不允许无效内存访问的可读、安全的代码。