Slice 类型

Slice 允许你引用 集合 中一段连续的元素序列,而不是整个集合。slice 是一种引用,因此它是一个非所有权指针。

为了说明 slice 的用途,让我们解决一个小编程问题:编写一个函数,该函数接受一个由空格分隔的单词字符串,并返回在该字符串中找到的第一个单词。如果该函数在字符串中找不到空格,则整个字符串必须是一个单词,因此应返回整个字符串。在没有 slice 的情况下,我们可能会像这样编写函数的签名

fn first_word(s: &String) -> ?

first_word 函数的参数是 &String。我们不想要字符串的所有权,所以这很好。但是我们应该返回什么呢?我们实际上没有办法谈论字符串的一部分。但是,我们可以返回单词结尾的索引,用空格表示。让我们尝试一下,如 Listing 4-7 所示。

文件名:src/main.rs

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Listing 4-7:first_word 函数,它返回 String 参数的字节索引值

因为我们需要逐个遍历 String 元素并检查值是否为空格,所以我们将使用 as_bytes 方法将 String 转换为字节数组

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

接下来,我们使用 iter 方法在字节数组上创建一个迭代器

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

我们将在 第 13 章 中更详细地讨论迭代器。现在,请了解 iter 是一种方法,它返回集合中的每个元素,而 enumerate 包装了 iter 的结果,并将每个元素作为元组的一部分返回。从 enumerate 返回的元组的第一个元素是索引,第二个元素是对元素的引用。这比我们自己计算索引要方便一些。

因为 enumerate 方法返回一个元组,所以我们可以使用模式来解构该元组。我们将在 第 6 章 中更详细地讨论模式。在 for 循环中,我们指定一个模式,该模式对于元组中的索引使用 i,对于元组中的单个字节使用 &item。因为我们从 .iter().enumerate() 获取对元素的引用,所以我们在模式中使用 &

for 循环内部,我们通过使用字节字面量语法来搜索表示空格的字节。如果我们找到一个空格,我们将返回位置。否则,我们将使用 s.len() 返回字符串的长度

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

现在我们有了一种找出字符串中第一个单词结尾索引的方法,但存在一个问题。我们单独返回一个 usize,但它只是在 &String 的上下文中才有意义的数字。换句话说,因为它与 String 是一个单独的值,所以无法保证它在将来仍然有效。考虑 Listing 4-8 中的程序,该程序使用了 Listing 4-7 中的 first_word 函数。

文件名:src/main.rs

Listing 4-8:存储调用 first_word 函数的结果,然后更改 String 内容

此程序在编译时没有任何错误,因为在调用 first_word 后,s 保留了写入权限。因为 words 的状态完全无关,所以 word 仍然包含值 5。我们可以将值 5 与变量 s 一起使用来尝试提取第一个单词,但这将是一个 bug,因为自从我们将 5 保存在 word 中以来,s 的内容已更改。

必须担心 word 中的索引与 s 中的数据失去同步,这既繁琐又容易出错!如果我们编写 second_word 函数,则管理这些索引会更加脆弱。它的签名必须看起来像这样

fn second_word(s: &String) -> (usize, usize) {

现在我们正在跟踪起始结束索引,并且我们有更多从特定状态的数据计算出来但与该状态完全无关的值。我们有三个不相关的变量在周围浮动,需要保持同步。

幸运的是,Rust 有一个解决此问题的方法:字符串 slice。

字符串 Slice

字符串 slice 是对 String 一部分的引用,它看起来像这样

与对整个 String 的引用(如 s2)不同,hello 是对 String 一部分的引用,在额外的 [0..5] 位中指定。我们通过在方括号内指定范围 [starting_index..ending_index] 来创建 slice,其中 starting_index 是 slice 中的第一个位置,ending_index 比 slice 中的最后一个位置多一个。

Slice 是特殊类型的引用,因为它们是“胖”指针,或带有元数据的指针。在这里,元数据是 slice 的长度。我们可以通过更改我们的可视化来窥探 Rust 数据结构的内部结构,从而看到此元数据

请注意,变量 helloworld 都具有 ptrlen 字段,它们共同定义了堆上字符串的下划线区域。你还可以看到 String 实际上是什么样的:字符串是字节向量 (Vec<u8>),它包含长度 len 和缓冲区 buf,缓冲区具有指针 ptr 和容量 cap

由于 slice 是引用,因此它们也会更改被引用数据的权限。例如,观察下面,当 hello 创建为 s 的 slice 时,s 会失去写入和所有权权限

范围语法

使用 Rust 的 .. 范围语法,如果你想从索引零开始,则可以删除两个句点之前的值。换句话说,这些是相等的

#![allow(unused)]
fn main() {
let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];
}

