Updated src/ownership/the_slice_type.md'.

This commit is contained in:
Hector PENG
2026-03-10 16:53:07 +08:00
parent 67bb6a289f
commit 610630f4d3
6 changed files with 43 additions and 54 deletions

View File

@@ -1,9 +1,11 @@
fn main() {
let reference_to_nothing = dangle();
println! ("{reference_to_nothing}");
}
fn dangle() -> &String {
fn dangle() -> String {
let s = String::from("hello");
&s
s
}

View File

@@ -0,0 +1,6 @@
[package]
name = "slice_demo"
version = "0.1.0"
edition = "2024"
[dependencies]

View File

@@ -1,18 +1,13 @@
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq! (slice, &[2, 3]);
println!("Hello, world!");
}
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];
return &s[..i];
}
}

View File

@@ -1,8 +0,0 @@
[package]
name = "slices"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -323,7 +323,6 @@ fn dangle() -> &String { // dangle 返回一个到 String 的引用
// 危险!
```
因为 `s` 是在 `dangle` 内部创建的,所以在 `dangle` 的代码完毕后,`s` 将被解分配。但我们试图返回对他的引用。这意味着这个引用将指向无效 `String`。这可不行Rust 将不允许我们这样做。

View File

@@ -1,22 +1,22 @@
# 切片类型
**The Slice Type**
所谓 *切片slices*,允许咱们引用集合中连续的元素序列。切片属于一种引用,因此他不拥有所有权。
这里有个小的编程问题:编写一个函数,该函数取由空格分隔单词的字符串,并返回其在该字符串中找到的第一个单词。若函数在字符串中未找到空格,则整个字符串必定是个单词,因此整个字符串应被返回。
*切片slices* 允许咱们引用某个集合中,连续的元素序列,而不是整个集合。切片属于一种引用,因此他不具有所有权
> **注意**:出于介绍切片的目的,我们在这一小节中仅假设 ASCII对 UTF-8 处理的更全面讨论,是在第 8 章的 [以字符串存储 UTF-8 编码的文本](../common_collections/strings.md) 小节处
这里有一个编程小问题:要编写一个,取个以空格分隔单词的字符串,并返回其在该字符串中,找到的第一个单词的函数。如果该函数在字符串中,未找到空格,那么整个字符串必定是一个单词,所以这整个字符串就应被返回。
我们来看看,在不使用切片的情况下,咱们要如何编写这个函数的签名,以了解切片将解决什么问题:
我们来看看,在不使用切片下咱们要怎样编写这个函数的签名,以了解切片将解决的问题:
```rust
fn first_word(s: &String) -> ?
```
函数 `first_word`着一`&String` 的参数。我们不需要所有权,所以这没有问题。但我们应返回什么呢?我们确实没有描述字符串的 *一部分* 的方法。不过,我们可以返回该单词,以空格表示的结束处的索引。我们来试试看,如下清单 4-7 所示。
函数 `first_word` 有个 `&String` 类型的参数。我们不需要所有权,所以这很好。(在惯用的 Rust 下,除非需要,否则函数不会取得他们参数的所有权,随着我们继续,其原因将变得清晰。)但我们应返回什么呢?我们确实没有一种描述字符串的 *一部分* 的方法。不过,我们可以返回以空格表示的单词结尾的索引。我们来尝试一下,如下清单 4-7 所示。
<a name="listing_4-7"></a>
文件名:`src/main.rs`
```rust
@@ -33,10 +33,9 @@ fn first_word(s: &String) -> usize {
}
```
*清单 4-7返回 `&String` 参数中某个字节索引值的 `first_word` 函数*
**清单 4-7**:返回 `String` 参数中字节索引值的 `first_word` 函数
因为我们需要逐个元素地遍历这个 `String`,并检查某个值是否为空格,所以我们将使用 `as_bytes` 方法,将咱们的 `String` 转换为一个字节数组。
因为我们需要逐个元素地遍历 `String` 并检查值是否为空格,所以我们将使用 `as_bytes` 方法将 `String` 转换为字节数组。
```rust
@@ -44,7 +43,7 @@ fn first_word(s: &String) -> usize {
```
接下来,我们使用 `iter` 方法,在这个字节数组上创建一个迭代器:
接下来,我们使用 `iter` 方法字节数组上创建一个迭代器:
```rust
@@ -52,11 +51,11 @@ fn first_word(s: &String) -> usize {
```
我们将在 [第 13 章](../functional_features/iterator.md) 详细讨论迭代器。现在,我们只需知道 `iter` 是个返回集合中每个元素的方法,而 `enumerate` 封装 `iter` 的结果,而将每个元素作为元组的一部分返回。`enumerate` 返回元组的第一个元素是索引,第二个元素是指向集合元素的引用。这比我们自己计算索引,要方便一
我们将在 [第 13 章](../functional_features/iterator.md) 中更详细讨论迭代器。现在只需了解`iter` 是个返回集合中每个元素的方法,而 `enumerate` 封装 `iter` 的结果将每个元素作为元组的一部分返回。`enumerate` 返回元组的第一个元素是索引,第二个元素是指向元素的引用。这比我们自己计算索引方便一
因为 `enumerate` 方法返回个元组,所以我们可以使用模式来解构这个元组。我们将在 [第 6 章](../enums_and_pattern_matching/match_control_flow.md#绑定到值的模式) 详细讨论模式。在这个 `for` 循环中,我们指定了其中 `i` 表示元组中的索引,`&item` 表示元组中单个字节的模式。因为我们 `.iter().enumerate()`,得到的是个指向集合元素的引用,所以我们在模式中使用 `&`
因为 `enumerate` 方法返回个元组,所以我们可以使用模式来解构元组。我们将在 [第 6 章](../enums_and_pattern_matching/match_control_flow.md#绑定到值的模式) 中更多地详细讨论模式。在 `for` 循环中,我们指定了一种模式,其中 `i` 表示元组中的索引,`&item` 表示元组中单个字节。因为我们得到一个对 `.iter().enumerate()` 中元素的引用,所以我们在模式中使用 `&`
这个 `for` 循环,我们通过使用字节字面值语法检索表示空格的字节。如果我们找到了空格,就返回其位置。否则,我们便通过使用 `s.len()`返回字符串的长度。
`for` 循环,我们通过使用字节字面值语法检索表示空格的字节。我们找到一个空格时,我们就返回其位置。否则,我们通过使用 `s.len()` 返回字符串的长度。
```rust
@@ -69,7 +68,7 @@ fn first_word(s: &String) -> usize {
```
我们现在有了已知找出字符串中第一个单词末尾索引的方法,但有个问题。我们单独返回一个 `usize`,但他只有在那个 `&String` 的上下文中,才是个有意义的数字。换句话说,因为他是个独立于那个 `String` 的值,所以不能保证他在将来仍然有效。请看下面清单 4-8 中的那个程序,他使用清单 4-7 中的 `first_word` 函数。
我们现在有一种办法,找出字符串中第一个单词末尾索引的方法,但有个问题。我们单独返回一个 `usize`,但他只 `&String` 的上下文中才是个有意义的数字。换句话说,因为他是个独立于 `String` 的值,所以不能保证他在将来仍然有效。请看下面清单 4-8 中的程序,他使用清单 4-7 中的 `first_word` 函数。
文件名:`src/main.rs`
@@ -82,34 +81,30 @@ fn main() {
s.clear(); // 这会清空那个 String令其等于 ""
// 这里 word 仍有着值 5已没有咱们可值 5
// 有意义地运用的字符串了。word 现在完全无效!
// word 在这里仍有着值 5 s 已不再有咱们可值 5
// 有意义地运用的任何内容。因此 word 现在完全无效!
}
```
*清单 4-8存储调用 `first_word` 函数的解构,并随后修改那个 `String` 的内容*
*清单 4-8存储调用 `first_word` 函数的结果,然后修改 `String` 的内容*
此程序会不带任何报错编译,即使在我们在调用 `s.clear()` 后使用 `word` 也会如此。由于 `word` 完全未与 `s` 的状态联系起来`word` 仍然包含值 `5`。我们本可以使用`5` 变量 `s`提取出第一个单词,但这将是个错误,因为自从我们`5` 保存`word``s` 的内容已经发生了变化。
这个程序可在没有任何报错编译,并将在我们在调用 `s.clear()` 后使用 `word` 如此行事。因为 `word` 完全没有连接 `s` 的状态,所以 `word` 仍然包含值 `5`。我们本可`5` 变量 `s` 一起使用来提取出第一个单词,但这将是个 bug,因为自从我们在 `word`保存 `5` 以来`s` 的内容已经发生了变化。
担心 `word` 中的索引与 `s` 中的数据不同步,既繁琐又容易出错!如果我们要编写一个 `second_word` 函数,那么管理这些索引就会变得更加棘手。其签名应该是这样的
不得不担心 `word` 中的索引与 `s` 中的数据不同步,既乏味又容易出错!我们要编写 `second_word` 函数,管理这些索引就会更加脆弱。其签名就必须如下
```rust
fn second_word(s: &String) -> (usize, usize) {
```
现在,我们正在跟踪起始索引 ** 结束索引,而且咱们还有更多的值是根据特定状态下的数据计算得出的,却完全与该状态无关。我们有三个需要保持同步的无关变量在游离。
现在,我们要跟踪起始 ** 终止索引,而且咱们还有更多的值,是根据特定状态下的数据计算得出的,但这些值又与该状态完全无关。我们有三个不相关的变量,需要保持同步
幸运的是Rust 有此问题的解决方法:字符串切片。
幸运的是Rust 有一种针对这个问题的解决方法:字符串切片
## 字符串切片
**String Slices**
所谓 *字符串切片*,是对字符串部分内容的引用,他看起来像这样:
所谓 *字符串切片*,是对 `String` 中连续元素序列的引用,他看起来像这样:
```rust
@@ -120,21 +115,21 @@ fn second_word(s: &String) -> (usize, usize) {
```
与对整个 `String` 的引用不同,`hello` 是对这个 `String`,由额外的 `[0..5]` 代码,所指定的一部分的引用。我们通过指 `[starting_index..ending_index]`使用括号内的范围创建切片,其中 `starting_index` 是切片中的第一个位置,`ending_index` 比切片中最后一个位置多一。在内部,切片这种数据结构存储了切片的起始位置和长度, `ending_index` 减去 `starting_index`。因此,在 `let world = &s[6..11];` 的情况下,`world` 将是个包含着指向 `s` 的索引 `6` 处字节指针,长度值为 `5`切片
与对整个 `String` 的引用不同,`hello` 是对 `String`一部分的引用,在额外的 `[0..5]` 部分中指定。我们通过指 `[starting_index..ending_index]`,使用括号内的范围创建切片,其中 `starting_index` 是切片中的第一个位置,`ending_index` 比切片中最后一个位置多一。在内部,切片数据结构存储了切片的起始位置和长度,对应于 `ending_index` 减去 `starting_index`。因此,在 `let world = &s[6..11];` 的情况下,`world` 将是个包含着指向 `s` 的索引 `6` 处字节指针, `5`长度值
> **译注**:切片应是一种灵巧指针。
图 4-6 以图表的形式,展示了这点。
图 4-7 以图展示了这点。
![指向一个 `String` 数据局部的字符串切片](../images/Ch04_06.svg)
*图 4-6指向某个 `String` 的字符串切片*
*图 4-6指向 `String` 的一部分的字符串切片*
使用 Rust 的 `..` 范围语法,如果咱们打算从索引 `0` 处开始,咱们可去掉两个句点前的值。换句话说,下面两个值是相等的:
Rust 的 `..` 范围语法下,当咱们打算从索引 `0` 处开始,咱们可去掉两个句点前的值。换句话说,下面两个值是相等的:
```rust
@@ -145,7 +140,7 @@ fn second_word(s: &String) -> (usize, usize) {
```
对于同一个字符串令牌,如果咱们的片段包括该 `String` 的最后一个字节,则可以去掉两个句点后的尾数。这意味着下面两个值是相等的:
同样的道理,当咱们的切片包含 `String` 的最后一个字节时,咱们可去掉尾随数字。这意味着下面两个值是相等的:
```rust
@@ -157,7 +152,7 @@ fn second_word(s: &String) -> (usize, usize) {
let slice = &s[3..];
```
要取用整个字符串时,还可以把开始与结束索引都舍弃掉。那么下面的语句就是等价的了
咱们还可同时去掉这两个值来取整个字符串的切片。因此,下面两个值是相等的
```rust
let s = String::from("hello");
@@ -168,10 +163,10 @@ fn second_word(s: &String) -> (usize, usize) {
let slice = &s[..];
```
> **注意**:字符串切片范围索引必须出现在有效的 UTF-8 字符边界处。如果咱们试图在某个多字节字符的中间创建字符串片段,咱们的程序将报错退出。为介绍字符串切片目的,我们在本节中假设仅有 ASCII 编码;有关 UTF-8 处理的更全面讨论,请参阅第 8 章的 [使用字符串存储 UTF-8 编码文本](../common_collections/strings.md) 小节
> **注意**:字符串切片范围索引必须出现在有效的 UTF-8 字符边界处。当咱们尝试在多字节字符的中间创建字符串切片时,咱们的程序将以错误退出
这些信息,我们来 `first_word` 重写为返回一个切片。表示 “字符串切片” 的类型,被写作 `&str`
考虑到所有这些信息,我们来重写 `first_word` 为返回一个切片。表示 “字符串切片” 的类型写为 `&str`
文件名:`src/main.rs`
@@ -191,9 +186,9 @@ fn first_word(s: &String) -> &str {
```
我们以与清单 4-7 中相同的方式获取到该单词结尾的索引,即查找第一次出现的空格。当我们找到一个空格时,我们使用该字符串的开头,与这个空格的索引,作为开始和结束索引返回一个字符串切片。
我们以与清单 4-7 中相同的方式获取单词结尾的索引,即查找第一次出现的空格。当我们找到一个空格时,我们使用该字符串的开头空格的索引,作为开始和结束索引返回一个字符串切片。
现在,当我们调用 `first_word` 时,会返回一个与所采用数据相关的值。该值由到切片起点的引用,和切片中元素的数量组成。
现在,当我们调用 `first_word` 时,我们会返回一个与所采用数据相关的值。该值由到切片起点的引用,和切片中元素的数量组成。
对于 `second_word` 函数来说,返回切片也是可行的: