Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help


x-i18n: generated_at: “2026-03-01T13:42:06Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: fb0ac90f3652f4096624bc008f2a5ade603ed1d7af078281cec7a88da66e82bb source_path: ch04-03-slices.md workflow: 16

切片类型 (The Slice Type)

The Slice Type

“切片 (Slices)”让你能引用集合 (collection)中连续的一系列元素。切片是一种引用,因此它没有所有权。

Slices let you reference a contiguous sequence of elements in a collection. A slice is a kind of reference, so it does not have ownership.

这里有一个编程小问题:编写一个函数,它接收一个由空格分隔单词的字符串,并返回在该字符串中找到的第一个单词。如果函数在字符串中没有找到空格,则整个字符串必定是一个单词,因此应该返回整个字符串。

Here’s a small programming problem: Write a function that takes a string of words separated by spaces and returns the first word it finds in that string. If the function doesn’t find a space in the string, the whole string must be one word, so the entire string should be returned.

注意:为了介绍切片,本节我们假设仅使用 ASCII;关于 UTF-8 处理的更详尽讨论请参见第 8 章的“使用字符串存储 UTF-8 编码的文本”部分。

Note: For the purposes of introducing slices, we are assuming ASCII only in this section; a more thorough discussion of UTF-8 handling is in the “Storing UTF-8 Encoded Text with Strings” section of Chapter 8.

让我们先看看在不使用切片的情况下如何编写此函数的签名,以理解切片将解决的问题:

Let’s work through how we’d write the signature of this function without using slices, to understand the problem that slices will solve:

fn first_word(s: &String) -> ?

first_word 函数有一个类型为 &String 的参数。我们不需要所有权,所以这没问题。(在惯用的 Rust 中,除非需要,否则函数不会获取其参数的所有权,随着深入学习,其原因会变得清晰。)但我们应该返回什么呢?我们并没有真正的方法来表达字符串的“一部分”。然而,我们可以返回单词结尾的索引,由空格表示。让我们尝试一下,如示例 4-7 所示。

The first_word function has a parameter of type &String. We don’t need ownership, so this is fine. (In idiomatic Rust, functions do not take ownership of their arguments unless they need to, and the reasons for that will become clear as we keep going.) But what should we return? We don’t really have a way to talk about part of a string. However, we could return the index of the end of the word, indicated by a space. Let’s try that, as shown in Listing 4-7.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-07/src/main.rs:here}}
}

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

Because we need to go through the String element by element and check whether a value is a space, we’ll convert our String to an array of bytes using the as_bytes method.

{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-07/src/main.rs:as_bytes}}

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

Next, we create an iterator over the array of bytes using the iter method:

{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-07/src/main.rs:iter}}

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

We’ll discuss iterators in more detail in Chapter 13. For now, know that iter is a method that returns each element in a collection and that enumerate wraps the result of iter and returns each element as part of a tuple instead. The first element of the tuple returned from enumerate is the index, and the second element is a reference to the element. This is a bit more convenient than calculating the index ourselves.

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

Because the enumerate method returns a tuple, we can use patterns to destructure that tuple. We’ll be discussing patterns more in Chapter 6. In the for loop, we specify a pattern that has i for the index in the tuple and &item for the single byte in the tuple. Because we get a reference to the element from .iter().enumerate(), we use & in the pattern.

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

Inside the for loop, we search for the byte that represents the space by using the byte literal syntax. If we find a space, we return the position. Otherwise, we return the length of the string by using s.len().

{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-07/src/main.rs:inside_for}}

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

We now have a way to find out the index of the end of the first word in the string, but there’s a problem. We’re returning a usize on its own, but it’s only a meaningful number in the context of the &String. In other words, because it’s a separate value from the String, there’s no guarantee that it will still be valid in the future. Consider the program in Listing 4-8 that uses the first_word function from Listing 4-7.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-08/src/main.rs:here}}
}

该程序编译时没有任何错误,如果我们在调用 s.clear() 之后使用 word 也是如此。因为 words 的状态完全没有联系,所以 word 仍然包含值 5。我们可以将该值 5 与变量 s 一起使用,尝试提取出第一个单词,但这将是一个 bug,因为自从我们在 word 中保存 5 以来,s 的内容已经发生了变化。

This program compiles without any errors and would also do so if we used word after calling s.clear(). Because word isn’t connected to the state of s at all, word still contains the value 5. We could use that value 5 with the variable s to try to extract the first word out, but this would be a bug because the contents of s have changed since we saved 5 in word.

必须担心 word 中的索引与 s 中的数据不同步是乏味且容易出错的!如果我们编写一个 second_word 函数,管理这些索引会更加脆弱。它的签名必须看起来像这样:

Having to worry about the index in word getting out of sync with the data in s is tedious and error-prone! Managing these indices is even more brittle if we write a second_word function. Its signature would have to look like this:

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

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

