Logo
Section 1 / 5

数组和切片

了解 Rust 中最常见的数据类型和访问模式。

导入

数组和切片

了解 Rust 中最常见的数据类型和访问模式。

让我们快速回顾一下数组和切片的概念。

数组

数组 是一个固定数量元素的集合,所有元素类型均为单一类型 T。数组的类型表示为 [T; n],其中 T 是任意类型,n 是固定的元素个数,即数组的大小在编译时就已经确定。

创建数组有两种方式:

  • 通过列出每个元素(例如 [A, B, C],其中 A、B 和 C 是相同类型的元素)。
  • 通过复制重复的方式,即声明一个元素及其复制次数(例如 [A; 3])。该元素类型必须实现 Copy trait。

以下是一些数组初始化的例子:

1let array: [f32; 3] = [1.1, 1.2, 1.3]; 2let array = [0i8; 3];

数组是零索引的,第一个元素位于索引 0,使用 usize 类型进行索引访问。我们也可以使用相同的语法为数组元素赋值,但前提是数组必须是 mut 的。

1let array = ["first", "second", "third"]; 2println!("Element 0 is: {}", array[0]); 3 4let mut new_array: [i32; 4] = [0; 4]; 5new_array[1] = 1; 6println!("Element 1 is: {}", new_array[1]);

我们可以使用 Debug 来打印数组。

事实上,编译器会自动为最多包含 32 个元素的数组实现 Debug 和 Default trait。

以下代码可以正常运行:

1let array = [0i8; 32]; 2println!("{:?}", array);

然而,以下代码将无法编译:

1// 注意:这不会编译! 2let array = [0i8; 33]; 3println!("{:?}", array);

运行上述代码时,会显示如下编译错误:

1error[E0277]: arrays only have std trait implementations for lengths 0..=32

Default trait 也仅对最多包含 32 个元素的数组自动实现。

切片

切片 是一种不拥有数据所有权的类型。切片用于引用集合中连续的一段元素序列。

数组天然可以转换为切片。

切片的标准形式是 &array[slice],它可以从整个集合中提取出一个切片。

例如:

1let array = [1, 2, 3, 4, 5]; 2let slice = &array[1..3]; 3println!("Slice: {:?}", slice);

运行上述代码,输出结果为:

1Slice: [2, 3]

这意味着切片的索引是左闭右开的:包含起始位置的元素,但不包含结束位置的元素。

让我们检查下面这段代码的行为:

1let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 2let slice = &array[1..2]; 3println!("Slice: {:?}", slice); 4let slice = &array[..3]; 5println!("Slice: {:?}", slice); 6let slice = &array[5..]; 7println!("Slice: {:?}", slice); 8let slice = &array[..0]; 9println!("Slice: {:?}", slice);

与预期不同的是,&array[..3]; 输出:

1Slice: [1, 2, 3]

而 &array[5..]; 输出:

1Slice: [6, 7, 8, 9, 10]

这看起来有些反直觉,直到我们意识到索引是从 0 开始的。

实际上,最后一个例子生成了一个空切片,因为我们要求的是从第 0 个元素之前的所有元素。

我们需要记住,切片不拥有数据所有权,因此可以在同一个数组中创建多个重叠的切片。

如果切片拥有所有权,它们就无法如此方便地访问数组。

常见的数组错误

熟悉其他编程语言(尤其是 C/C++ 或 Python)的人可能会在 Rust 中循环遍历方面遇到困难。

考虑以下代码:

1// 注意:这不会编译! 2let magic_choice = [0, 1, 2]; 3let magic_answer = ["first", "second", "third"]; 4 5for i in magic_choice { 6 println!("{}", magic_answer[i]); 7}

从概念上讲,这个过程并不复杂:我们使用 magic_choice 数组(作为索引)来获取 magic_answer 数组中的对应元素。

这种结构在很多场景中都很常见,比如让用户自行选择要操作的数组。

然而,这里会出现错误:

1error[E0277]: `[{integer}; 3]` is not an iterator

问题出在哪里?

正如通常的情况一样,编译器给出了提示:

1borrow the array with `&` or call `.iter()` on it to iterate over it

很好!我们尝试对数组进行迭代:

1// 注意:这不会编译! 2let magic_choice = [0, 1, 2]; 3let magic_answer = ["first", "second", "third"]; 4 5for i in magic_choice.iter() { 6 println!("{}", magic_answer[i]); 7}

这段代码仍然无法编译,并报错如下:

1error[E0277]: the type `[&str]` cannot be indexed by `&{integer}`

编译器推断出 magic_choice 数组的类型是整数类型。但数组的索引必须是 usize 类型。