同样,如果你的 slice 包含 String 的最后一个字节,则可以删除尾随数字。这意味着这些是相等的

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];
}

你还可以删除两个值以获取整个字符串的 slice。所以这些是相等的

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];
}

注意:字符串 slice 范围索引必须出现在有效的 UTF-8 字符边界处。如果你尝试在多字节字符的中间创建字符串 slice,则你的程序将退出并显示错误。为了介绍字符串 slice 的目的,我们在本节中仅假设 ASCII;有关 UTF-8 处理的更详尽讨论在 “使用 String 存储 UTF-8 编码文本”第 8 章的章节。

使用字符串 slice 重写 first_word

考虑到所有这些信息,让我们重写 first_word 以返回 slice。“字符串 slice” 的类型写为 &str

文件名:src/main.rs

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {}

我们以与 Listing 4-7 中相同的方式获取单词结尾的索引,方法是查找空格的第一次出现。当我们找到一个空格时,我们使用字符串的开头和空格的索引作为起始和结束索引返回一个字符串 slice。

现在,当我们调用 first_word 时,我们会得到一个与底层数据绑定的单个值。该值由对 slice 起始点的引用和 slice 中元素的数量组成。

返回 slice 也适用于 second_word 函数

fn second_word(s: &String) -> &str {

现在我们有了一个简单的 API,它更难出错,因为编译器将确保对 String 的引用保持有效。还记得 Listing 4-8 中程序的 bug 吗,当我们获得第一个单词结尾的索引,然后清空字符串,因此我们的索引无效?该代码在逻辑上是不正确的,但没有显示任何立即的错误。如果我们继续尝试将第一个单词索引与空字符串一起使用,则问题会在以后出现。Slice 使此 bug 不可能发生,并让我们更早地知道我们的代码存在问题。例如

文件名:src/main.rs

你可以看到,现在调用 first_word 会删除 s 的写入权限,这会阻止我们调用 s.clear()。这是编译器错误

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 |
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 |
20 |     println!("the first word is: {word}");
   |                                  ------ immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

回想一下借用规则,如果我们对某物有不可变引用,那么我们也无法获取可变引用。因为 clear 需要截断 String,所以它需要获取可变引用。调用 clear 之后的 println! 使用 word 中的引用,因此不可变引用必须在该点仍然处于活动状态。Rust 不允许 clear 中的可变引用和 word 中的不可变引用同时存在,并且编译失败。Rust 不仅使我们的 API 更易于使用,而且还在编译时消除了一整类错误!

字符串字面量是 Slice

回想一下,我们讨论过字符串字面量存储在二进制文件中。现在我们了解了 slice,我们可以正确理解字符串字面量

#![allow(unused)]
fn main() {
let s = "Hello, world!";
}

此处 s 的类型为 &str:它是一个指向二进制文件中特定点的 slice。这也是字符串字面量是不可变的的原因;&str 是一个不可变引用。

字符串 Slice 作为参数

了解到你可以获取字面量和 String 值的 slice,这使我们对 first_word 进行了另一项改进,那就是它的签名

fn first_word(s: &String) -> &str {

更有经验的 Rustacean 会编写 Listing 4-9 中所示的签名,因为它允许我们在 &String 值和 &str 值上使用相同的函数。

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // `first_word` works on slices of `String`s, whether partial or whole
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or whole
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

Listing 4-9:通过对 s 参数的类型使用字符串 slice 来改进 first_word 函数

如果我们有一个字符串 slice,我们可以直接传递它。如果我们有一个 String,我们可以传递 String 的 slice 或对 String 的引用。这种灵活性利用了解引用强制转换,这是我们将在 “带有函数和方法的隐式解引用强制转换”第 15 章的章节中介绍的功能。

定义一个函数来接受字符串 slice 而不是对 String 的引用,这使我们的 API 更加通用和有用,而不会丢失任何功能

文件名:src/main.rs

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // `first_word` works on slices of `String`s, whether partial or whole
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or whole
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

其他 Slice

你可能想象得到,字符串 slice 专门用于字符串。但是还有更通用的 slice 类型。考虑以下数组

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
}

正如我们可能想要引用字符串的一部分一样,我们也可能想要引用数组的一部分。我们会这样做

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);
}

此 slice 的类型为 &[i32]。它的工作方式与字符串 slice 相同,通过存储对第一个元素的引用和长度。你将对所有其他类型的集合使用这种 slice。当我们讨论第 8 章中的 vector 时,我们将详细讨论这些集合。

总结

Slice 是一种特殊的引用,它引用序列(如字符串或 vector)的子范围。在运行时,slice 表示为“胖指针”,其中包含指向范围开始的指针和范围的长度。与基于索引的范围相比,slice 的一个优点是 slice 在使用时不会失效。