Now we’re tracking a starting and an ending index, and we have even more values that were calculated from data in a particular state but aren’t tied to that state at all. We have three unrelated variables floating around that need to be kept in sync.

幸运的是,Rust 为这个问题提供了一个解决方案:字符串切片。

Luckily, Rust has a solution to this problem: string slices.

字符串切片 (String Slices)

String Slices

“字符串切片 (string slice)”是对 String 中一部分连续元素的引用,它看起来像这样:

A string slice is a reference to a contiguous sequence of the elements of a String, and it looks like this:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-17-slice/src/main.rs:here}}
}

hello 不是对整个 String 的引用,而是对 String 一部分的引用,由额外的 [0..5] 部分指定。我们使用方括号内的范围来创建切片,通过指定 [starting_index..ending_index],其中 starting_index 是切片中的第一个位置,而 ending_index 比切片中的最后一个位置大 1。在内部,切片数据结构存储切片的起始位置和长度,长度对应于 ending_index 减去 starting_index。因此,在 let world = &s[6..11]; 的情况下,world 将是一个包含指向 s 索引 6 处字节的指针以及长度值 5 的切片。

Rather than a reference to the entire String, hello is a reference to a portion of the String, specified in the extra [0..5] bit. We create slices using a range within square brackets by specifying [starting_index..ending_index], where starting_index is the first position in the slice and ending_index is one more than the last position in the slice. Internally, the slice data structure stores the starting position and the length of the slice, which corresponds to ending_index minus starting_index. So, in the case of let world = &s[6..11];, world would be a slice that contains a pointer to the byte at index 6 of s with a length value of 5.

图 4-7 以图表形式展示了这一点。

Figure 4-7 shows this in a diagram.

三张表格:一张代表 s 的栈数据的表格,它指向堆上字符串数据 "hello world" 索引 0 处的字节。第三张表代表切片 world 的栈数据,它有一个长度值 5,并指向堆数据表的第 6 个字节。

图 4-7:引用 String 一部分的字符串切片

利用 Rust 的 .. 范围语法,如果你想从索引 0 开始,可以省略两个点之前的数值。换句话说,这两者是等价的:

With Rust’s .. range syntax, if you want to start at index 0, you can drop the value before the two periods. In other words, these are equal:

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

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

同理,如果你的切片包含 String 的最后一个字节,你可以省略末尾的数字。这意味着这两者是等价的:

By the same token, if your slice includes the last byte of the String, you can drop the trailing number. That means these are equal:

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

let len = s.len();

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

你也可以省略这两个值来获取整个字符串的切片。所以,这两者是等价的:

You can also drop both values to take a slice of the entire string. So, these are equal:

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

let len = s.len();

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

注意:字符串切片范围索引必须发生在有效的 UTF-8 字符边界处。如果你尝试在多字节字符的中间创建字符串切片,程序将因错误而退出。

Note: String slice range indices must occur at valid UTF-8 character boundaries. If you attempt to create a string slice in the middle of a multibyte character, your program will exit with an error.

记住所有这些信息后,让我们重写 first_word 以返回切片。表示“字符串切片”的类型写作 &str

With all this information in mind, let’s rewrite first_word to return a slice. The type that signifies “string slice” is written as &str:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-18-first-word-slice/src/main.rs:here}}
}

我们以示例 4-7 中相同的方式获取单词结尾的索引,即寻找第一次出现的空格。当我们找到空格时,我们使用字符串的开头作为起始索引,空格的索引作为结束索引,返回一个字符串切片。

We get the index for the end of the word the same way we did in Listing 4-7, by looking for the first occurrence of a space. When we find a space, we return a string slice using the start of the string and the index of the space as the starting and ending indices.

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

Now when we call first_word, we get back a single value that is tied to the underlying data. The value is made up of a reference to the starting point of the slice and the number of elements in the slice.

返回切片对于 second_word 函数也同样有效:

Returning a slice would also work for a second_word function:

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

我们现在有了一个简单直观的 API,它更难出错,因为编译器将确保指向 String 的引用保持有效。还记得示例 4-8 程序中的那个 bug 吗?当时我们获取了第一个单词结尾的索引,但随后清空了字符串,导致索引失效。那段代码逻辑上是错误的,但没有显示任何即时错误。如果以后我们继续尝试将第一个单词索引与清空的字符串一起使用,问题就会显现出来。切片使这个 bug 变得不可能,并让我们更早地知道代码存在问题。使用切片版本的 first_word 会抛出一个编译时错误:

We now have a straightforward API that’s much harder to mess up because the compiler will ensure that the references into the String remain valid. Remember the bug in the program in Listing 4-8, when we got the index to the end of the first word but then cleared the string so our index was invalid? That code was logically incorrect but didn’t show any immediate errors. The problems would show up later if we kept trying to use the first word index with an emptied string. Slices make this bug impossible and let us know much sooner that we have a problem with our code. Using the slice version of first_word will throw a compile-time error:

