数组和切片
了解 Rust 中最常见的数据类型和访问模式。
让我们快速回顾一下数组和切片的概念。
数组
数组 是一个固定数量元素的集合,所有元素类型均为单一类型 T。数组的类型表示为 [T; n],其中 T 是任意类型,n 是固定的元素个数,即数组的大小在编译时就已经确定。
创建数组有两种方式:
- 通过列出每个元素(例如
[A, B, C],其中A、B和C是相同类型的元素)。 - 通过复制重复的方式,即声明一个元素及其复制次数(例如
[A; 3])。该元素类型必须实现Copytrait。
以下是一些数组初始化的例子:
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 是一个整数数组。
这个例子有两个重要意义:
- 它展示了如何借助编译器的提示逐步修正代码。
- 它说明了将整数类型设置错误会导致编译失败。那些习惯于其他语言的开发者常常会犯这类错误。
另一个常见的错误是试图强行指定数据类型。这是因为如果我们习惯了在 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);
如你所见,通过合理使用切片模式,我们可以大大简化复杂的匹配逻辑。