|
|
|
@ -2,34 +2,24 @@
|
|
|
|
|
|
|
|
|
|
> [ch04-03-slices.md](https://github.com/rust-lang/book/blob/main/src/ch04-03-slices.md)
|
|
|
|
|
> <br>
|
|
|
|
|
> commit d377d2effa9ae9173026e35a7e464b8f5b82409a
|
|
|
|
|
> commit a5e0c5b2c5f9054be3b961aea2c7edfeea591de8
|
|
|
|
|
|
|
|
|
|
另一个没有所有权的数据类型是 *slice*。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。
|
|
|
|
|
*slice* 允许你引用集合中一段连续的元素序列,而不用引用整个集合。slice 是一类引用,所以它没有所有权。
|
|
|
|
|
|
|
|
|
|
这里有一个编程小习题:编写一个函数,该函数接收一个字符串,并返回在该字符串中找到的第一个单词。如果函数在该字符串中并未找到空格,则整个字符串就是一个单词,所以应该返回整个字符串。
|
|
|
|
|
|
|
|
|
|
让我们考虑一下这个函数的签名:
|
|
|
|
|
让我们推敲下如何不用 slice 编写这个函数的签名,来理解 slice 能解决的问题:
|
|
|
|
|
|
|
|
|
|
```rust,ignore
|
|
|
|
|
fn first_word(s: &String) -> ?
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`first_word` 函数有一个参数 `&String`。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取 **部分** 字符串的办法。不过,我们可以返回单词结尾的索引。试试如示例 4-7 中的代码。
|
|
|
|
|
`first_word` 函数有一个参数 `&String`。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取 **部分** 字符串的办法。不过,我们可以返回单词结尾的索引,结尾由一个空格表示。试试如示例 4-7 中的代码。
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名: src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-07/src/main.rs:here}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 4-7:`first_word` 函数返回 `String` 参数的一个字节索引值</span>
|
|
|
|
@ -37,28 +27,23 @@ fn first_word(s: &String) -> usize {
|
|
|
|
|
因为需要逐个元素的检查 `String` 中的值是否为空格,需要用 `as_bytes` 方法将 `String` 转化为字节数组:
|
|
|
|
|
|
|
|
|
|
```rust,ignore
|
|
|
|
|
let bytes = s.as_bytes();
|
|
|
|
|
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-07/src/main.rs:as_bytes}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
接下来,使用 `iter` 方法在字节数组上创建一个迭代器:
|
|
|
|
|
|
|
|
|
|
```rust,ignore
|
|
|
|
|
for (i, &item) in bytes.iter().enumerate() {
|
|
|
|
|
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-07/src/main.rs:iter}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
我们将在第十三章详细讨论迭代器。现在,只需知道 `iter` 方法返回集合中的每一个元素,而 `enumerate` 包装了 `iter` 的结果,将这些元素作为元组的一部分来返回。`enumerate` 返回的元组中,第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。
|
|
|
|
|
我们将在[第十三章][ch13]详细讨论迭代器。现在,只需知道 `iter` 方法返回集合中的每一个元素,而 `enumerate` 包装了 `iter` 的结果,将这些元素作为元组的一部分来返回。`enumerate` 返回的元组中,第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。
|
|
|
|
|
|
|
|
|
|
因为 `enumerate` 方法返回一个元组,我们可以使用模式来解构,我们将在第 6 章中进一步讨论有关模式的问题。所以在 `for` 循环中,我们指定了一个模式,其中元组中的 `i` 是索引而元组中的 `&item` 是单个字节。因为我们从 `.iter().enumerate()` 中获取了集合元素的引用,所以模式中使用了 `&`。
|
|
|
|
|
因为 `enumerate` 方法返回一个元组,我们可以使用模式来解构,我们将在[第六章][ch6]中进一步讨论有关模式的问题。所以在 `for` 循环中,我们指定了一个模式,其中元组中的 `i` 是索引而元组中的 `&item` 是单个字节。因为我们从 `.iter().enumerate()` 中获取了集合元素的引用,所以模式中使用了 `&`。
|
|
|
|
|
|
|
|
|
|
在 `for` 循环中,我们通过字节的字面值语法来寻找代表空格的字节。如果找到了一个空格,返回它的位置。否则,使用 `s.len()` 返回字符串的长度:
|
|
|
|
|
|
|
|
|
|
```rust,ignore
|
|
|
|
|
if item == b' ' {
|
|
|
|
|
return i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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` 函数的程序。
|
|
|
|
@ -66,28 +51,7 @@ s.len()
|
|
|
|
|
<span class="filename">文件名: src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
# 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() {
|
|
|
|
|
let mut s = String::from("hello world");
|
|
|
|
|
|
|
|
|
|
let word = first_word(&s); // word 的值为 5
|
|
|
|
|
|
|
|
|
|
s.clear(); // 这清空了字符串,使其等于 ""
|
|
|
|
|
|
|
|
|
|
// word 在此处的值仍然是 5,
|
|
|
|
|
// 但是没有更多的字符串让我们可以有效地应用数值 5。word 的值现在完全无效!
|
|
|
|
|
}
|
|
|
|
|
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-08/src/main.rs:here}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 4-8:存储 `first_word` 函数调用的返回值并接着改变 `String` 的内容</span>
|
|
|
|
@ -109,15 +73,10 @@ fn second_word(s: &String) -> (usize, usize) {
|
|
|
|
|
**字符串 slice**(*string slice*)是 `String` 中一部分值的引用,它看起来像这样:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
let s = String::from("hello world");
|
|
|
|
|
|
|
|
|
|
let hello = &s[0..5];
|
|
|
|
|
let world = &s[6..11];
|
|
|
|
|
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-17-slice/src/main.rs:here}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这类似于引用整个 `String` 不过带有额外的 `[0..5]` 部分。它不是对整个 `String` 的引用,而是对部分 `String` 的引用。
|
|
|
|
|
|
|
|
|
|
可以使用一个由中括号中的 `[starting_index..ending_index]` 指定的 range 创建一个 slice,其中 `starting_index` 是 slice 的第一个位置,`ending_index` 则是 slice 最后一个位置的后一个值。在其内部,slice 的数据结构存储了 slice 的开始位置和长度,长度对应于 `ending_index` 减去 `starting_index` 的值。所以对于 `let world = &s[6..11];` 的情况,`world` 将是一个包含指向 `s` 索引 6 的指针和长度值 5 的 slice。
|
|
|
|
|
不同于整个 `String` 的引用,`hello` 是一个部分 `String` 的引用,由一个额外的 `[0..5]` 部分指定。可以使用一个由中括号中的 `[starting_index..ending_index]` 指定的 range 创建一个 slice,其中 `starting_index` 是 slice 的第一个位置,`ending_index` 则是 slice 最后一个位置的后一个值。在其内部,slice 的数据结构存储了 slice 的开始位置和长度,长度对应于 `ending_index` 减去 `starting_index` 的值。所以对于 `let world = &s[6..11];` 的情况,`world` 将是一个包含指向 `s` 索引 6 的指针和长度值 5 的 slice。
|
|
|
|
|
|
|
|
|
|
图 4-6 展示了一个图例。
|
|
|
|
|
|
|
|
|
@ -163,17 +122,7 @@ let slice = &s[..];
|
|
|
|
|
<span class="filename">文件名: src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
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[..]
|
|
|
|
|
}
|
|
|
|
|
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-18-first-word-slice/src/main.rs:here}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
我们使用跟示例 4-7 相同的方式获取单词结尾的索引,通过寻找第一个出现的空格。当找到一个空格,我们返回一个字符串 slice,它使用字符串的开始和空格的索引作为开始和结束的索引。
|
|
|
|
@ -191,36 +140,13 @@ fn second_word(s: &String) -> &str {
|
|
|
|
|
<span class="filename">文件名: src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
```rust,ignore,does_not_compile
|
|
|
|
|
fn main() {
|
|
|
|
|
let mut s = String::from("hello world");
|
|
|
|
|
|
|
|
|
|
let word = first_word(&s);
|
|
|
|
|
|
|
|
|
|
s.clear(); // 错误!
|
|
|
|
|
|
|
|
|
|
println!("the first word is: {}", word);
|
|
|
|
|
}
|
|
|
|
|
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-19-slice-error/src/main.rs:here}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这里是编译错误:
|
|
|
|
|
|
|
|
|
|
```console
|
|
|
|
|
$ 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` due to previous error
|
|
|
|
|
{{#include ../listings/ch04-understanding-ownership/no-listing-19-slice-error/output.txt}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
回忆一下借用规则,当拥有某值的不可变引用时,就不能再获取一个可变引用。因为 `clear` 需要清空 `String`,它尝试获取一个可变引用。在调用 `clear` 之后的 `println!` 使用了 `word` 中的引用,所以这个不可变的引用在此时必须仍然有效。Rust 不允许 `clear` 中的可变引用和 `word` 中的不可变引用同时存在,因此编译失败。Rust 不仅使得我们的 API 简单易用,也在编译时就消除了一整类的错误!
|
|
|
|
@ -246,7 +172,7 @@ fn first_word(s: &String) -> &str {
|
|
|
|
|
而更有经验的 Rustacean 会编写出示例 4-9 中的签名,因为它使得可以对 `String` 值和 `&str` 值使用相同的函数:
|
|
|
|
|
|
|
|
|
|
```rust,ignore
|
|
|
|
|
fn first_word(s: &str) -> &str {
|
|
|
|
|
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-09/src/main.rs:here}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 4-9: 通过将 `s` 参数的类型改为字符串 slice 来改进 `first_word` 函数</span>
|
|
|
|
@ -256,32 +182,7 @@ fn first_word(s: &str) -> &str {
|
|
|
|
|
<span class="filename">文件名: src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
# 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 中传入 `String` 的 slice
|
|
|
|
|
let word = first_word(&my_string[..]);
|
|
|
|
|
|
|
|
|
|
let my_string_literal = "hello world";
|
|
|
|
|
|
|
|
|
|
// first_word 中传入字符串字面值的 slice
|
|
|
|
|
let word = first_word(&my_string_literal[..]);
|
|
|
|
|
|
|
|
|
|
// 因为字符串字面值 **就是** 字符串 slice,
|
|
|
|
|
// 这样写也可以,即不使用 slice 语法!
|
|
|
|
|
let word = first_word(my_string_literal);
|
|
|
|
|
}
|
|
|
|
|
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-09/src/main.rs:usage}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 其他类型的 slice
|
|
|
|
@ -310,5 +211,7 @@ assert_eq!(slice, &[2, 3]);
|
|
|
|
|
|
|
|
|
|
所有权系统影响了 Rust 中很多其他部分的工作方式,所以我们还会继续讲到这些概念,这将贯穿本书的余下内容。让我们开始第五章,来看看如何将多份数据组合进一个 `struct` 中。
|
|
|
|
|
|
|
|
|
|
[ch13]: ch13-02-iterators.html
|
|
|
|
|
[ch6]: ch06-02-match.html#绑定值的模式
|
|
|
|
|
[strings]: ch08-02-strings.html#使用字符串存储-utf-8-编码的文本
|
|
|
|
|
[deref-coercions]: ch15-02-deref.html#函数和方法的隐式-deref-强制转换
|
|
|
|
|
[deref-coercions]: ch15-02-deref.html#函数和方法的隐式-deref-强制转换
|
|
|
|
|