|
|
|
@ -1,22 +1,21 @@
|
|
|
|
|
## 使用字符串储存 UTF-8 编码的文本
|
|
|
|
|
|
|
|
|
|
> [ch08-02-strings.md](https://github.com/rust-lang/book/blob/main/src/ch08-02-strings.md)
|
|
|
|
|
> <br>
|
|
|
|
|
> commit 668c64760b5c7ea654facb4ba5fe9faddfda27cc
|
|
|
|
|
<!-- https://github.com/rust-lang/book/blob/main/src/ch08-02-strings.md -->
|
|
|
|
|
<!-- commit 3a30e4c1fbe641afc066b3af9eb01dcdf5ed8b24 -->
|
|
|
|
|
|
|
|
|
|
第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解它。字符串是新晋 Rustacean 们通常会被困住的领域,这是由于三方面理由的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些要素结合起来对于来自其他语言背景的程序员就可能显得很困难了。
|
|
|
|
|
|
|
|
|
|
在集合章节中讨论字符串的原因是,字符串就是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在这一部分,我们会讲到 `String` 中那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论 `String` 与其他集合不一样的地方,例如索引 `String` 是很复杂的,由于人和计算机理解 `String` 数据方式的不同。
|
|
|
|
|
在集合章节中讨论字符串的原因是,字符串就是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在本小节中,我们会讲到 `String` 中那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论 `String` 与其他集合不一样的地方,例如索引 `String` 是很复杂的,由于人和计算机理解 `String` 数据方式的不同。
|
|
|
|
|
|
|
|
|
|
### 什么是字符串?
|
|
|
|
|
|
|
|
|
|
在开始深入这些方面之前,我们需要讨论一下术语 **字符串** 的具体意义。Rust 的核心语言中只有一种字符串类型:字符串 slice `str`,它通常以被借用的形式出现,`&str`。第四章讲到了 **字符串 slices**:它们是一些对储存在别处的 UTF-8 编码字符串数据的引用。举例来说,由于字符串字面值被储存在程序的二进制输出中,因此字符串字面值也是字符串 slices。
|
|
|
|
|
我们先定义一下**字符串**这一术语的具体意义。Rust 的核心语言中只有一种字符串类型,字符串 slice `str`,它通常以被借用的形式出现,`&str`。第四章讲到了**字符串 slices**:它们是一些对储存在别处的 UTF-8 编码字符串数据的引用。举例来说,由于字符串字面值被储存在程序的二进制输出中,因此它们也是字符串 slices。
|
|
|
|
|
|
|
|
|
|
字符串(`String`)类型由 Rust 标准库提供,而不是编入核心语言,它是一种可增长、可变、可拥有、UTF-8 编码的字符串类型。当 Rustaceans 提及 Rust 中的 "字符串 "时,他们可能指的是 `String` 或 string slice `&str` 类型,而不仅仅是其中一种类型。虽然本节主要讨论 `String`,但这两种类型在 Rust 的标准库中都有大量使用,而且 `String` 和 字符串 slices 都是 UTF-8 编码的。
|
|
|
|
|
|
|
|
|
|
### 新建字符串
|
|
|
|
|
|
|
|
|
|
很多 `Vec` 可用的操作在 `String` 中同样可用,事实上 `String` 被实现为一个带有一些额外保证、限制和功能的字节 vector 的封装。其中一个同样作用于 `Vec<T>` 和 `String` 函数的例子是用来新建一个实例的 `new` 函数,如示例 8-11 所示。
|
|
|
|
|
很多 `Vec<T>` 上可用的操作在 `String` 中同样可用,事实上 `String` 被实现为一个带有一些额外保证、限制和功能的字节 vector 的封装。其中一个同样作用于 `Vec<T>` 和 `String` 函数的例子是用来新建一个实例的 `new` 函数,如示例 8-11 所示。
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-11/src/main.rs:here}}
|
|
|
|
@ -24,7 +23,7 @@
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 8-11:新建一个空的 `String`</span>
|
|
|
|
|
|
|
|
|
|
这新建了一个叫做 `s` 的空的字符串,接着我们可以向其中装载数据。通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,可以使用 `to_string` 方法,它能用于任何实现了 `Display` trait 的类型,比如字符串字面值。示例 8-12 展示了两个例子。
|
|
|
|
|
这新建了一个叫做 `s` 的空的字符串,接着我们可以向其中加载数据。通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,可以使用 `to_string` 方法,它能用于任何实现了 `Display` trait 的类型,比如字符串字面值。示例 8-12 展示了两个例子。
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-12/src/main.rs:here}}
|
|
|
|
@ -44,7 +43,7 @@
|
|
|
|
|
|
|
|
|
|
因为字符串应用广泛,这里有很多不同的用于字符串的通用 API 可供选择。其中一些可能看起来多余,不过都有其用武之地!在这个例子中,`String::from` 和 `.to_string` 最终做了完全相同的工作,所以如何选择就是代码风格与可读性的问题了。
|
|
|
|
|
|
|
|
|
|
记住字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据,如示例 8-14 所示。
|
|
|
|
|
记住字符串是 UTF-8 编码的,所以可以包含任何经过正确编码的数据,如示例 8-14 所示。
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-14/src/main.rs:here}}
|
|
|
|
@ -78,7 +77,7 @@
|
|
|
|
|
|
|
|
|
|
如果 `push_str` 方法获取了 `s2` 的所有权,就不能在最后一行打印出其值了。好在代码如我们期望那样工作!
|
|
|
|
|
|
|
|
|
|
`push` 方法被定义为获取一个单独的字符作为参数,并附加到 `String` 中。示例 8-17 展示了使用 `push` 方法将字母 "l" 加入 `String` 的代码。
|
|
|
|
|
`push` 方法被定义为获取一个单独的字符作为参数,并附加到 `String` 中。示例 8-17 展示了使用 `push` 方法将字母 *l* 加入 `String` 的代码。
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-17/src/main.rs:here}}
|
|
|
|
@ -86,7 +85,7 @@
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 8-17:使用 `push` 将一个字符加入 `String` 值中</span>
|
|
|
|
|
|
|
|
|
|
执行这些代码之后,`s` 将会包含 “lol”。
|
|
|
|
|
执行这些代码之后,`s` 将会包含 `lol`。
|
|
|
|
|
|
|
|
|
|
#### 使用 `+` 运算符或 `format!` 宏拼接字符串
|
|
|
|
|
|
|
|
|
@ -106,25 +105,25 @@ fn add(self, s: &str) -> String {
|
|
|
|
|
|
|
|
|
|
在标准库中你会发现,`add` 的定义使用了泛型和关联类型。在这里我们替换为了具体类型,这也正是当使用 `String` 值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解 `+` 运算那微妙部分的线索。
|
|
|
|
|
|
|
|
|
|
首先,`s2` 使用了 `&`,意味着我们使用第二个字符串的 **引用** 与第一个字符串相加。这是因为 `add` 函数的 `s` 参数:只能将 `&str` 和 `String` 相加,不能将两个 `String` 值相加。不过等一下 —— `&s2` 的类型是 `&String`, 而不是 `add` 第二个参数所指定的 `&str`。那么为什么示例 8-18 还能编译呢?
|
|
|
|
|
首先,`s2` 使用了 `&`,意味着我们使用第二个字符串的**引用**与第一个字符串相加。这是因为 `add` 函数的 `s` 参数:只能将 `&str` 和 `String` 相加,不能将两个 `String` 值相加。不过等一下 —— `&s2` 的类型是 `&String`, 而不是 `add` 第二个参数所指定的 `&str`。那么为什么示例 8-18 还能编译呢?
|
|
|
|
|
|
|
|
|
|
之所以能够在 `add` 调用中使用 `&s2` 是因为 `&String` 可以被 **强转**(*coerced*)成 `&str`。当`add`函数被调用时,Rust 使用了一个被称为 **Deref 强制转换**(*deref coercion*)的技术,你可以将其理解为它把 `&s2` 变成了 `&s2[..]`。第十五章会更深入的讨论 Deref 强制转换。因为 `add` 没有获取参数的所有权,所以 `s2` 在这个操作后仍然是有效的 `String`。
|
|
|
|
|
之所以能够在 `add` 调用中使用 `&s2` 是因为 `&String` 可以被 **强转**(*coerced*)成 `&str`。当`add`函数被调用时,Rust 使用了一个被称为 **Deref 强制转换**(*deref coercion*)的技术,实际上会把 `&s2` 转换为 `&s2[..]`。第十五章会更深入的讨论 Deref 强制转换。因为 `add` 没有获取参数的所有权,所以在这个操作后 `s2` 仍然是有效的 `String`。
|
|
|
|
|
|
|
|
|
|
其次,可以发现签名中 `add` 获取了 `self` 的所有权,因为 `self` **没有** 使用 `&`。这意味着示例 8-18 中的 `s1` 的所有权将被移动到 `add` 调用中,之后就不再有效。所以虽然 `let s3 = s1 + &s2;` 看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取 `s1` 的所有权,附加上从 `s2` 中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝,不过实际上并没有:这个实现比拷贝要更高效。
|
|
|
|
|
其次,可以发现签名中 `add` 获取了 `self` 的所有权,因为 `self` **没有**使用 `&`。这意味着示例 8-18 中的 `s1` 的所有权将被移动到 `add` 调用中,之后就不再有效。所以虽然 `let s3 = s1 + &s2;` 看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取 `s1` 的所有权,附加上从 `s2` 中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝,不过实际上并没有:这个实现比拷贝要更高效。
|
|
|
|
|
|
|
|
|
|
如果想要级联多个字符串,`+` 的行为就显得笨重了:
|
|
|
|
|
如果想要级联多个字符串,`+` 运算符的行为就显得笨重了:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch08-common-collections/no-listing-01-concat-multiple-strings/src/main.rs:here}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这时 `s` 的内容会是 “tic-tac-toe”。在有这么多 `+` 和 `"` 字符的情况下,很难理解具体发生了什么。对于更为复杂的字符串链接,可以使用 `format!` 宏:
|
|
|
|
|
这时 `s` 的内容会是 `tic-tac-toe`。在有这么多 `+` 和 `"` 字符的情况下,很难理解具体发生了什么。对于更为复杂的字符串链接,可以使用 `format!` 宏:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch08-common-collections/no-listing-02-format/src/main.rs:here}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这些代码也会将 `s` 设置为 “tic-tac-toe”。`format!` 与 `println!` 的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果内容的 `String`。这个版本就好理解的多,宏 `format!` 生成的代码使用引用所以不会获取任何参数的所有权。
|
|
|
|
|
这些代码也会将 `s` 设置为 `tic-tac-toe`。`format!` 与 `println!` 的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果内容的 `String`。这个版本就好理解的多,宏 `format!` 生成的代码使用引用因此不会获取任何参数的所有权。
|
|
|
|
|
|
|
|
|
|
### 索引字符串
|
|
|
|
|
|
|
|
|
@ -142,36 +141,36 @@ fn add(self, s: &str) -> String {
|
|
|
|
|
{{#include ../listings/ch08-common-collections/listing-08-19/output.txt}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 是如何在内存中储存字符串的。
|
|
|
|
|
错误和提示说明了全部问题:Rust 的字符串不支持索引。那么,为什么会这样呢?为了回答这个问题,我们必须先聊一聊 Rust 是如何在内存中储存字符串的。
|
|
|
|
|
|
|
|
|
|
#### 内部表现
|
|
|
|
|
|
|
|
|
|
`String` 是一个 `Vec<u8>` 的封装。让我们看看示例 8-14 中一些正确编码的字符串的例子。首先是这一个:
|
|
|
|
|
`String` 是一个 `Vec<u8>` 的封装。让我们看看示例 8-14 中一些正确编码的字符串的例子。首先是这一例:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-14/src/main.rs:spanish}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
在这里,`len` 的值是 4,这意味着储存字符串 “Hola” 的 `Vec` 的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢?(注意这个字符串中的首字母是西里尔字母的 Ze 而不是数字 3。)
|
|
|
|
|
在这里,`len` 的值是 `4`,这意味着储存字符串 `"Hola"` 的 vector 的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。下面这一行可能会让你感到意外(注意这个字符串中的首字母是西里尔字母的 *Ze* 而不是数字 3。):
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch08-common-collections/listing-08-14/src/main.rs:russian}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
当问及这个字符是多长的时候有人可能会说是 12。然而,Rust 的回答是 24。这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为每个 Unicode 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。作为演示,考虑如下无效的 Rust 代码:
|
|
|
|
|
如果有人问及该字符串的长度,你可能会回答 12。然而,Rust 的回答是 24:这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为在这个字符串中每个 Unicode 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。作为演示,考虑如下无效的 Rust 代码:
|
|
|
|
|
|
|
|
|
|
```rust,ignore,does_not_compile
|
|
|
|
|
let hello = "Здравствуйте";
|
|
|
|
|
let answer = &hello[0];
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
我们已经知道 `answer` 不是第一个字符 `3`。当使用 UTF-8 编码时,(西里尔字母的 Ze)`З` 的第一个字节是 `208`,第二个是 `151`,所以 `answer` 实际上应该是 `208`,不过 `208` 自身并不是一个有效的字母。返回 `208` 可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引 0 位置所能提供的唯一数据。用户通常不会想要一个字节值被返回。即使这个字符串只有拉丁字母,如果 `&"hello"[0]` 是返回字节值的有效代码,它也会返回 `104` 而不是 `h`。
|
|
|
|
|
我们已经知道 `answer` 不是第一个字符 `З`。当使用 UTF-8 编码时,`З` 的第一个字节是 `208`,第二个是 `151`,所以 `answer` 实际上应该是 `208`,不过 `208` 自身并不是一个有效的字母。返回 `208` 可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引 0 位置所能提供的唯一数据。用户通常不会想要一个字节值被返回,即使这个字符串只有拉丁字母,如果 `&"hi"[0]` 是返回字节值的有效代码,它也会返回 `104` 而不是 `h`。
|
|
|
|
|
|
|
|
|
|
为了避免返回意外的值并造成不能立刻发现的 bug,Rust 根本不会编译这些代码,并在开发过程中及早杜绝了误会的发生。
|
|
|
|
|
|
|
|
|
|
#### 字节、标量值和字形簇!天呐!
|
|
|
|
|
|
|
|
|
|
这引起了关于 UTF-8 的另外一个问题:从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中 **字母** 的概念)。
|
|
|
|
|
这引起了关于 UTF-8 的另外一个问题:从 Rust 的角度来讲,事实上有三种相关方式可以查看字符串:字节、标量值和字形簇(最接近人们眼中 **字母**(*letters*)的概念)。
|
|
|
|
|
|
|
|
|
|
比如这个用梵文书写的印度语单词 “नमस्ते”,最终它储存在 vector 中的 `u8` 值看起来像这样:
|
|
|
|
|
|
|
|
|
@ -206,15 +205,15 @@ let hello = "Здравствуйте";
|
|
|
|
|
let s = &hello[0..4];
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这里,`s` 会是一个 `&str`,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着 `s` 将会是 “Зд”。
|
|
|
|
|
这里,`s` 会是一个 `&str`,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着 `s` 将会是 `Зд`。
|
|
|
|
|
|
|
|
|
|
如果获取 `&hello[0..1]` 会发生什么呢?答案是:Rust 在运行时会 panic,就跟访问 vector 中的无效索引时一样:
|
|
|
|
|
如果尝试用类似 `&hello[0..1]` 的方式对字符的部分字节进行 slice,Rust 会在运行时 panic,就跟访问 vector 中的无效索引时一样:
|
|
|
|
|
|
|
|
|
|
```console
|
|
|
|
|
{{#include ../listings/ch08-common-collections/output-only-01-not-char-boundary/output.txt}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
你应该小心谨慎地使用这个操作,因为这么做可能会使你的程序崩溃。
|
|
|
|
|
在使用 range 来创建字符串 slice 时要格外小心,因为这么做可能会使你的程序崩溃。
|
|
|
|
|
|
|
|
|
|
### 遍历字符串的方法
|
|
|
|
|
|
|
|
|
@ -241,7 +240,7 @@ for b in "Зд".bytes() {
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这些代码会打印出组成 `String` 的 4 个字节:
|
|
|
|
|
这些代码会打印出组成字符串的四个字节:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
208
|
|
|
|
@ -250,17 +249,14 @@ for b in "Зд".bytes() {
|
|
|
|
|
180
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
不过请记住有效的 Unicode 标量值可能会由不止一个字节组成。
|
|
|
|
|
不过请务必记住有效的 Unicode 标量值可能会由不止一个字节组成。
|
|
|
|
|
|
|
|
|
|
从字符串中获取如同天城文这样的字形簇是很复杂的,所以标准库并没有提供这个功能。[crates.io](https://crates.io/)<!-- ignore --> 上有些提供这样功能的 crate。
|
|
|
|
|
|
|
|
|
|
### 字符串并不简单
|
|
|
|
|
|
|
|
|
|
总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理 `String` 数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何预先处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发周期后期免于处理涉及非 ASCII 字符的错误。
|
|
|
|
|
总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理 `String` 数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何预先处理 UTF-8 数据。这种权衡相比其他语言更多地暴露出了字符串的复杂性,不过也使你在开发周期后期免于处理涉及非 ASCII 字符的错误。
|
|
|
|
|
|
|
|
|
|
好消息是标准库提供了很多围绕 `String` 和 `&str` 构建的功能,来帮助我们正确处理这些复杂场景。请务必查看这些使用方法的文档,例如 `contains` 来搜索一个字符串,和 `replace` 将字符串的一部分替换为另一个字符串。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
称作 `String` 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。当 Rustacean 们谈到 Rust 的 “字符串”时,它们通常指的是 `String` 或字符串 slice `&str` 类型,而不特指其中某一个。虽然本部分内容大多是关于 `String` 的,不过这两个类型在 Rust 标准库中都被广泛使用,`String` 和字符串 slices 都是 UTF-8 编码的。
|
|
|
|
|
|
|
|
|
|
现在让我们转向一些不太复杂的集合:哈希 map!
|
|
|
|
|