From a227b1ffd2a2854d7f6b2f045e36b3e493d75153 Mon Sep 17 00:00:00 2001 From: Hector PENG Date: Sat, 7 Jun 2025 18:14:14 +0800 Subject: [PATCH] Updated 'src/common_collections/strings.md'. --- :w | 378 ------------------------------------------------------------- 1 file changed, 378 deletions(-) delete mode 100644 :w diff --git a/:w b/:w deleted file mode 100644 index 7a9455b..0000000 --- a/:w +++ /dev/null @@ -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` 的许多相同操作对 `String` 也可使用,因为 `String` 实际上是作为字节矢量的封装器实现的,不过带有一些额外的保证、限制及功能。`Vec` 和 `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` 的内容一样。此外,咱们还可以方便地使用 `+` 运算符或 `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` 的一种封装(a `String` is a wrapper over a `Vec`)。下面来看看清单 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) - -