{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-19-slice-error/src/main.rs:here}}

这是编译器错误:

Here’s the compiler error:

{{#include ../listings/ch04-understanding-ownership/no-listing-19-slice-error/output.txt}}

回想一下借用规则,如果我们拥有某样东西的不可变引用,就不能同时再获取一个可变引用。因为 clear 需要截断 String,所以它需要获取一个可变引用。在调用 clear 之后的 println! 使用了 word 中的引用,因此不可变引用在该点必须仍然有效。Rust 不允许 clear 中的可变引用和 word 中的不可变引用同时存在,编译失败。Rust 不仅使我们的 API 更易于使用,还消除了一整类编译时错误!

Recall from the borrowing rules that if we have an immutable reference to something, we cannot also take a mutable reference. Because clear needs to truncate the String, it needs to get a mutable reference. The println! after the call to clear uses the reference in word, so the immutable reference must still be active at that point. Rust disallows the mutable reference in clear and the immutable reference in word from existing at the same time, and compilation fails. Not only has Rust made our API easier to use, but it has also eliminated an entire class of errors at compile time!

字符串字面量即切片 (String Literals as Slices)

String Literals as Slices

回想一下,我们谈到过字符串字面量被存储在二进制文件中。现在我们了解了切片,就可以正确地理解字符串字面量了:

Recall that we talked about string literals being stored inside the binary. Now that we know about slices, we can properly understand string literals:

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

这里的 s 类型是 &str:它是一个指向二进制文件特定点的切片。这也是为什么字符串字面量是不可变的;&str 是一个不可变引用。

The type of s here is &str: It’s a slice pointing to that specific point of the binary. This is also why string literals are immutable; &str is an immutable reference.

字符串切片作为参数 (String Slices as Parameters)

String Slices as Parameters

既然知道可以获取字面量和 String 值的切片,这引导我们对 first_word 进行最后一次改进,即它的签名:

Knowing that you can take slices of literals and String values leads us to one more improvement on first_word, and that’s its signature:

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

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

A more experienced Rustacean would write the signature shown in Listing 4-9 instead because it allows us to use the same function on both &String values and &str values.

{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-09/src/main.rs:here}}

如果我们有一个字符串切片,可以直接传递。如果我们有一个 String,可以传递该 String 的切片或对该 String 的引用。这种灵活性利用了“解引用强制转换 (deref coercions)”,这是我们将在第 15 章“Using Deref Coercions in Functions and Methods”部分介绍的功能。

If we have a string slice, we can pass that directly. If we have a String, we can pass a slice of the String or a reference to the String. This flexibility takes advantage of deref coercions, a feature we will cover in the “Using Deref Coercions in Functions and Methods” section of Chapter 15.

定义函数以接收字符串切片而不是对 String 的引用,使我们的 API 更加通用且有用,同时不损失任何功能:

Defining a function to take a string slice instead of a reference to a String makes our API more general and useful without losing any functionality:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-09/src/main.rs:usage}}
}

其他切片 (Other Slices)

Other Slices

正如你可能想象的,字符串切片是特定于字符串的。但也有更通用的切片类型。考虑这个数组:

String slices, as you might imagine, are specific to strings. But there’s a more general slice type too. Consider this array:

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

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

Just as we might want to refer to part of a string, we might want to refer to part of an array. We’d do so like this:

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

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

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

这个切片的类型是 &[i32]。它的工作方式与字符串切片相同,通过存储第一个元素的引用和长度。你将为各种其他集合使用这种切片。我们将在第 8 章讨论向量 (vectors) 时详细讨论这些集合。

This slice has the type &[i32]. It works the same way as string slices do, by storing a reference to the first element and a length. You’ll use this kind of slice for all sorts of other collections. We’ll discuss these collections in detail when we talk about vectors in Chapter 8.

总结 (Summary)

Summary

所有权、借用和切片的概念确保了 Rust 程序在编译时的内存安全。Rust 语言让你以与其他系统编程语言相同的方式控制内存使用。但是,让数据的所有者在超出作用域时自动清理该数据,意味着你不需要为了获得这种控制权而编写和调试额外的代码。

The concepts of ownership, borrowing, and slices ensure memory safety in Rust programs at compile time. The Rust language gives you control over your memory usage in the same way as other systems programming languages. But having the owner of data automatically clean up that data when the owner goes out of scope means you don’t have to write and debug extra code to get this control.

所有权影响了 Rust 的许多其他部分的工作方式,因此在本书的其余部分中,我们将进一步讨论这些概念。让我们继续第 5 章,看看如何在 struct(结构体)中将各部分数据组合在一起。

Ownership affects how lots of other parts of Rust work, so we’ll talk about these concepts further throughout the rest of the book. Let’s move on to Chapter 5 and look at grouping pieces of data together in a struct.