数据类型

Rust 中的每个值都属于某种 数据类型,它告诉 Rust 指定的是哪种数据,以便 Rust 知道如何处理该数据。我们将研究两种数据类型子集:标量和复合类型。

请记住,Rust 是一种 静态类型 语言,这意味着它必须在编译时知道所有变量的类型。编译器通常可以根据值以及我们如何使用它来推断我们想要使用的类型。在可能存在多种类型的情况下,例如当我们在第 2 章的 “将猜测与秘密数字进行比较” 部分中使用 parseString 转换为数字类型时,我们必须添加类型注解,像这样章节。

#![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 类型。此类型声明指示与其关联的值应为无符号整数(有符号整数类型以 i 而不是 u 开头),它占用 32 位空间。表 3-1 显示了 Rust 中的内置整数类型。我们可以使用这些变体中的任何一种来声明整数值的类型。

表 3-1:Rust 中的整数类型

长度有符号无符号
8 位i8u8
16 位i16u16
32 位i32u32
64 位i64u64
128 位i128u128
archisizeusize

每个变体可以是 有符号无符号 的,并且具有显式大小。有符号无符号 指的是数字是否可能为负数——换句话说,数字是否需要带符号(有符号),或者它是否将始终为正数,因此可以不带符号表示(无符号)。这就像在纸上写数字一样:当符号很重要时,数字会显示加号或减号;但是,当可以安全地假设数字为正数时,则不显示符号。有符号数使用 二进制补码 存储表示法。

每个有符号变体可以存储从 -(2n - 1) 到 2n - 1 - 1(包括端点)的数字,其中 n 是该变体使用的位数。因此,i8 可以存储从 -(27) 到 27 - 1 的数字,即 -128 到 127。无符号变体可以存储从 0 到 2n - 1 的数字,因此 u8 可以存储从 0 到 28 - 1 的数字,即 0 到 255。

此外,isizeusize 类型取决于运行程序的计算机的体系结构,这在表中表示为“arch”:如果您在 64 位体系结构上,则为 64 位,如果您在 32 位体系结构上,则为 32 位。

您可以使用表 3-2 中所示的任何形式编写整数文字。请注意,可以为多种数字类型的数字文字添加类型后缀,例如 57u8,以指定类型。数字文字也可以使用 _ 作为视觉分隔符,使数字更易于阅读,例如 1_000,它与您指定 1000 的值相同。

表 3-2:Rust 中的整数文字

数字文字示例
十进制98_222
十六进制0xff
八进制0o77
二进制0b1111_0000
字节 (仅 u8)b'A'

那么您如何知道要使用哪种整数类型呢?如果您不确定,Rust 的默认值通常是不错的起点:整数类型默认为 i32。您使用 isizeusize 的主要情况是在索引某种集合时。

整数溢出

假设您有一个 u8 类型的变量,它可以容纳介于 0 和 255 之间的值。如果您尝试将变量更改为该范围之外的值,例如 256,则会发生 整数溢出,这可能会导致两种行为之一。当您在调试模式下编译时,Rust 包含整数溢出检查,如果发生此行为,会导致您的程序在运行时 panic。当程序因错误退出时,Rust 使用术语 panicking;我们将在第 9 章的 “使用 panic! 处理不可恢复的错误”章节。

当您在发布模式下使用 --release 标志进行编译时,Rust 不会包含导致 panic 的整数溢出检查。相反,如果发生溢出,Rust 会执行 二进制补码环绕。简而言之,大于类型可以容纳的最大值的值“环绕”到类型可以容纳的最小值的最小值。在 u8 的情况下,值 256 变为 0,值 257 变为 1,依此类推。程序不会 panic,但变量的值可能不是您期望的值。依赖整数溢出的环绕行为被认为是错误的。

要显式处理溢出的可能性,您可以使用标准库为原始数字类型提供的这些方法系列

  • 在所有模式下使用 wrapping_* 方法(例如 wrapping_add)进行环绕。
  • 如果使用 checked_* 方法发生溢出,则返回 None 值。
  • 使用 overflowing_* 方法返回该值和一个指示是否发生溢出的布尔值。
  • 使用 saturating_* 方法在值的最小值或最大值处饱和。

浮点类型

Rust 还有两种用于 浮点数 的原始类型,它们是带小数点的数字。Rust 的浮点类型为 f32f64,它们的大小分别为 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 中的布尔类型有两个可能的值:truefalse。布尔值的大小为一个字节。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 类型的大小为四个字节,表示 Unicode 标量值,这意味着它可以表示比 ASCII 更多的内容。带重音的字母;中文、日文和韩文字符;表情符号;和零宽度空格都是 Rust 中有效的 char 值。Unicode 标量值的范围从 U+0000U+D7FFU+E000U+10FFFF(包括端点)。但是,“字符”在 Unicode 中并不是一个真正的概念,因此您对“字符”的人类直觉可能与 Rust 中 char 的含义不符。我们将在第 8 章的 “使用字符串存储 UTF-8 编码文本”在第 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 并将其转换为三个单独的变量 xyz。这称为 解构,因为它将单个元组分解为三个部分。最后,程序打印 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 章)或者当您想确保始终具有固定数量的元素时。但是,数组不如 vector 类型灵活。vector 是标准库提供的类似集合类型,它允许增长或缩小大小。如果您不确定是使用数组还是 vector,那么您应该使用 vector 的可能性很大。第 8 章更详细地讨论了 vector。

但是,当您知道元素的数量不需要更改时,数组更有用。例如,如果您在程序中使用月份名称,您可能会使用数组而不是 vector,因为您知道它将始终包含 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 运行此代码并输入 01234,程序将打印出数组中该索引处的相应值。如果您改为输入超出数组末尾的数字(例如 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 也不会允许无效内存访问的可读、安全的代码。