Refining Ch04.

This commit is contained in:
rust-lang.xfoss.com 2023-12-13 15:41:03 +08:00
parent c0d1fe7d9d
commit d850f94421
4 changed files with 91 additions and 46 deletions

View File

@ -0,0 +1,8 @@
[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

@ -0,0 +1,10 @@
fn main() {
let s = String::from("hello");
let len = s.len();
let slice_1 = &s[3..len];
let slice_2 = &s[3..];
assert_eq! (slice_1, slice_2);
}

View File

@ -90,7 +90,7 @@
- [函数式语言特性:迭代器与闭包](Ch13_Functional_Language_Features_Iterators_and_Closures.md)
- [闭包:会捕获其环境的匿名函数](functional_features/closures.md)
- [使用迭代器处理条目序列](functional_features/iterator.md)
- [使用迭代器处理条目序列](functional_features/iterators.md)
- [改进咱们的 I/O 项目](functional_features/improving_io_project.md)
- [性能比较:循环与迭代器](functional_features/performance.md)

View File

@ -3,17 +3,19 @@
**The Slice Type**
*切片slices* 特性,实现了对集合中一个连续元素序列,而非对整个集合的引用。切片是引用的一种类别,因此他不会持有所有权。
*切片slices* 允许咱们引用某个集合中,连续的元素序列,而不是整个集合。切片属于一种引用,因此他不具有所有权。
这里有个小的编程问题:编写一个取得字符串,而返回在那个字符串中找到的第一个单词的函数。在函数在那个字符串中未找到空格时,那么这整个字符串就一定是一个单词,因此就要返回这整个字符串了。
这里有一个编程小问题:要编写一个,取个以空格分隔单词的字符串,并返回其在该字符串中,找到的第一个单词的函数。如果该函数在字符串中,未找到空格,那么整个字符串必定是一个单词,所以这整个字符串就应被返回。
我们来看看,在不使用切片的情况下,咱们要如何编写这个函数的签名,以了解切片将解决什么问题:
下面就要在不使用切片特性的情况下,来看看该怎么编写这个函数的签名,从而搞明白切片要解决的问题:
```rust
fn first_word(s: &String) -> ?
```
这个 `first_word` 函数,有着一个作为参数的 `&String` 类型。这里不想要所有权,因此这是没问题的。不过应该返回什么呢?这里实在没有一种描述字符串 *局部part* 的方式。不过,这里可以返回那个单词的、以一个空格表示的结尾的索引。先来试试这个,如下面清单 4-7 所示:
函数 `first_word` 有着一个 `&String` 的参数。我们不需要所有权,所以这没有问题。但我们应返回什么呢?我们确实没有描述字符串的 *一部分* 的方法。不过,我们可以返回该单词,以空格表示的结束处的索引。我们来试试看,如下清单 4-7 所示。
文件名:`src/main.rs`
@ -31,25 +33,31 @@ 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
let bytes = s.as_bytes();
let bytes = s.as_bytes();
```
接着,这里使用 `iter` 方法,创建了一个在该字节数组上的迭代器:
接下来,我们使用 `iter` 方法,在这个字节数组上,创建了一个迭代器:
```rust
for (i, &item) in bytes.iter().enumerate() {
for (i, &item) in bytes.iter().enumerate() {
```
在第 13 章,将讨论到迭代器的更多细节。而现在,明白 `iter` 是个返回集合中各个元素的方法,而那个 `enumerate` 则会将 `iter` 的结果进行封装进而将各个元素作为一个元组的组成部分,进行返回即可。自 `enumerate` 返回的元组第一个元素就是索引值,而第二个元素,则是到 `iter` 返回元素的索引。相比由代码编写者自己计算索引,这就要方便一点。
由于 `enumerate` 方法返回了一个元组,因此这里就可以使用模式,来解构那个元组。在 [第 6 章](Ch06_Enums_and_Pattern_Matching.md#绑定到值的模式),会对模式进行更多讨论。在那个 `for` 循环中,指定了一个有着用于那个元组中索引的 `i`,以及用于那个元组中单个字节的 `&item` 的模式。由于这里获得的是一个到从 `.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()` 中,得到的是个指向集合元素的引用,所以我们在模式中,使用了 `&`
在这个 `for` 循环中,我们通过使用字节字面值语法,检索表示空格的字节。如果我们找到了空格,就返回其位置。否则,我们便通过使用 `s.len()`,返回该字符串的长度。
在那个 `for` 循环内部这里通过使用字节字面值语法the byte literal syntax就表示空格的字节进行了搜索。在找到空格时就返回空格的位置。否则就通过使用 `s.len()` 返回该字符串的长度。
```rust
if item == b' ' {
@ -60,7 +68,9 @@ for (i, &item) in bytes.iter().enumerate() {
s.len()
```
现在就有了一种找出字符串中第一个单词末尾索引的方法了,不过这里有个问题。这里所返回的只是个 `usize`,然而这个返回值只是在 `&String` 的语境下,才是个有意义的数字。也就是说,由于这个返回的 `usize` 类型值,是从那个 `String` 值获取到的孤立值,因此就没办法保证在以后仍然有效。关于这点,可考虑在清单 4-8 中、用到了清单 4-7 中 `first_word` 函数的这么一个程序。
我们现在有了已知找出字符串中,第一个单词末尾索引的方法,但有个问题。我们单独返回一个 `usize`,但他只有在那个 `&String` 的上下文中,才是个有意义的数字。换句话说,因为他是个独立于那个 `String` 的值,所以不能保证他在将来仍然有效。请看下面清单 4-8 中的那个程序,他使用了清单 4-7 中的 `first_word` 函数。
文件名:`src/main.rs`
@ -68,77 +78,94 @@ for (i, &item) in bytes.iter().enumerate() {
fn main() {
let mut s = String::from("The quick brown fox jumps over the lazy dog.");
let word = first_word(&s); // 变量 word 将得值 5
let word = first_word(&s); // word 将得值 5
s.clear(); // 这个语句会清空该字符串,令其等于 ""
s.clear(); // 这会清空那个 String,令其等于 ""
// 到这里变量 word 仍有着值 5但已经不再有那个可将值 5 有意义的运用
// 到的字符串了。变量 5 现在完全无用了
// 这里 word 仍有着值 5但已没有咱们可将值 5
// 有意义地运用的字符串了。word 现在完全无效
}
```
*清单 4-8将来自调用 `first_word` 函数的结果存储起来,并在随后修改那个 `String`的内容*
*清单 4-8存储调用 `first_word` 函数的解构,并随后修改那个 `String` 的内容*
该程序会不带任何错误地编译,且同样会在调用了 `s.clear()`后使用变量 `word`其仍会完成后续执行this program compiles without any errors and would do so if we used `word` after calling `s.clear()`)。由于变量 `word` 完全未被连接到变量 `s` 的状态,因此变量 `word` 仍包含着值 `5`。这里仍可使用那个值 `5` 与变量 `s`,来尝试提取出第一个单词,但由于自将值 `5` 保存在 `word` 中以来,变量 `s` 的内容已被修改因此这样做将是个程序错误a bug
此程序会不带任何报错地编译,即使在我们在调用 `s.clear()` 之后使用 `word` 也会如此。由于 `word` 完全未与 `s` 的状态联系起来,`word` 仍然包含值 `5`。我们本可以使用值 `5` 于变量 `s`,提取出第一个单词,但这将是个错误,因为自从我们将 `5` 保存在 `word` 中后,`s` 的内容已经发生了变化。
要担心 `word` 中的索引与 `s` 中的数据不同步,既繁琐又容易出错!如果我们要编写一个 `second_word` 函数,那么管理这些索引,就会变得更加棘手。其签名应该是这样的:
这种不可避免的担心变量 `word` 中的索引,失去与变量 `s` 中的数据同步,就会十分烦人且容易发生错误!而在要编写 `second_word` 函数时,对这些索引的管理,将更加脆弱。`second_word` 的函数签名,将务必看起来像下面这样:
```rust
fn second_word(s: &String) -> (usize, usize) {
```
现在就得对一个开始 *和* 结束索引保持跟踪,同时甚至还有更多的、要从特定状态中的数据计算出的值,而这些值又完全没有与那种状态联系起来。这样就有了三个无关的、需要同步保持的变量漂浮着。
幸运的是Rust 有此问题的解决办法那就是字符串切片string slices
现在,我们要跟踪起始 *和* 终止索引,而且咱们还有更多的值,是根据特定状态下的数据计算得出的,但这些值又与该状态完全无关。我们有三个不相关的变量,需要保持同步。
幸运的是Rust 有此问题的解决方法:字符串切片。
## 字符串切片
字符串切片是到某个 `String` 类型值部分的引用,而看起来像下面这样:
**String Slices**
所谓 *字符串切片*,是对字符串部分内容的引用,他看起来像这样:
```rust
let s = String::from("The quick brown fox jumps over the lazy dog.");
let s = String::from("hello world");
let the = &s[0..3];
let quick = &s[4..9];
let hello = &s[0..5];
let world = &s[6..11];
```
与到整个 `String` 值的引用 `&s` 不同,`the` 是到这个 `String` 的,在那个附加 `[0..3]` 中所指明的一部分的引用。通过指定 `[start_index..ending_index]`,而使用了在一对方括号里的一个范围,这里创建出了切片,其中的 `starting_index` 是切片中首个位置,而 `ending_index` 则是比切片中最后位置多一的位置索引。切片数据结构内部,存储着开始位置与该切片的长度,长度即 `ending_index` 减去 `starting_index`。那么在示例 `let quick = &s[4..9];` 中,`quick` 就会包含一个到变量 `s` 的索引 `4` 处字节的指针。
下图 4-6 展示对此进行了展示。
与对整个 `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 以图表的形式,展示了这一点。
![指向一个 `String` 数据局部的字符串切片](images/Ch04_06.svg)
*图 4-6指向一个 `String` 数据局部的字符串切片*
*图 4-6指向某个 `String` 的字符串切片*
在 Rust 的 `..` 范围语法the `..` range syntax 之下,在希望于索引为零处开始时,那么就可以舍弃那两个点之前的值。也就是说,写开始索引 `0` 与不写,是等价的:
```
let s = String::from("hello");
使用 Rust 的 `..` 范围语法,如果咱们打算从索引 `0` 处开始,咱们可以去掉两个句点前的值。换句话说,下面这两个值是相等的:
let slice = &s[0..2];
let slice = &s[..2];
```
对同一个字符串令牌,在切片包含了那个 `String` 的最后字节时,那么就可以舍弃那结尾的数字。即意味着下面的语句是等价的:
```rust
let s = String::from("hello");
let s = String::from("hello");
let len = s.len();
let slice = &s[0..2];
let slice = &s[..2];
```
let slice = &s[3..len];
let slice = &s[3..];
对于同一个字符串令牌,如果咱们的片段包括该 `String` 的最后一个字节,则可以去掉两个句点后的尾数。这意味着下面两个值是相等的:
```rust
let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];
```
要取用整个字符串时,还可以把开始与结束索引都舍弃掉。那么下面的语句就是等价的了:
```rust
let s = String::from("hello");
let s = String::from("hello");
let len = s.len();
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];
let slice = &s[0..len];
let slice = &s[..];
```
> **注意**:这些字符串切片的范围索引值,必须出现于有效的 UTF-8 字符边界处。若在 UTF-8 多字节字符中间,尝试创建字符串切片,那么程序就会以错误退出。这里只是为介绍字符串切片目的,而假定本小节中只使用 ASCII 字符;在第 8 章的 [“以 `String` 类型值存储 UTF-8 编码的文本”](Ch08_Common_Collections.md#使用-string-存储-utf-8-编码的文本) 小节,有着对 UTF-8 字符串的更全面讨论。