我们可以将 i 元素显式转换为 usize:

1// 注意:这不会编译! 2let magic_choice = [0, 1, 2]; 3let magic_answer = ["first", "second", "third"]; 4 5for i in magic_choice.iter() { 6 println!("{}", magic_answer[i as usize]); 7}

但代码依然有问题。这次报错变为:

1error[E0606]: casting `&i32` as `usize` is invalid

再次,编译器给出了有用的建议:

1cannot cast `&i32` as `usize` 2help: dereference the expression: `*i`

根据这个提示,我们可以修正代码如下,最终成功编译:

1// 这段代码可以编译! 2let magic_choice = [0, 1, 2]; 3let magic_answer = ["first", "second", "third"]; 4 5for i in magic_choice.iter() { 6 println!("{}", magic_answer[*i as usize]); 7}

许多初学 Rust 的开发者都会遇到编译器的挑战。只要我们能逐步理解每一步的错误,这其实是一个非常有益的学习过程。

现在我们的代码已经正确运行了,让我们回顾一下之前犯的几个错误:

  • 循环方式错误(没有使用 .iter())
  • 索引类型不是 usize
  • 类型转换方式错误
  • 没有正确解引用

这些问题的根本原因在于:编译器推断出 magic_choice 是一个整数数组。

这个例子有两个重要意义:

  1. 它展示了如何借助编译器的提示逐步修正代码。
  2. 它说明了将整数类型设置错误会导致编译失败。那些习惯于其他语言的开发者常常会犯这类错误。

另一个常见的错误是试图强行指定数据类型。这是因为如果我们习惯了在 C 等语言中用整数作为索引,通常会用 int 类型循环。

有趣的是,我们其实可以从一开始就让编译器正确推断类型,例如:

1// 这段代码可以编译! 2let magic_choice = [0, 1, 2]; 3let magic_answer = ["first", "second", "third"]; 4 5for i in magic_choice.iter() { 6 println!("{}", magic_answer[*i]); 7}

如果我们只是直接解引用而不进行类型转换,编译器就能理解我们想要的是一个 usize 类型的数组。

在 C/C++ 中强制类型转换很常见。有 C 背景的开发者往往倾向于到处使用类型转换,希望借此让编译器理解意图。

但实际上,如果我们能在使用前就明确告诉编译器数据的类型,就不需要这些额外的转换步骤。

事实上,我们完全可以从一开始就明确类型!

看看下面这段代码:

1// 这段代码可以编译! 2let magic_choice: [usize; 3] = [0, 1, 2]; 3let magic_answer = ["first", "second", "third"]; 4 5for i in magic_choice.iter() { 6 println!("{}", magic_answer[*i]); 7}

在这种情况下,我们一开始就声明了正确的类型,后续一切顺利。

切片模式

Rust 最强大的特性之一就是其强大的模式匹配能力。

但我们也可以使用切片模式进行匹配!

切片模式可以从数组中提取元素:

1let point = [1.22, 3.3]; 2let [x, y] = point; 3println!("X: {}", x); 4println!("Y: {}", y);

那么,如何在切片匹配中应用这一机制呢?

考虑以下代码:

1fn print_elements(elements: Vec<&str>) { 2 match elements.as_slice() { 3 [] => println!("No elements"), 4 [element] => println!("One element only: {}", element), 5 _ => println!("More than one element present"), 6 } 7} 8 9print_elements(vec![]); 10print_elements(vec!["one"]); 11print_elements(vec!["one", "two"]);

在上面的代码中,我们将 Vec<&str> 视为切片(通过 as_slice() 方法)来进行匹配。

注意:本节假设你已熟悉向量(vector)。

让我们分析这段代码的工作原理:

  • match 的第一个分支是空切片,因此匹配空向量。
  • 第二个分支是一个包含单个元素的切片模式,它会提取并打印该元素。
  • 第三个分支匹配任何包含多个元素的向量。

再看下面的修改版本:

1fn print_elements(elements: Vec<&str>) { 2 match elements.as_slice() { 3 [] => println!("No elements"), 4 [element] => println!("One element only: {}", element), 5 [element1, element2] => println!("Two elements\n1: {}\n2: {}", element1, element2), 6 _ => println!("More than one element present"), 7 } 8} 9 10let empty = vec![]; 11let one = vec!["one"]; 12let two = vec!["one", "two"]; 13let three = vec!["one", "two", "three"]; 14 15print_elements(empty); 16print_elements(one); 17print_elements(three); 18print_elements(two);

如你所见,通过合理使用切片模式,我们可以大大简化复杂的匹配逻辑。

开始之前

向量和字符串