mirror of
https://github.com/gnu4cn/rust-lang-zh_CN.git
synced 2026-08-22 14:13:28 +08:00
Updated 'src/common_collections/strings.md'.
This commit is contained in:
378
:w
378
:w
@@ -1,378 +0,0 @@
|
||||
# 使用 `String` 存储 UTF-8 编码的文本
|
||||
|
||||
在第 4 章中,我们曾讨论过字符串,现在我们将对其进行更深入的研究。新的 Rust 公民通常卡在字符串上,原因有三:
|
||||
|
||||
- Rust 有暴露可能错误的倾向;
|
||||
- 字符串是种比许多程序员认为的更复杂的数据结构;
|
||||
- 以及 UTF-8。
|
||||
|
||||
当咱们从其他编程语言转至 Rust 时,这些因素就会以看起来很难的方式,纠缠在一起。
|
||||
|
||||
|
||||
我们之所以会在集合语境下讨论字符串,是因为字符串是作为字节集合,加上一些在这些字节被解释为文本时,提供有用功能的方法实现的。在本节中,我们将讨论那些 `String` 上的、每种集合类型都有的操作,比如创建、更新及读取等。我们还将讨论 `String` 与其他集合的不同之处,即由于人与计算机在解析 `String` 数据时的差异,而对 `String` 的索引会变得如何复杂。
|
||||
|
||||
|
||||
## 何为字符串?
|
||||
|
||||
|
||||
我们将首先定义出所谓 *字符串* 的含义。Rust 的核心语言中,只有一种字符串类型,即通常以其借用形式 `&str` 出现的字符串切片 `str`。在第 4 章我们曾谈到过 [*字符串片切片*](../ownership/the_slice_type.md#字符串切片),他们是对存储在别处的 UTF-8 编码字符串数据的一些引用。例如,字符串的字面值,就存储在程序的二进制文件中,而因此属于一些字符串的切片。
|
||||
|
||||
|
||||
所谓 `String` 类型,则是由 Rust 的标准库提供,而未被编码到核心语言,是种可增长、可变、有所有权、UTF-8 编码的字符串类型。当 Rust 公民提及 Rust 中的 “字符串” 时,他们可能指的是 `String` 或字符串切片的 `&str` 类型,而不单是其中一种类型。虽然本节主要讨论 `String`,但这两种类型在 Rust 的标准库中都有大量使用,同时 `String` 和字符串切片均为 UTF-8 编码的。
|
||||
|
||||
|
||||
## 创建一个新的 `String`
|
||||
|
||||
对 `Vec<T>` 的许多相同操作对 `String` 也可使用,因为 `String` 实际上是作为字节矢量的封装器实现的,不过带有一些额外的保证、限制及功能。`Vec<T>` 和 `String` 下以同一方式工作的函数示例,便是创建出一个实例的 `new` 函数,如下清单 8-11 所示。
|
||||
|
||||
|
||||
```rust
|
||||
let mut s = String::new();
|
||||
```
|
||||
|
||||
*清单 8-11:创建一个新的空 `String`*
|
||||
|
||||
|
||||
这行代码会创建出一个名为 `s` 的新空字符串,然后我们就可以向其中加载数据。通常情况下,我们会使用一些咱们开始该字符串的初始数据。为此,我们会使用 `to_string` 方法,该方法适用于任何实现了 `Display` 特质的类型,正如字符串字面量那样。下面清单 8-12 展示了两个示例。
|
||||
|
||||
|
||||
```rust
|
||||
let data = "初始内容";
|
||||
|
||||
let s = data.to_string();
|
||||
|
||||
// 该方法同样直接工作于字面值之上
|
||||
let s = "初始内容".to_string();
|
||||
```
|
||||
|
||||
*清单 8-12:使用 `to_string` 方法从某个字符串字面值创建出 `String`*
|
||||
|
||||
这段代码会创建出一个包含 `初始内容` 的字符串。
|
||||
|
||||
|
||||
我们还可以使用函数 `String::from`,从某个字符串字面量创建出字符串。下面清单 8-13 中的代码,等同于清单 8-12 中使用 `to_string` 的代码。
|
||||
|
||||
|
||||
```rust
|
||||
let s = String::from("初始内容");
|
||||
```
|
||||
|
||||
*清单 8-13:使用 `String::from` 函数,从某个字符串字面值创建出 `String`*
|
||||
|
||||
|
||||
由于字符串的用途非常广泛,我们可以使用许多不同的字符串通用 API,从而为我们提供了大量选择。其中有些看起来是多余的,但他们都有自己的用武之地!在这个示例中,`String::from` 和 `to_string` 做的是同一件事,所以咱们选择哪个只是风格与可读性的问题。
|
||||
|
||||
请记住,字符串是 UTF-8 编码的,因此我们可以在其中包含任何正确编码的数据,如下清单 8-14 所示。
|
||||
|
||||
```rust
|
||||
let hello = String::from("السلام عليكم");
|
||||
let hello = String::from("Dobrý den");
|
||||
let hello = String::from("Hello");
|
||||
let hello = String::from("שָׁלוֹם");
|
||||
let hello = String::from("नमस्ते");
|
||||
let hello = String::from("こんにちは");
|
||||
let hello = String::from("안녕하세요");
|
||||
let hello = String::from("你好");
|
||||
let hello = String::from("Olá");
|
||||
let hello = String::from("Здравствуйте");
|
||||
let hello = String::from("Hola");
|
||||
let hello = String::from("👋");
|
||||
```
|
||||
|
||||
*清单 8-14:以字符串存储不同语言的问候语*
|
||||
|
||||
所有这些都是有效的 `String` 值。
|
||||
|
||||
|
||||
## 更新字符串
|
||||
|
||||
在咱们向某个 `String` 中压入更多数据时,其大小和内容都会发生变化,就像 `Vec<T>` 的内容一样。此外,咱们还可以方便地使用 `+` 运算符或 `format!` 宏,连接多个 `String` 值。
|
||||
|
||||
|
||||
|
||||
### 使用 `push_str` 与 `push` 追加内容到某个 `String`
|
||||
|
||||
如下清单 8-15 所示,通过使用 `push_str` 方法追加一个字符串切片,我们可以增长某个 `String`。
|
||||
|
||||
```rust
|
||||
let mut s = String::from("foo");
|
||||
s.push_str("bar");
|
||||
println! ("{}", s);
|
||||
```
|
||||
|
||||
*清单 8-15:使用 `push_str` 方法将字符串切片追加到某个 `String`*
|
||||
|
||||
|
||||
在这两行后,`s` 将包含 `foobar`。`push_str` 方法会取个字符串切片,因为我们并不会想要取得那个参数的所有权。例如,在下面清单 8-16 中的代码中,我们打算在将 `s2` 的内容追加到 `s1` 后,能够使用 `s2`。
|
||||
|
||||
|
||||
```rust
|
||||
let mut s1 = String::from("foo");
|
||||
let s2 = "bar";
|
||||
|
||||
s1.push_str(s2);
|
||||
|
||||
println! ("{}", s2);
|
||||
```
|
||||
|
||||
*清单 8-16:在将其内容追加到某个 `String` 后,使用该字符串切片*
|
||||
|
||||
|
||||
如果 `push_str` 方法取得了 `s2` 的所有权,我们就无法在最后那行打印出他的值。然而,这段代码会如我们与其那样工作!
|
||||
|
||||
`push` 方法会取单个字符作为参数,并将其添加到 `String`。下面清单 8-17 使用 `push` 方法将字母 `l` 追加到某个 `String`。
|
||||
|
||||
```rust
|
||||
let mut s = String::from("lo");
|
||||
s.push('l');
|
||||
```
|
||||
|
||||
*清单 8-17:使用 `push` 方法,将一个字符添加到某个 `String`*
|
||||
|
||||
作为上面代码的结果,`s` 将包含 `lol`。
|
||||
|
||||
|
||||
### 使用 `+` 运算符或 `format!` 宏的字符串连接
|
||||
|
||||
**Concatenation with the `+` Operator or the `format!` Macro**
|
||||
|
||||
|
||||
通常,会想要将两个既有字符串合在一起。完成此操作的一种方式,就是使用 `+` 运算符,如下清单 8-18 中所示。
|
||||
|
||||
```rust
|
||||
let s1 = String::from("Hello, ");
|
||||
let s2 = String::from("world!");
|
||||
let s3 = s1 + &s2; // 请注意这里的 s1 已被迁移,而不再能被使用了
|
||||
```
|
||||
|
||||
*清单 8-18:运用 `+` 运算符来将两个 `String` 值结合为一个新的 `String` 值*
|
||||
|
||||
这个字符串 `s3` 将包含 `Hello, world!`。`s1` 在该字符串加法之后不再有效的原因,以及这里使用到 `s2` 引用的原因,与这里使用 `+` 运算符时,被调用的那个方法的签名有关。这个 `+` 运算符使用了 `add` 方法,而该方法的签名使用了 `add` 方法,`add` 方法的签名,看起来与下面的类似:
|
||||
|
||||
```rust
|
||||
fn add(self, s: &str) -> String
|
||||
```
|
||||
|
||||
在标准库中,就会看到使用泛型定义的 `add` 函数。这里已将泛型的那些参数,用具体类型进行了替换,即在以 `String` 类型值调用是所发生的。在第 10 章就会讨论泛型。这个函数签名,提供了了解那个 `+` 运算符棘手之处所需的线索。
|
||||
|
||||
首先,这里的 `s2` 有个 `&`,表示这里这里正将第二个字符串的 *引用*,添加到第一个字符串。这是由于 `add` 函数中的那个 `s` 参数的原因:这里只能将一个 `&str` 添加到某个 `String`;这里是无法将两个 `String` 相加在一起的。不过稍等一下 -- `&s2` 的类型是 `&String`,而非在 `add` 函数的第二个参数中所指明的 `&str`。那为何清单 8-18 会编译呢?
|
||||
|
||||
这里之所以能在到 `add` 的调用中使用 `&s2` 的原因,在于编译器可将那个 `&String` 参数,*强制转换* 为 `&str` 类型。在调用 `add` 方法时,Rust 使用了 *解引用强制转换(deref coercion)* 特性,在这里该特性就将 `&s2` 转换为了 `&s2[..]`。在第 15 章就将深入讨论这个解引用强制转换。由于 `add` 方法并未占据那个 `s` 参数的所有权,因此 `s2` 在此运算之后,仍将有效。
|
||||
|
||||
其次,这里可以看到,在该方法签名中,由于 `self` *没有* `&`,那么 `add` 就取得了 `self` 的所有权。这就意味着清单 8-18 中的 `s1` 将被迁移到那个 `add` 调用中,并在那之后便不再有效。这样看来,尽管 `let s3 = s1 + &s2;` 这个语句看起来将同时拷贝这两个字符串,并创建一个新的字符串,不过此语句实际上是要取得 `s1` 的所有权,追加 `s2` 内容的一份拷贝,进而随后返回该运算结果的所有权。也就是说,看起来这行语句构造了很多拷贝,但并没有;这样的实现比拷贝更为高效。
|
||||
|
||||
在需要连接多个字符串时,这个 `+` 运算符的行为就变得笨拙了:
|
||||
|
||||
```rust
|
||||
let s1 = String::from("tic");
|
||||
let s2 = String::from("toc");
|
||||
let s3 = String::from("toe");
|
||||
|
||||
let s = s1 + "-" + &s2 + "-" + &s3;
|
||||
```
|
||||
|
||||
此处 `s` 将为 `tic-toc-toe`。由于有些全部的 `+` 与 `"` 字符,因此就难于看清发生了什么。对于较复杂的字符串合并,可这个 `format!` 宏:
|
||||
|
||||
```rust
|
||||
let s1 = String::from("tic");
|
||||
let s2 = String::from("toc");
|
||||
let s3 = String::from("toe");
|
||||
|
||||
let s = format! ("{}-{}-{}", s1, s2, s3);
|
||||
```
|
||||
|
||||
这段代码同样把 `s` 设置为了 `tic-toc-toe`。这个 `format!` 宏与 `println!` 宏的运作类似,而与将输出打印到屏幕不同,他会将结果内容,以一个 `String` 加以返回。使用 `format!` 这个版本的代码,读起来容易得多,且由于 `format!` 宏所生成的代码,使用的是引用,那么这个调用就不会占据任何一个其参数的所有权。
|
||||
|
||||
|
||||
## 索引到字符串内部
|
||||
|
||||
**Indexing into Strings**
|
||||
|
||||
|
||||
再许多其他编程语言中,经由通过索引而引用字符串中的一些单独字符,都是有效且常见的操作。不过在 Rust 中,当尝试使用索引来访问某个 `String` 的一些部分时,就会收到错误。请考虑下面清单 8-19 中的无效代码。
|
||||
|
||||
```rust
|
||||
let s1 = String::from("hello");
|
||||
let h = s1[0];
|
||||
```
|
||||
|
||||
*清单 8-19:尝试在某个字符串上使用索引语法*
|
||||
|
||||
该代码将引发下面的错误:
|
||||
|
||||
```console
|
||||
$ cargo run
|
||||
Compiling string_demo v0.1.0 (/home/peng/rust-lang/projects/string_demo)
|
||||
error[E0277]: the type `String` cannot be indexed by `{integer}`
|
||||
--> src/main.rs:3:13
|
||||
|
|
||||
3 | let h = s1[0];
|
||||
| ^^^^^ `String` cannot be indexed by `{integer}`
|
||||
|
|
||||
= help: the trait `Index<{integer}>` is not implemented for `String`
|
||||
|
||||
For more information about this error, try `rustc --explain E0277`.
|
||||
error: could not compile `string_demo` due to previous error
|
||||
```
|
||||
|
||||
这个报错和提示讲清了缘由:Rust 的字符串不支持索引。但为什么不支持呢?要回到这个问题,就要探讨一下 Rust 是怎样在内存中存储字符串的。
|
||||
|
||||
|
||||
### 内部表示
|
||||
|
||||
**Internal Representation**
|
||||
|
||||
|
||||
`String` 是对 `Vec<u8>` 的一种封装(a `String` is a wrapper over a `Vec<v8>`)。下面来看看清单 8-14 中,那里的一些以 UTF-8 良好编码的示例字符串。首先是这个:
|
||||
|
||||
```rust
|
||||
let hello = String::from("Hola");
|
||||
```
|
||||
|
||||
在此示例中,`len` 将为 `4`,这表示这个存储着字符串 “Hola” 的矢量长度为 4 个字节。这些字母在以 UTF-8 编码时,每个占用 1 个字节。而接下来的这行,就会惊讶到你了。(请注意这个字符串是以大写西里尔字母 `Ze` 开头,而非阿拉伯数字 `3`。)
|
||||
|
||||
```rust
|
||||
let hello = String::from("Здравствуйте");
|
||||
```
|
||||
|
||||
在被问及这个字符串有多长时,你可能会讲是 `12`。事实上,Rust 的答案是 `24`:由于那个字符串中的每个 Unicode 标量值,都有占用 2 字节的存储,故那就是以 UTF-8 编码 `Здравствуйте` 所用占用的字节数。由于这个缘故,到该字符串的那些字节的所以,就不会总是对应到某个有效的 Unicode 标量值了。为对此加以演示,请设想下面这段无效的 Rust 代码:
|
||||
|
||||
```rust
|
||||
let hello = String::from("Здравствуйте");
|
||||
let answer = &hello[0];
|
||||
```
|
||||
|
||||
这里当然清楚 `answer` 将不会是那第一个字母 `З`。在以 UTF-8 编码时,`З` 的第一个字节是 `208`,同时第二个字节为 `151`,因此看起来 `answer` 事实上应该是 `208`,但 `208` 本身并不是个有效的字符。在用户要求该字符串的首个字母时,返回 `208` 就不会是他们所想要;然而,那却是 Rust 在字节索引 `0` 处有的唯一数据了。即使字符串只包含拉丁字母,用户也通常不想要那个返回的字节值:即便 `&"hello"[0]` 是返回了字节值的有效代码,他也会返回 `104`,而非 `h`。
|
||||
|
||||
那么答案就是,为避免返回一个不期望的值,以及避免引发一些可能无法立即发现的程序错误,Rust 就根本不会编译此代码,并阻止了在开发过程早期阶段的这些误解。
|
||||
|
||||
|
||||
### 字节、标量值与字素簇!我的天!
|
||||
|
||||
**Bytes and Scalar Values and Grapheme Clusters! Oh My!)**
|
||||
|
||||
|
||||
有关 UTF-8 的另一点,即为从 Rust 视角看待字符串,事实上有三种相关方式:视为字节、标量值与字素簇(而字素簇则是与我们称之为 *文字/letters* 的东西的最接近的事物了)。
|
||||
|
||||
在看到以梵文字书写的印地语词汇 `"नमस्ते"` 时,这个词汇是以看起来像下面这样的一些 `u8` 类型值的矢量存储的:
|
||||
|
||||
```rust
|
||||
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]
|
||||
```
|
||||
|
||||
这是 18 个字节,也是计算机最终存储该数据的方式。而在将他们视为 Unicode 的标量值,即 Rust 的 `char` 类型时,则这些字节看起来是这样的:
|
||||
|
||||
```rust
|
||||
['न', 'म', 'स', '्', 'त', 'े']
|
||||
```
|
||||
|
||||
这里就有了六个 `char` 值了,但其中第四与第六个并不是文字(letters):他们是自身并无意义的变音符号。最后,在将这些 Unicode 标量值视为字素簇时,就得到了人类所称呼的、四个构成了那个印地词语的四个文字:
|
||||
|
||||
```rust
|
||||
["न", "म", "स्", "ते"]
|
||||
```
|
||||
|
||||
Rust 提供了解析计算机存储的原始字符串数据的数种不同方式,因此各个程序就可以选择他所需的解析方式,这与该数据为何种人类语言无关。
|
||||
|
||||
Rust 不允许索引进入 `String` 来获取某个字符的终极原因,即是索引操作,被认为总是消耗固定的时间(即 `O(1)`)。但由于 Rust 将不得不从开头遍历到索引位置,来确定那里有多少个有效字符,由此对于在 `String` 上执行索引操作,所消耗的时间是无法确保持续一致的。
|
||||
|
||||
|
||||
## 对字符串进行切片操作
|
||||
|
||||
**Slicing Strings**
|
||||
|
||||
|
||||
由于字符串索引操作的返回值类型不明朗:可能是字节值、字符、字素簇,或者字符串切片,因此索引到字符串中去,通常是个糟糕的主意。而在确实需要使用索引来创建字符串切片时,那么 Rust 就要求提供更具体的索引。
|
||||
|
||||
与使用带有单个数字的 `[]` 相比,可使用带有范围的 `[]`,来创建包含一些特定字节的字符串切片:
|
||||
|
||||
```rust
|
||||
let hello = String::from("Здравствуйте");
|
||||
|
||||
let s = &hello[0..4];
|
||||
```
|
||||
|
||||
这里的 `s` 将是个包含该字符串头 4 个字节的 `&str`。早先曾提到过,每个的这些字符都是 2 字节,那么这就意味着 `s` 将为 `Зд`。
|
||||
|
||||
而在尝试使用类似 `&hello[0..1]` 这样的操作,来对某个字符的那些字节的一部分进行切片时,Rust 就会在运行时,以与在矢量中访问无效索引时同样的方式终止运行:
|
||||
|
||||
```console
|
||||
thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`'...
|
||||
```
|
||||
|
||||
由于使用范围来创建出字符串切片这样的操作,可能将程序崩溃掉,因此进行这样操作时应小心谨慎。
|
||||
|
||||
|
||||
## 迭代字符串的一些方法
|
||||
|
||||
**Methods for Iterating Over Strings**
|
||||
|
||||
|
||||
在字符串的各个片段上进行操作的最好方式,就是显示地指明是要字符还是字节。对于单独的 Unicode 标量值,就使用 `chars` 方法。在 `नमस्ते` 上调用 `chars`,就会分理出并返回六个类型 `char` 的值来,进而就可以对结果进行迭代,而访问到各个元素:
|
||||
|
||||
```rust
|
||||
for c in "नमस्ते".chars() {
|
||||
println!("{}", c);
|
||||
}
|
||||
```
|
||||
|
||||
该代码将打印下面的东西:
|
||||
|
||||
```rust
|
||||
न
|
||||
म
|
||||
स
|
||||
|
||||
त
|
||||
|
||||
```
|
||||
|
||||
此外,`bytes` 方法返回的则是各个原始字节,对与特定领域,这方法可能正好:
|
||||
|
||||
```rust
|
||||
for b in s.bytes() {
|
||||
println!("{}", b);
|
||||
}
|
||||
```
|
||||
|
||||
该代码将打印出构成这个 `String` 的 18 个字节来:
|
||||
|
||||
```console
|
||||
224
|
||||
164
|
||||
168
|
||||
224
|
||||
164
|
||||
174
|
||||
224
|
||||
164
|
||||
184
|
||||
224
|
||||
165
|
||||
141
|
||||
224
|
||||
164
|
||||
164
|
||||
224
|
||||
165
|
||||
135
|
||||
```
|
||||
|
||||
不过要确保记住,有效的 Unicode 标量值可能有多余 1 个字节组成。
|
||||
|
||||
而从字符串获取字素簇则是复杂的,因此这项功能并未由标准库提供。在需要该功能时,在 [crates.io](https://crates.io/) 上有一些可用的代码箱。
|
||||
|
||||
|
||||
## 字符串并不简单
|
||||
|
||||
**Strings Are Not So Simple**
|
||||
|
||||
|
||||
总的来说,字符串是复杂的。不同编程语言,在以何种程度将这种复杂度呈现给编程者上,做出了不同的选择。Rust 选择了将正确处理 `String` 数据,作为所有 Rust 程序的默认行为,这就意味着 Rust 程序员就必须在处理 UTF-9 数据时,要提前投入更多思考。这种权衡暴露了相较于其他编程语言,更多的字符串复杂度,但这防止了在软件开发生命周期后期,将涉及到的非 ASCII 字符的错误处理。
|
||||
|
||||
接下来就要换到,有点复杂的东西:哈希图!
|
||||
|
||||
|
||||
(End)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user