diff --git a/projects/slices/Cargo.toml b/projects/slices/Cargo.toml new file mode 100644 index 0000000..ffd8810 --- /dev/null +++ b/projects/slices/Cargo.toml @@ -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] diff --git a/projects/slices/src/main.rs b/projects/slices/src/main.rs new file mode 100644 index 0000000..b75ff75 --- /dev/null +++ b/projects/slices/src/main.rs @@ -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); +} diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 9f24a30..3167e42 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -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) diff --git a/src/ownership/the_slice_type.md b/src/ownership/the_slice_type.md index e4fe4a7..906d9f4 100644 --- a/src/ownership/the_slice_type.md +++ b/src/ownership/the_slice_type.md @@ -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 字符串的更全面讨论。