跳转到内容

Rust slice(切片)

slice(切片)让你借用数组或 vector 的一段,相当于看向其中一部分的“视图”,既不复制数据,也不拿走所有权。如果你已经对单个值用过借用,那么 slice 就是把同样的思路用在一段连续的元素上。

fn main() {
let numbers = [10, 20, 30, 40, 50];
let middle = &numbers[1..4]; // 索引为 1、2、3 的元素
println!("{:?}", middle);
}

输出:

[20, 30, 40]
▶ 动手试一试

1..4 就是你在 for 循环里见过的那种范围:从索引 1 开始,在索引 4 之前停下,所以取到的是索引 1、2 和 3。

下面几种简写经常会用到:

fn main() {
let numbers = [10, 20, 30, 40, 50];
println!("{:?}", &numbers[..3]); // 从开头到索引 3 之前 -> [10, 20, 30]
println!("{:?}", &numbers[2..]); // 从索引 2 到末尾 -> [30, 40, 50]
println!("{:?}", &numbers[..]); // 全部 -> [10, 20, 30, 40, 50]
}
▶ 动手试一试

其实你早就在用 slice 了:&str 就是 String(或另一个字符串)的一个 slice!这也是这两种类型配合得这么好的原因:

fn main() {
let sentence = String::from("The quick brown fox");
let first_word = &sentence[0..3];
println!("{first_word}"); // "The"
}
▶ 动手试一试

这里的句子特意保留了英文:字符串 slice 的范围按字节计算,而一个汉字在 UTF-8 里占 3 个字节。换成中文的话,0..3 只能取到第一个汉字,如果范围切在某个汉字的中间,程序还会 panic。

你也许会想,把需要的那一段复制成一个新的 Vec 或 String 不就行了?slice 完全不需要这样做:它只是指向现有数据的一部分,就像引用(&)指向单个值一样。这意味着没有额外的内存分配,没有复制,而且你在借用那一课学到的规则照样适用。

slice 特别适合用作函数参数,因为数组和 vector 都能传进去:

fn sum(numbers: &[i32]) -> i32 {
let mut total = 0;
for n in numbers {
total += n;
}
total
}
fn main() {
let array = [1, 2, 3];
let vector = vec![4, 5, 6];
println!("{}", sum(&array));
println!("{}", sum(&vector));
}
▶ 动手试一试

&[i32] 既能接收来自数组的 slice,也能接收来自 Vec 的 slice:一个函数,两种调用方式都能用。

到目前为止,我们的 slice 都只是读取数据。可变 slice(&mut [T])可以让函数修改传给它的元素,但也只能修改这些元素:

fn double_all(numbers: &mut [i32]) {
for i in 0..numbers.len() {
numbers[i] *= 2;
}
}
fn main() {
let mut scores = vec![1, 2, 3, 4, 5];
double_all(&mut scores[1..3]); // 只有索引 1 和 2
println!("{:?}", scores);
double_all(&mut scores); // 整个 vector
println!("{:?}", scores);
}

输出:

[1, 4, 6, 4, 5]
[2, 8, 12, 8, 10]
▶ 动手试一试

第一次调用传入的是 &mut scores[1..3],所以 double_all 只能看到索引 1 和 2。这个范围之外的元素,它碰都碰不到。在函数内部,slice 的索引又从 0 开始:对 double_all 来说,它拿到的只是一个有两个数字的列表。

这样做之所以安全,靠的是你会在借用那一课正式学到的一条规则:函数持有 &mut slice 的时候,别的代码都不能使用这些元素,所以代码里永远不会有两个地方同时修改它们。另外,vector 本身也必须声明为 mut,否则 Rust 不允许你可变地借用它。

创建 vector vec![3, 6, 9, 12, 15, 18]。把它的前一半和后一半作为两个 slice 分别打印出来。提示:先算出 let mid = numbers.len() / 2;,再用 ..mid 和 mid..。

查看答案
fn main() {
let numbers = vec![3, 6, 9, 12, 15, 18];
let mid = numbers.len() / 2;
let first_half = &numbers[..mid];
let second_half = &numbers[mid..];
println!("前一半:{:?}", first_half);
println!("后一半:{:?}", second_half);
}

输出:

前一半:[3, 6, 9]
后一半:[12, 15, 18]
▶ 动手试一试

因为 mid 是根据 len() 算出来的,所以增加或删除数字之后,这段代码照样能用。如果长度是奇数,多出来的那个元素会被分到后一半。

写一个函数 largest(numbers: &[i32]) -> i32,返回其中最大的数字。分别用一个数组、一个 vector,以及只包含 vector 第一个元素的 slice 来调用它。提示:从 numbers[0] 开始,然后遍历其余的元素,就像处理数组时那样。

查看答案
fn largest(numbers: &[i32]) -> i32 {
let mut biggest = numbers[0];
for n in numbers {
if *n > biggest {
biggest = *n;
}
}
biggest
}
fn main() {
let array = [7, 2, 9, 4];
let vector = vec![10, 35, 21];
println!("{}", largest(&array));
println!("{}", largest(&vector));
println!("{}", largest(&vector[..1]));
}

输出:

9
35
10
▶ 动手试一试

遍历 slice 时,你拿到的是每个元素的引用(&i32),而不是数字本身。*n 的意思是“n 指向的那个值”,所以 *n > biggest 比较的是两个普通的数字。这个版本遇到空 slice 时会 panic,因为 numbers[0] 不存在。以后你会学到,这种情况可以返回一个 Option。

  • slice(&[T])借用数组或 vector 的一段,而不复制它。
  • 1..4 选中索引 1 到 3;省略 .. 的某一边,就表示从开头开始或一直到末尾。
  • &str 本身就是一段文本的 slice,所以字符串和 slice 有那么多共同的行为。
  • 想让函数同时接收数组和 vector 时,参数类型优先用 &[T]。
  • 可变 slice(&mut [T])让函数只修改传给它的那些元素。

小测验

1. slice 能让你对数组或 vector 的一部分做什么?

2. &numbers[1..4] 包含哪些索引?

3. 为什么 &[i32] 很适合用作函数的参数类型?

得分: 0 / 3