mirror of
https://github.com/gnu4cn/rust-lang-zh_CN.git
synced 2024-12-27 13:21:09 +08:00
Refined Ch04.
This commit is contained in:
parent
d850f94421
commit
d82f9e9a76
@ -1,10 +1,20 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
let s = String::from("hello");
|
let a = [1, 2, 3, 4, 5];
|
||||||
|
|
||||||
let len = s.len();
|
let slice = &a[1..3];
|
||||||
|
|
||||||
let slice_1 = &s[3..len];
|
assert_eq! (slice, &[2, 3]);
|
||||||
let slice_2 = &s[3..];
|
}
|
||||||
|
|
||||||
assert_eq! (slice_1, slice_2);
|
|
||||||
|
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[..]
|
||||||
}
|
}
|
||||||
|
@ -129,7 +129,7 @@ fn second_word(s: &String) -> (usize, usize) {
|
|||||||
下图 4-6 以图表的形式,展示了这一点。
|
下图 4-6 以图表的形式,展示了这一点。
|
||||||
|
|
||||||
|
|
||||||
![指向一个 `String` 数据局部的字符串切片](images/Ch04_06.svg)
|
![指向一个 `String` 数据局部的字符串切片](../images/Ch04_06.svg)
|
||||||
|
|
||||||
*图 4-6:指向某个 `String` 的字符串切片*
|
*图 4-6:指向某个 `String` 的字符串切片*
|
||||||
|
|
||||||
@ -168,10 +168,11 @@ fn second_word(s: &String) -> (usize, usize) {
|
|||||||
let slice = &s[..];
|
let slice = &s[..];
|
||||||
```
|
```
|
||||||
|
|
||||||
> **注意**:这些字符串切片的范围索引值,必须出现于有效的 UTF-8 字符边界处。若在 UTF-8 多字节字符中间,尝试创建字符串切片,那么程序就会以错误退出。这里只是为介绍字符串切片目的,而假定本小节中只使用 ASCII 字符;在第 8 章的 [“以 `String` 类型值存储 UTF-8 编码的文本”](Ch08_Common_Collections.md#使用-string-存储-utf-8-编码的文本) 小节,有着对 UTF-8 字符串的更全面讨论。
|
> **注意**:字符串切片的范围索引,必须出现在有效的 UTF-8 字符边界处。如果咱们试图在某个多字节字符的中间创建字符串片段,咱们的程序将报错退出。为介绍字符串切片目的,我们在本节中假设仅有 ASCII 编码;有关 UTF-8 处理的更全面讨论,请参阅第 8 章的 [使用字符串存储 UTF-8 编码文本](../common_collections/strings.md) 小节。
|
||||||
|
|
||||||
|
|
||||||
对这全部字符串切片的情况了然在胸,那么下面就来将 `first_word` 重写为返回切片。表示 “字符串切片” 的类型,写做 `&str`:
|
有了这些信息,我们来将 `first_word` 重写为返回一个切片。表示 “字符串切片” 的类型,被写作 `&str`:
|
||||||
|
|
||||||
|
|
||||||
文件名:`src/main.rs`
|
文件名:`src/main.rs`
|
||||||
|
|
||||||
@ -189,54 +190,58 @@ fn first_word(s: &String) -> &str {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
这里是以前面清单 4-7 中所做的同样方式,即查找首次出现的空格,而获取到该单词结束处的索引。在找到空格时,就运用该字符串的开头,与那个空格的索引,作为字符串切片开始与结束索引,而返回一个字符串切片。
|
|
||||||
|
|
||||||
现在当调用 `first_word` 函数时,取回的便是与所用 `String` 数据联系起来单个值。这个值是由到切片起点的引用,与切片中元素个数所组成。
|
我们以与清单 4-7 中,相同的方式获取到该单词结尾的索引,即查找第一次出现的空格。当我们找到一个空格时,我们使用该字符串的开头,与这个空格的索引,作为开始和结束索引,返回一个字符串切片。
|
||||||
|
|
||||||
|
现在,当我们调用 `first_word` 时,就会返回一个与所采用数据相关的值。该值由到切片起点的引用,和切片中元素的数量组成。
|
||||||
|
|
||||||
|
对于 `second_word` 函数来说,返回切片也是可行的:
|
||||||
|
|
||||||
这样返回切片,对于 `second_word` 函数,也是有效的:
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn second_word(s: &String) -> &str {
|
fn second_word(s: &String) -> &str {
|
||||||
```
|
```
|
||||||
|
|
||||||
由于编译器将确保到那个 `String` 数据中引用保持有效,因此现在就有了一个简单的、相比之前那个不那么容易搞混的 API 了。还记得在清单 4-8 中那个程序里的错误吧,即那个在已经获取到首个单词结束位置的索引,而随后清除了那个字符串,因此得到的索引就不在有效的问题。那段代码虽然逻辑上不正确,但也不会立即给出什么错误来。若继续尝试使用空字符串上的首个单词结束索引,这些问题仍会出现。切片就令到这个代码错误不可能了,并实现了更快发现代码问题。使用切片版本的 `first_word` 函数,就会抛出一个编译时错误:
|
现在,我们有了一个简单明了的 API,而且更难出错,因为编译器会确保对那个 `String` 的引用保持有效。还记得清单 4-8 中程序的错误吗?当时我们得到了第一个单词末尾的索引,但随后又清除了那个字符串,因此咱们索引就无效来了。那段代码在逻辑上是错误的,但并没有立即给出任何错误。如果我们继续尝试对某个清空的字符串,使用第一个单词的索引,那么该问题就会在稍后出现。而切片则不会出现这种错误,并能让我们更早地知道,咱们代码出现了问题。使用切片版本的 `first_word` 会抛出一个编译时报错:
|
||||||
|
|
||||||
|
|
||||||
文件名:`src/main.rs`
|
文件名:`src/main.rs`
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut s = String::from("The quick brown fox jumps over the lazy dog.");
|
let mut s = String::from("hello world");
|
||||||
|
|
||||||
let word = first_word(&s);
|
let word = first_word(&s);
|
||||||
|
|
||||||
s.clear();
|
s.clear(); // error!
|
||||||
|
|
||||||
println! ("首个单词为:{}", word);
|
println!("the first word is: {}", word);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
下面就是那个编译器错误消息:
|
|
||||||
|
下面是那个编译器报错:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ cargo run ✔
|
$ cargo run
|
||||||
Compiling ownership_demo v0.1.0 (/home/peng/rust-lang/projects/ownership_demo)
|
Compiling slices v0.1.0 (C:\tools\msys64\home\Lenny.Peng\rust-lang-zh_CN\projects\slices)
|
||||||
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
|
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
|
||||||
--> src/main.rs:6:5
|
--> src\main.rs:6:5
|
||||||
|
|
|
|
||||||
4 | let word = first_word(&s);
|
4 | let word = first_word(&s);
|
||||||
| -- immutable borrow occurs here
|
| -- immutable borrow occurs here
|
||||||
5 |
|
5 |
|
||||||
6 | s.clear();
|
6 | s.clear(); // error!
|
||||||
| ^^^^^^^^^ mutable borrow occurs here
|
| ^^^^^^^^^ mutable borrow occurs here
|
||||||
7 |
|
7 |
|
||||||
8 | println! ("首个单词为:{}", word);
|
8 | println!("the first word is: {}", word);
|
||||||
| ---- immutable borrow later used here
|
| ---- immutable borrow later used here
|
||||||
|
|
||||||
For more information about this error, try `rustc --explain E0502`.
|
For more information about this error, try `rustc --explain E0502`.
|
||||||
error: could not compile `ownership_demo` due to previous error
|
error: could not compile `slices` (bin "slices") due to previous error
|
||||||
```
|
```
|
||||||
|
|
||||||
回顾借用规则,在有着到某数据的不可变引用时,就不能同时有可变引用。由于 `clear` 方法需要清空那个 `String` 值,那么就需要得到一个可变引用。而在 `clear` 方法调用之后的 `println!`,用到了变量 `word` 里的引用,那么这个不可变引用于那个时刻,就必将仍是活跃的。Rust 不允许 `clear` 中的可变引用,与 `word` 中的不可变引用同时存在,进而编译失败。可以看出,Rust 不光令到这个 `first_word` 的 API 更易于使用,他还在运行时就消除了这一整类错误!
|
回顾一下借用规则,如果我们有个不可变的引用,我们就不能同时取得一个可变的引用。因为 `clear` 需要截断这个 `String`,所以他需要得到一个可变引用。`clear` 调用后的那个 `println!`,使用了 `word` 中的引用,因此这个不可变引用,在此时必须仍然有效。Rust 不允许 `clear` 中的可变引用,和 `word` 中的不可变引用同时存在,因此编译会失败。Rust 不仅让我们的 API 更易于使用,还消除了编译时的一整类报错!
|
||||||
|
|
||||||
|
|
||||||
### 作为切片的字符串字面值
|
### 作为切片的字符串字面值
|
||||||
@ -244,13 +249,15 @@ error: could not compile `ownership_demo` due to previous error
|
|||||||
**String Literals as Slices**
|
**String Literals as Slices**
|
||||||
|
|
||||||
|
|
||||||
还记得前面讲到过,那些硬编码的、存储在二进制可执行文件内部的字符串字面值吧。现在了解了切片,那么就可以很好理解字符串字面值了:
|
回想一下,我们曾讲到过的存储在二进制文件中的字符串字面值。现在我们知道了分片,我们就能正确理解字符串字面值了:
|
||||||
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let s = "Hello, world!";
|
let s = "Hello, world!";
|
||||||
```
|
```
|
||||||
|
|
||||||
这里的变量 `s` 的类型,即为 `&str`:他是个指向到二进制文件特殊点位的一个切片。这也是为何字符串字面值为不可变的原因;`&str` 类型属于不可变引用。
|
|
||||||
|
这里 `s` 的类型,就是 `&str`:他是一个指向二进制中特定点的切片。这也是字符串字面值不可变的原因;`&str` 是个不可变引用。
|
||||||
|
|
||||||
|
|
||||||
### 作为参数的字符串切片
|
### 作为参数的字符串切片
|
||||||
@ -258,23 +265,28 @@ let s = "Hello, world!";
|
|||||||
**String Slices as Parameters**
|
**String Slices as Parameters**
|
||||||
|
|
||||||
|
|
||||||
了解了咱们可在函数中,取字符串字面值的切片及 `String` 值,就引出了对 `first_word` 函数的又一项改进,而下面就是函数 `first_word` 的签名:
|
明白咱们可以取字符串字面值和 `String` 值的切片后,我们就可以对 `first_word` 进行另一项改进,那就是他的签名:
|
||||||
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn first_word(s: &String) -> &str {
|
fn first_word(s: &String) -> &str {
|
||||||
```
|
```
|
||||||
|
|
||||||
更老道的 Rust 公民将把这个函数签名,写着像下面清单 4-9 中所展示的那样,这是因为下面这样写,就实现了在 `&String` 与 `&str` 两种类型值上,可使用同一个函数:
|
|
||||||
|
更有经验的 Rustacean,会写下下面清单 4-9 中的签名,因为他允许我们,对 `&String` 值和 `&str` 值,使用同一个函数。
|
||||||
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn first_word(s: &str) -> &str {
|
fn first_word(s: &str) -> &str {
|
||||||
```
|
```
|
||||||
|
|
||||||
*清单 4-9:通过对 `s` 参数的类型使用字符串切片,对 `first_word` 函数进行改进*
|
*清单 4-9:通过对 `s` 参数的类型使用字符串切片,改进这个 `first_word` 函数*
|
||||||
|
|
||||||
在咱们有着某个字符串切片时,那么就可以直接传递那个字符串切片。而在咱们有着一个 `String` 时,则可传递该 `String` 的切片,或到这个 `String` 的引用。这种灵活性,是利用了 *强制引用解除,deref coercions* 特性,在第 15 章的 [函数与方法下的隐式强制解引用](Ch05_Smart_Pointers.md#函数与方法下的隐式解引用强制转换) 小节,将讲到的一种特性。
|
|
||||||
|
|
||||||
这样定义出取字符串切片,而非到 `String` 值引用做参数的函数,令到这个 API 在不丢失任何功能的情况下,变得更为通用和有用:
|
如果我们有个字符串切片,我们可以直接传递他。如果我们有个 `String`,我们可以这个 `String` 切片,或到这个 `String` 的某个引用。这种灵活性,利用了我们将在第 15 章 [函数和方法中的隐式解引用强制转换](../smart_pointers/deref-t.md#函数与方法下的隐式解引用强制转换) 小节中,介绍的 *解引用强制转换* 特性。
|
||||||
|
|
||||||
|
定义一个取字符串切片,而非到某个 `String` 的引用的函数,使得我们的 API 更为通用和实用,而不会丢失任何功能:
|
||||||
|
|
||||||
|
|
||||||
文件名:`src/main.rs`
|
文件名:`src/main.rs`
|
||||||
|
|
||||||
@ -282,47 +294,42 @@ fn first_word(s: &str) -> &str {
|
|||||||
fn main() {
|
fn main() {
|
||||||
let s = String::from("The quick brown fox jumps over the lazy dog.");
|
let s = String::from("The quick brown fox jumps over the lazy dog.");
|
||||||
|
|
||||||
// 函数 first_word 在 String 值的切片上有效,不管是部分还是全部的切片
|
// `first_word` 会在 String 的切片上工作,不管是部分还是整个 String
|
||||||
let word = first_word(&s[0..6]);
|
let word = first_word(&s[0..6]);
|
||||||
println! ("{}", word);
|
|
||||||
|
|
||||||
let word = first_word(&s[..]);
|
let word = first_word(&s[..]);
|
||||||
println! ("{}", word);
|
|
||||||
|
|
||||||
// 函数 first_word 还在 String 变量的引用上有效,而 String 变量的引用
|
// `first_word` 还对 String 的引用有效,这与 String 的整个切片等价
|
||||||
// 与 String 值的整个切片是等价的
|
|
||||||
let word = first_word(&s);
|
let word = first_word(&s);
|
||||||
println! ("{}", word);
|
|
||||||
|
|
||||||
let s_string_literal = "hello word";
|
let s_string_literal = "hello word";
|
||||||
|
|
||||||
// 函数 first_word 在字符串字面值上有效,不论是部分还是整体
|
// `first_word` 在字符串字面值上有效,不论部分还是整体
|
||||||
let word = first_word(&s_string_literal[0..6]);
|
let word = first_word(&s_string_literal[0..6]);
|
||||||
println! ("{}", word);
|
|
||||||
|
|
||||||
let word = first_word(&s_string_literal[..]);
|
let word = first_word(&s_string_literal[..]);
|
||||||
println! ("{}", word);
|
|
||||||
|
|
||||||
// 由于字符串字面值已经 是 字符串切片,因此无需切片语法,这
|
// 由于字符串字面值已经 *是* 字符串切片,
|
||||||
// 也是有效的!
|
// 因此无需切片语法,这也会工作。
|
||||||
let word = first_word(s_string_literal);
|
let word = first_word(s_string_literal);
|
||||||
|
|
||||||
println! ("{}", word);
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## 其他切片
|
## 其他切片
|
||||||
|
|
||||||
或许已经想到,字符串切片是特定于字符串的。然而还有更多通用切片类型呢。请看下面这个数组:
|
**Other Slices**
|
||||||
|
|
||||||
|
|
||||||
|
如同咱们可能想象的那样,字符串切片是专门针对字符串的。但还有一种更通用的切片类型。请看这个数组:
|
||||||
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let a = [1, 2, 3, 4, 5];
|
let a = [1, 2, 3, 4, 5];
|
||||||
```
|
```
|
||||||
|
|
||||||
就跟要引用字符串的部分一样,也可能要引用数组的部分。那么就将像下面这样,来完成对数组一部分的引用:
|
正如我们可能要引用字符串的一部分,我们也可能想引用数组的一部分。我们可以这样做:
|
||||||
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|
||||||
let a = [1, 2, 3, 4, 5];
|
let a = [1, 2, 3, 4, 5];
|
||||||
|
|
||||||
let slice = &a[1..3];
|
let slice = &a[1..3];
|
||||||
@ -330,11 +337,13 @@ let a = [1, 2, 3, 4, 5];
|
|||||||
assert_eq! (slice, &[2, 3]);
|
assert_eq! (slice, &[2, 3]);
|
||||||
```
|
```
|
||||||
|
|
||||||
这个切片变量 `slice` 的类型为 `&[i32]`。数组切片的原理与字符串切片一样,都是经由存储到首个元素的引用,和切片长度实现的。今后将对所有类别的其他集合,运用到这种切片。在第 8 章讲到各种矢量时,就会对这些集合加以讨论。
|
|
||||||
|
这个切片的类型为 `&[i32]`。其工作方式与字符串切片相同,都是存储了到第一个元素的引用和长度。在其他各种集合中,咱们都将用到这种切片。我们将在第 8 章讨论矢量时,详细讨论这些集合。
|
||||||
|
|
||||||
|
|
||||||
# 本章小结
|
# 本章小结
|
||||||
|
|
||||||
所有权、借用及切片等概念,在编译时确保了 Rust 程序中的内存安全。Rust 语言所给到的对内存运用的掌控方式,与别的系统编程语言相同,但会让数据的所有者,在其超出作用域时,自动清理掉其数据,这就意味着咱们不必编写并调试额外代码,来实现这种控制。
|
|
||||||
|
|
||||||
所有权对 Rust 程序的许多其他部分都有影响,因此在本书其余部分,都将更进一步的涉及到这些所有权的概念。接下来就要移步第 5 章,而在结构体 `struct` 中,如何将小块数据组装起来。
|
所有权、借用和切片的概念,确保了 Rust 程序在编译时的内存安全。Rust 给到了咱们,与其他系统编程语言同样方式的,对咱们内存使用的掌控,但在数据的所有者超出作用域时,会让数据的所有者,自动清理该数据,这意味着咱们不必编写和调试额外代码,来获得这种控制。
|
||||||
|
|
||||||
|
所有权会影响 Rust 许多其他部分的工作方式,因此我们将在本书的其余部分,进一步讨论这些概念。我们来继续阅读第 5 章,看看如何在 `struct` 中,对数据块进行分组。
|
||||||
|
Loading…
Reference in New Issue
Block a user