探索 Rust 数据类型

RustRustBeginner
立即练习

This tutorial is from open-source community. Access the source code

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

欢迎来到数据类型实验。本实验是《Rust 程序设计语言》的一部分。你可以在 LabEx 中练习 Rust 技能。

在本实验中,我们将探索 Rust 中的数据类型概念,即每个值都被赋予一种特定类型,以确定如何处理它;在可能有多种类型的情况下,必须添加类型标注,以便向编译器提供必要的信息。

数据类型

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

请记住,Rust 是一种静态类型语言,这意味着它必须在编译时就知道所有变量的类型。编译器通常可以根据值以及我们使用值的方式推断出我们想要使用的类型。在有多种可能类型的情况下,例如在“将猜测值与秘密数字进行比较”中使用 parseString 转换为数字类型时,我们必须添加类型标注,如下所示:

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[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^ consider giving `guess` a type

对于其他数据类型,你会看到不同的类型标注。

标量类型

标量类型表示单个值。Rust 有四种主要的标量类型:整数、浮点数、布尔值和字符。你可能从其他编程语言中了解过这些类型。让我们来看看它们在 Rust 中的工作方式。

整数类型

整数是没有小数部分的数字。在第 2 章中,我们使用了一种整数类型 u32。这种类型声明表示与之关联的值应该是一个无符号整数(有符号整数类型以 i 开头,而不是 u),它占用 32 位空间。表 3-1 展示了 Rust 中的内置整数类型。我们可以使用这些变体中的任何一个来声明整数值的类型。

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

长度 有符号 无符号
8 位 i8 u8
16 位 i16 u16
32 位 i32 u32
64 位 i64 u64
128 位 i128 u128
取决于架构 isize usize

每个变体既可以是有符号的,也可以是无符号的,并且具有明确的大小。“有符号”和“无符号”指的是数字是否可能为负数——换句话说,数字是否需要带有符号(有符号),或者它是否只会是正数,因此可以不带符号表示(无符号)。这就像在纸上写数字:当符号重要时,数字会带有加号或减号;然而,当可以安全地假设数字为正数时,它就不带符号显示。有符号数字使用补码表示法存储。

每个有符号变体可以存储从 -(2<sup>{=html}n - 1</sup>{=html}) 到 2<sup>{=html}n - 1</sup>{=html} - 1(包括两端)的数字,其中 n 是该变体使用的位数。所以 i8 可以存储从 -(2<sup>{=html}7</sup>{=html}) 到 2<sup>{=html}7</sup>{=html} - 1 的数字,即 -128 到 127。无符号变体可以存储从 0 到 2<sup>{=html}n</sup>{=html} - 1 的数字,所以 u8 可以存储从 0 到 2<sup>{=html}8</sup>{=html} - 1 的数字,即 0 到 255。

此外,isizeusize 类型取决于程序运行所在计算机的架构,在表中表示为“取决于架构”:如果是在 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 会包含对整数溢出的检查,如果发生这种行为,会导致程序在运行时恐慌。当程序因错误而退出时,Rust 使用术语“恐慌”;我们将在“使用 panic! 处理不可恢复的错误”中更深入地讨论恐慌。

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

为了显式处理溢出的可能性,你可以使用标准库为原始数字类型提供的这些方法家族:

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

浮点类型

Rust 还有两种用于表示浮点数的原始类型,即带有小数点的数字。Rust 的浮点类型是 f32f64,它们的大小分别为 32 位和 64 位。默认类型是 f64,因为在现代 CPU 上,它的速度与 f32 大致相同,但精度更高。所有浮点类型都是有符号的。

创建一个名为 data-types 的新项目:

cargo new data-types
cd data-types

以下是一个展示浮点数实际应用的示例:

文件名:src/main.rs

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

浮点数是根据 IEEE-754 标准表示的。f32 类型是单精度浮点数,而 f64 具有双精度。

数值运算

Rust 支持你对所有数字类型所期望的基本数学运算:加法、减法、乘法、除法和取余。整数除法向零截断为最接近的整数。以下代码展示了如何在 let 语句中使用每种数值运算:

文件名:src/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 中的布尔类型有两个可能的值:truefalse。布尔值的大小为一个字节。Rust 中的布尔类型使用 bool 来指定。例如:

文件名:src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // 带有显式类型标注
}

使用布尔值的主要方式是通过条件语句,比如 if 表达式。我们将在“控制流”一章中介绍 if 表达式在 Rust 中的工作方式。

字符类型

Rust 的 char 类型是该语言中最基本的字母类型。以下是一些声明 char 值的示例:

文件名:src/main.rs

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // 带有显式类型标注
    let heart_eyed_cat = '😻';
}

请注意,我们使用单引号指定 char 字面量,而字符串字面量使用双引号。Rust 的 char 类型大小为四个字节,代表一个 Unicode 标量值,这意味着它能表示的远不止 ASCII 字符。带重音的字母、中文、日文和韩文汉字、表情符号以及零宽度空格在 Rust 中都是有效的 char 值。Unicode 标量值范围从 U+0000U+D7FF 以及 U+E000U+10FFFF(包括两端)。然而,“字符”在 Unicode 中并不是一个真正的概念,所以你对“字符”的直观理解可能与 Rust 中的 char 并不一致。我们将在“使用字符串存储 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 分解为三个单独的变量 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 章中更详细地讨论栈和堆),或者当你希望确保始终有固定数量的元素时,数组很有用。不过,数组不像向量类型那样灵活。向量是标准库提供的一种类似的集合类型,其大小允许增长或缩小。如果你不确定是使用数组还是向量,很可能应该使用向量。第 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]; 相同,但方式更简洁。

访问数组元素

数组是一块已知固定大小的连续内存块,可以在栈上分配。你可以使用索引来访问数组的元素,如下所示:

文件名: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!("请输入一个数组索引。");

    let mut index = String::new();

    io::stdin()
     .read_line(&mut index)
     .expect("读取行失败");

    let index: usize = index
     .trim()
     .parse()
     .expect("输入的索引不是一个数字");

    let element = a[index];

    println!(
        "索引 {index} 处的元素值是:{element}"
    );
}

这段代码编译成功。如果你使用 cargo run 运行这段代码并输入 01234,程序将打印出数组中该索引处的对应值。如果你输入一个超出数组末尾的数字,比如 10,你会看到如下输出:

thread'main' panicked at 'index out of bounds: the len is 5 but the index is
10', src/main.rs:19:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

程序在索引操作中使用无效值时导致了一个运行时错误。程序带着错误信息退出,没有执行最后的 println! 语句。当你试图使用索引访问元素时,Rust 会检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度,Rust 将会 panic。这个检查必须在运行时进行,特别是在这种情况下,因为编译器不可能知道用户在运行代码时会输入什么值。

这是 Rust 内存安全原则起作用的一个例子。在许多低级语言中,不会进行这种检查,当你提供一个错误的索引时,可能会访问无效内存。Rust 通过立即退出而不是允许内存访问并继续来保护你免受这种错误的影响。第 9 章将讨论更多关于 Rust 的错误处理,以及如何编写既不会 panic 也不会允许无效内存访问的可读、安全的代码。

总结

恭喜你!你已经完成了数据类型实验。你可以在 LabEx 中练习更多实验来提升你的技能。