diff --git a/:w b/:w new file mode 100644 index 0000000..7a9455b --- /dev/null +++ b/:w @@ -0,0 +1,378 @@ +# 使用 `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) + + diff --git a/projects/string_demo/src/main.rs b/projects/string_demo/src/main.rs index 6f89f5b..1300967 100644 --- a/projects/string_demo/src/main.rs +++ b/projects/string_demo/src/main.rs @@ -1,7 +1,11 @@ fn main() { - let mut s = String::from("hello"); + let s = "नमस्ते"; - s.push_str(", world!"); // push_str() 方法会追加一个字面值,到某个 String + for c in s.chars() { + println! ("{}", c); + } - println! ("{}", s); // 这将打印出 `hello, world!` + for b in s.bytes() { + println! ("{}", b); + } } diff --git a/src/common_collections/strings.md b/src/common_collections/strings.md index 590669d..ddc2f81 100644 --- a/src/common_collections/strings.md +++ b/src/common_collections/strings.md @@ -1,31 +1,30 @@ # 使用 `String` 存储 UTF-8 编码的文本 -在第 4 章中,就曾谈到过字符串,而现在则要深入审视他们。萌新 Rust 公民,通常会由于以下三个搅在一起的原因,而被字符串给卡住: +在第 4 章中,我们曾讨论过字符串,现在我们将对其进行更深入的研究。新的 Rust 公民通常卡在字符串上,原因有三: -- 作为 Rust 暴露各种可能错误的选择; -- 相比于许多程序员所意识到的,字符串是一种更具复杂度的数据结构; -- 还有就是 UTF-8。 +- Rust 有暴露可能错误的倾向; +- 字符串是种比许多程序员认为的更复杂的数据结构; +- 以及 UTF-8。 + +当咱们从其他编程语言转至 Rust 时,这些因素就会以看起来很难的方式,纠缠在一起。 -在从别的语言转到 Rust 时,这些因素就会有以看起来有难度的方式,纠缠在一起。 - -由于字符串是作为字节集合,加上一些在将这些字节解析为文本时,提供有用功能的方法,这样来实现的,因此这里就在集合的语境中,来讨论字符串了。在本小节,将谈及在 `String` 类型上的那些所有集合都有的操作,比如创建、更新与读取等等。这里也会讨论 `String` 与其他集合的不同之处,即通过对比人类与机器解读 `String` 类型数据的不同之处,来搞清楚索引进到某个 `String` 有何等复杂。 +我们之所以会在集合语境下讨论字符串,是因为字符串是作为字节集合,加上一些在这些字节被解释为文本时,提供有用功能的方法实现的。在本节中,我们将讨论那些 `String` 上的、每种集合类型都有的操作,比如创建、更新及读取等。我们还将讨论 `String` 与其他集合的不同之处,即由于人与计算机在解析 `String` 数据时的差异,而对 `String` 的索引会变得如何复杂。 -## 何为 `String`? +## 何为字符串? -这里首先就要定义一下,*字符串(string)* 这个名词指的是什么。Rust 在其核心语言中,只有一种字符串类型,那就是字符串切片类型 `str`,该类型通常是以其被借用的形式 `&str` 而出现。在第 4 章中,就讲到过 *字符串切片(string slices)*,他们是到一些存储在各处的、以 UTF-8 编码的字符串数据的引用。 +我们将首先定义出所谓 *字符串* 的含义。Rust 的核心语言中,只有一种字符串类型,即通常以其借用形式 `&str` 出现的字符串切片 `str`。在第 4 章我们曾谈到过 [*字符串片切片*](../ownership/the_slice_type.md#字符串切片),他们是对存储在别处的 UTF-8 编码字符串数据的一些引用。例如,字符串的字面值,就存储在程序的二进制文件中,而因此属于一些字符串的切片。 -而 `String` 类型,则是由 Rust 标准库所提供,而非编码进核心语言的,一种可增长、可变、(所有权)被持有的、UTF-8 编码的字符串类型(the `String` type, which is provided by Rust's standard library rather than coded into the core lanuage, is a growable, mutable, owned, UTF-8 encoded string type)。在 Rust 公民提到 Rust 中的 “字符串” 时,他们可能指的既是 `String`,也可可能是字符串切片的 `&str` 类型,而不仅仅是这些类型其中之一。虽然这个小节很大部分讲的是 `String`,但在 Rust 标准库中,两种类型都有重度使用,且 `String` 与字符串切片,都是 UTF-8 编码的。 -Rust 标准库还包含了一些其他字符串类型,比如 `OsString`、`OsStr`、`CString` 及 `CStr` 等等。一些库代码箱则可提供到甚至更多的用于存储字符串数据的选项。发现这些名称都是以 `String` 或 `Str` 结尾的了吧?他们指向的都是是有所有权的与借用的变种,就跟先前所见到的 `String` 与 `str` 类型一样。比如,这些字符串类型就可存储不同编码或在内存中以不同方式表示的文本。本章中不会讨论这些其他字符串类型;请参阅 API 文档,了解更多有关如何使用他们,以及何时哪个是恰当的字符串类型的更多知识。 +所谓 `String` 类型,则是由 Rust 的标准库提供,而未被编码到核心语言,是种可增长、可变、有所有权、UTF-8 编码的字符串类型。当 Rust 公民提及 Rust 中的 “字符串” 时,他们可能指的是 `String` 或字符串切片的 `&str` 类型,而不单是其中一种类型。虽然本节主要讨论 `String`,但这两种类型在 Rust 的标准库中都有大量使用,同时 `String` 和字符串切片均为 UTF-8 编码的。 ## 创建一个新的 `String` +对 `Vec` 的许多相同操作对 `String` 也可使用,因为 `String` 实际上是作为字节矢量的封装器实现的,不过带有一些额外的保证、限制及功能。`Vec` 和 `String` 下以同一方式工作的函数示例,便是创建出一个实例的 `new` 函数,如下清单 8-11 所示。 -`Vec` 的许多同样操作,对 `String` 也是可用的,这里就以创建一个新字符串的 `new` 函数开始,如下清单 8-11 中所示。 ```rust let mut s = String::new(); @@ -33,7 +32,9 @@ Rust 标准库还包含了一些其他字符串类型,比如 `OsString`、`OsS *清单 8-11:创建一个新的空 `String`* -这行代码就创建了一个新的、名为 `s` 的空字符串,随后就可以将数据加载进这个空字符串了。通常,这里会有一些初始数据作为字符串的开头。为此,就要使用 `to_string` 方法,这个方法在所有实现了 `Display` 特质(the `Display` trait),正如字符串字面值这样的类型上,都是可用的。下面清单 8-12 给出了两个示例。 + +这行代码会创建出一个名为 `s` 的新空字符串,然后我们就可以向其中加载数据。通常情况下,我们会使用一些咱们开始该字符串的初始数据。为此,我们会使用 `to_string` 方法,该方法适用于任何实现了 `Display` 特质的类型,正如字符串字面量那样。下面清单 8-12 展示了两个示例。 + ```rust let data = "初始内容"; @@ -44,21 +45,24 @@ Rust 标准库还包含了一些其他字符串类型,比如 `OsString`、`OsS let s = "初始内容".to_string(); ``` -*清单 8-12:使用 `to_string` 方法自字符串字面值创建出一个 `String`* +*清单 8-12:使用 `to_string` 方法从某个字符串字面值创建出 `String`* -此代码传教了一个包含 `初始内容` 的字符串。 +这段代码会创建出一个包含 `初始内容` 的字符串。 + + +我们还可以使用函数 `String::from`,从某个字符串字面量创建出字符串。下面清单 8-13 中的代码,等同于清单 8-12 中使用 `to_string` 的代码。 -这里也可以使用函数 `String::from` 来从字符串字面值创建 `String`。下面清单 8-13 众多的代码,与使用 `to_string` 的清单 8-12 中的代码等价。 ```rust let s = String::from("初始内容"); ``` -*清单 8-13:使用 `String::from` 函数,从字符串字面值创建一个 `String`* +*清单 8-13:使用 `String::from` 函数,从某个字符串字面值创建出 `String`* -由于字符串有相当多的用途,因此就可以使用字符串的众多不同的通用 API,而赋予到很多选择。这些字符串通用 API 中的一些,可能看起来是重复的,但他们全都有他们的用处!就在这个示例中,`String::from` 与 `to_string` 两个函数完成的是同样的事情,那么选择哪个,就关乎代码风格与可读性了。 -请记住字符串都是 UTF-8 编码的,因此就可以将任何编码恰当的数据,包含在字符串中,如下清单 8-14 中所示。 +由于字符串的用途非常广泛,我们可以使用许多不同的字符串通用 API,从而为我们提供了大量选择。其中有些看起来是多余的,但他们都有自己的用武之地!在这个示例中,`String::from` 和 `to_string` 做的是同一件事,所以咱们选择哪个只是风格与可读性的问题。 + +请记住,字符串是 UTF-8 编码的,因此我们可以在其中包含任何正确编码的数据,如下清单 8-14 所示。 ```rust let hello = String::from("السلام عليكم"); @@ -75,21 +79,20 @@ Rust 标准库还包含了一些其他字符串类型,比如 `OsString`、`OsS let hello = String::from("👋"); ``` -*清单 8-14:以不同语言在字符串中存储问候语* +*清单 8-14:以字符串存储不同语言的问候语* -全部这些都是有效的 `String` 值。 +所有这些都是有效的 `String` 值。 -## 更新 `String` +## 更新字符串 + +在咱们向某个 `String` 中压入更多数据时,其大小和内容都会发生变化,就像 `Vec` 的内容一样。此外,咱们还可以方便地使用 `+` 运算符或 `format!` 宏,连接多个 `String` 值。 -在将更多数据压入到其中时,`String` 可以增长大小,且就跟 `Vec` 的内容一样,内容可以改变。此外,还可以方便地使用 `+` 运算符或 `format!` 宏,来连接一些 `String` 值。 +### 使用 `push_str` 与 `push` 追加内容到某个 `String` -### 使用 `push_str` 与 `push`,往 `String` 追加数据 - - -通过使用 `push_str` 方法来追加一个字符串切片,就可以增大 `String`,如下清单 8-15 中所示的那样。 +如下清单 8-15 所示,通过使用 `push_str` 方法追加一个字符串切片,我们可以增长某个 `String`。 ```rust let mut s = String::from("foo"); @@ -97,63 +100,72 @@ Rust 标准库还包含了一些其他字符串类型,比如 `OsString`、`OsS println! ("{}", s); ``` -*清单 8-15:使用 `push_str` 方法将一个字符串切片追加到某个 `String`* +*清单 8-15:使用 `push_str` 方法将字符串切片追加到某个 `String`* + + +在这两行后,`s` 将包含 `foobar`。`push_str` 方法会取个字符串切片,因为我们并不会想要取得那个参数的所有权。例如,在下面清单 8-16 中的代码中,我们打算在将 `s2` 的内容追加到 `s1` 后,能够使用 `s2`。 -在这两行之后,`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 为 {}", s2); + + println! ("{}", s2); ``` -*清单 8-16:在将一个字符串切片的内容追加到某个 `String` 后再对其加以使用* +*清单 8-16:在将其内容追加到某个 `String` 后,使用该字符串切片* -若这个 `push_str` 方法取得了 `s2` 的所有权,那么这里就无法在最后一行打印其值。然而,这段代码正如预期那样运作了! -相比 `push_str` 方法,这个 `push` 方法则会取单个字符作为参数,并将其添加到 `String`。下面清单 8-17 就使用这个 `push` 方法,将字母 "l" 添加到了一个 `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`* +*清单 8-17:使用 `push` 将一个字符添加到 `String`* -作为上面代码的结果,`s` 将包含 `lol`。 +作为结果,`s` 将包含 `lol`。 ### 使用 `+` 运算符或 `format!` 宏的字符串连接 **Concatenation with the `+` Operator or the `format!` Macro** +通常情况下,咱们会想要合并两个现有字符串。一种方法是使用 `+` 运算符,如下清单 8-18 所示。 -通常,会想要将两个既有字符串合在一起。完成此操作的一种方式,就是使用 `+` 运算符,如下清单 8-18 中所示。 ```rust let s1 = String::from("Hello, "); let s2 = String::from("world!"); - let s3 = s1 + &s2; // 请注意这里的 s1 已被迁移,而不再能被使用了 + let s3 = s1 + &s2; // 请注意这里 s1 已被迁移,而不能再被使用 ``` -*清单 8-18:运用 `+` 运算符来将两个 `String` 值结合为一个新的 `String` 值* +*清单 8-18:使用 `+` 运算符合并两个 `String` 值为新的 `String` 值* + + +字符串 `s3` 将包含 `Hello, world!`。加法运算后,字符串 `s1` 不再有效的原因,以及我们使用到 `s2` 引用的原因,都与我们使用 `+` 运算符时调用的方法签名有关。`+` 运算符使用了 `add` 方法,该方法的签名如下所示: -这个字符串 `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`。在这里,我们代入了具体类型,这就是我们对 `String` 值调用此方法时的情况。我们将在第 10 章讨论 [泛型](../Ch10_Generic_Types_Traits_and_Lifetimes.md)。这个签名为我们提供了理解 `+` 这个运算符中,棘手部分所需的线索。 -这里之所以能在到 `add` 的调用中使用 `&s2` 的原因,在于编译器可将那个 `&String` 参数,*强制转换* 为 `&str` 类型。在调用 `add` 方法时,Rust 使用了 *解引用强制转换(deref coercion)* 特性,在这里该特性就将 `&s2` 转换为了 `&s2[..]`。在第 15 章就将深入讨论这个解引用强制转换。由于 `add` 方法并未占据那个 `s` 参数的所有权,因此 `s2` 在此运算之后,仍将有效。 +首先,`s2` 带有一个 `&`,这意味着我们是将第二个字符串的 *引用*,加到第一个字符串。这是因为 `add` 函数中的那个 `s` 参数:我们只能将一个 `&str` 加到某个 `String`;我们不能将两个 `String` 值加到一起。但是等等,`&s2` 的类型为 `&String`,而不是 `&str`,正如 `add` 的第二个参数所指定的那样。那么为什么清单 8-18 会编译呢? -其次,这里可以看到,在该方法签名中,由于 `self` *没有* `&`,那么 `add` 就取得了 `self` 的所有权。这就意味着清单 8-18 中的 `s1` 将被迁移到那个 `add` 调用中,并在那之后便不再有效。这样看来,尽管 `let s3 = s1 + &s2;` 这个语句看起来将同时拷贝这两个字符串,并创建一个新的字符串,不过此语句实际上是要取得 `s1` 的所有权,追加 `s2` 内容的一份拷贝,进而随后返回该运算结果的所有权。也就是说,看起来这行语句构造了很多拷贝,但并没有;这样的实现比拷贝更为高效。 -在需要连接多个字符串时,这个 `+` 运算符的行为就变得笨拙了: +我们能够在到 `add` 调用中使用 `&s2` 的原因是,编译器可以将 `&String` 参数,强制转换为 `&str`。当我们调用 `add` 方法时,Rust 会使用 *解引用强制转换*,在这里其会将 `&s2` 转换为 `&s2[..]`。我们将在第 15 章更详细地讨论 [解引用强制转换](../smart_pointers/deref-t.md)。由于 `add` 方法不会取得参数 `s` 的所有权,因此在这个操作后,`s2` 仍将是个有效的 `String` 值。 + +其次,我们可以在这个签名中看到,`add` 函数会取得 `self` 的所有权,因为 `self` *没有* `&`。这意味着列表 8-18 中的 `s1`,将被迁移到 `add` 这个调用中,并且在调用之后将不再有效。因此,尽管 `let s3 = s1 + &s2;` 看起来像是会拷贝两个字符串,并创建出一个新字符串,但实际上该语句会取得 `s1` 的所有权,将 `s2` 内容的一份副本追加到 `s1` 上,然后返回结果的所有权。换句话说,虽然看起来像是进行了多次拷贝,但实际上并非如此;该实现比拷贝更高效。 + +在我们需要连接多个字符串时,`+` 运算符的行为会变得笨重: ```rust let s1 = String::from("tic"); @@ -163,7 +175,8 @@ fn add(self, s: &str) -> String let s = s1 + "-" + &s2 + "-" + &s3; ``` -此处 `s` 将为 `tic-toc-toe`。由于有些全部的 `+` 与 `"` 字符,因此就难于看清发生了什么。对于较复杂的字符串合并,可这个 `format!` 宏: +此时,`s` 将是 `tic-tac-toe`。由于包含大量 `+` 和 `"` 字符,很难看清具体情况。对于更复杂的字符串合并,我们可以改用 `format!` 这个宏: + ```rust let s1 = String::from("tic"); @@ -173,24 +186,25 @@ fn add(self, s: &str) -> String let s = format! ("{}-{}-{}", s1, s2, s3); ``` -这段代码同样把 `s` 设置为了 `tic-toc-toe`。这个 `format!` 宏与 `println!` 宏的运作类似,而与将输出打印到屏幕不同,他会将结果内容,以一个 `String` 加以返回。使用 `format!` 这个版本的代码,读起来容易得多,且由于 `format!` 宏所生成的代码,使用的是引用,那么这个调用就不会占据任何一个其参数的所有权。 + +这段代码也会将 `s` 设置为 `tic-toc-toe`。`format!` 宏与 `println!` 类似,但他不会将输出打印到屏幕上,而是返回一个包含内容的 `String`。使用 `format!` 的代码版本更加易于阅读,且由 `format!` 宏生成的代码使用了引用,因此这个调用不会取得其任何参数的所有权。 -## 索引到字符串内部 +## 字符串的索引 **Indexing into Strings** +在许多其他编程语言中,通过索引引用字符串中的单个字符,是种有效且常见的操作。然而,如果咱们尝试在 Rust 中使用索引语法,访问某个 `String` 的一些部分,咱们将得到一个报错。请设想下面列表 8-19 中的无效代码。 -再许多其他编程语言中,经由通过索引而引用字符串中的一些单独字符,都是有效且常见的操作。不过在 Rust 中,当尝试使用索引来访问某个 `String` 的一些部分时,就会收到错误。请考虑下面清单 8-19 中的无效代码。 ```rust let s1 = String::from("hello"); let h = s1[0]; ``` -*清单 8-19:尝试在某个字符串上使用索引语法* +*清单 8-19:尝试对某个 `String` 使用索引语法* -该代码将引发下面的错误: +这段代码将导致一些报错: ```console $ cargo run @@ -207,7 +221,7 @@ For more information about this error, try `rustc --explain E0277`. error: could not compile `string_demo` due to previous error ``` -这个报错和提示讲清了缘由:Rust 的字符串不支持索引。但为什么不支持呢?要回到这个问题,就要探讨一下 Rust 是怎样在内存中存储字符串的。 +该报错与注释,揭示了问题所在:Rust 字符串不支持索引操作。但为什么不支持呢?要回答这个问题,我们需要讨论 Rust 是如何在内存中存储字符串的。 ### 内部表示 @@ -215,68 +229,75 @@ error: could not compile `string_demo` due to previous error **Internal Representation** -`String` 是对 `Vec` 的一种封装(a `String` is a wrapper over a `Vec`)。下面来看看清单 8-14 中,那里的一些以 UTF-8 良好编码的示例字符串。首先是这个: +`String` 是对 `Vec` 的封装。我们来看看列表 8-14 中,咱们的那些正确编码的 UTF-8 示例字符串。首先,这个: ```rust let hello = String::from("Hola"); ``` -在此示例中,`len` 将为 `4`,这表示这个存储着字符串 “Hola” 的矢量长度为 4 个字节。这些字母在以 UTF-8 编码时,每个占用 1 个字节。而接下来的这行,就会惊讶到你了。(请注意这个字符串是以大写西里尔字母 `Ze` 开头,而非阿拉伯数字 `3`。) +在这种情况下,`len` 值将为 `4`,这意味着存储字符串 `"Hola"` 的矢量值长度为 4 字节。在以 UTF-8 编码时,这些字母每个占用一字节。然而,下面这行代码可能会让咱们感到意外(请注意,这个字符串以大写的西里尔字母 *Ze* 开头,而非数字 3): + ```rust let hello = String::from("Здравствуйте"); ``` -在被问及这个字符串有多长时,你可能会讲是 `12`。事实上,Rust 的答案是 `24`:由于那个字符串中的每个 Unicode 标量值,都有占用 2 字节的存储,故那就是以 UTF-8 编码 `Здравствуйте` 所用占用的字节数。由于这个缘故,到该字符串的那些字节的所以,就不会总是对应到某个有效的 Unicode 标量值了。为对此加以演示,请设想下面这段无效的 Rust 代码: +在咱们被问及该字符串的长度时,你可能会说 12。事实上,Rust 的答案是 24:这是以 UTF-8 编码 “Здравствуйте” 所需的字节数,因为该字符串中的每个 Unicode 标量值,都需要 2 个字节的存储空间。因此,对字符串字节的索引,并不总是对应于有效的 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`。 +咱们已经知道 `answer` 不会是 `З`,即第一个字母。当以 UTF-8 编码时,`З` 的第一个字节是 `208`,第二个字节是 `151`,因此 `answer` 似乎应是 `208`,但 `208` 本身并不是个有效的字符。在用户询问该字符串的首字母时,返回 `208` 可能并非用户所期望的结果;然而,这是 Rust 在字节索引 `0` 处唯一拥有的数据。用户通常不想要返回的字节值,即使该字符串仅包含拉丁字母:若 `&"hi"[0]` 是返回字节值的有效代码,他将返回 `104`,而非 `h`。 -那么答案就是,为避免返回一个不期望的值,以及避免引发一些可能无法立即发现的程序错误,Rust 就根本不会编译此代码,并阻止了在开发过程早期阶段的这些误解。 +因此,答案是:为避免返回非预期值,而引发可能不会立即被发现的错误,Rust 根本不会编译这段代码,从而在开发过程早期就防止误解。 ### 字节、标量值与字素簇!我的天! -**Bytes and Scalar Values and Grapheme Clusters! Oh My!)** +**Bytes and Scalar Values and Grapheme Clusters! Oh My!** -有关 UTF-8 的另一点,即为从 Rust 视角看待字符串,事实上有三种相关方式:视为字节、标量值与字素簇(而字素簇则是与我们称之为 *文字/letters* 的东西的最接近的事物了)。 +关于 UTF-8 的另一要点,是从 Rust 角度来看,实际上存在三种相关的字符串表示方式: -在看到以梵文字书写的印地语词汇 `"नमस्ते"` 时,这个词汇是以看起来像下面这样的一些 `u8` 类型值的矢量存储的: +- 字节; +- 标量值 +- 以及字素簇(与我们所说的 *字母* 最接近的概念)。 + +在我们看到以梵文,Devanagari,书写的印地语单词 “नमस्ते” 时,他会被存储为一个 `u8` 值的矢量,看起来像这样: ```rust [224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135] ``` -这是 18 个字节,也是计算机最终存储该数据的方式。而在将他们视为 Unicode 的标量值,即 Rust 的 `char` 类型时,则这些字节看起来是这样的: +这是 18 个字节,也是计算机最终存储这个数据的方式。若我们将其视为 Unicode 的标量值,即 Rust 的 `char` 类型,那么这些字节看起来如下: ```rust ['न', 'म', 'स', '्', 'त', 'े'] ``` -这里就有了六个 `char` 值了,但其中第四与第六个并不是文字(letters):他们是自身并无意义的变音符号。最后,在将这些 Unicode 标量值视为字素簇时,就得到了人类所称呼的、四个构成了那个印地词语的四个文字: +这里有六个 `char` 值,但第四和第六个不属于字母:他们是独立存在时没有意义的音标符号。最后,在我们将其视为字素簇是,我们就会得到人类所说的,构成印地语单词的四个字母: + ```rust ["न", "म", "स्", "ते"] ``` -Rust 提供了解析计算机存储的原始字符串数据的数种不同方式,因此各个程序就可以选择他所需的解析方式,这与该数据为何种人类语言无关。 - -Rust 不允许索引进入 `String` 来获取某个字符的终极原因,即是索引操作,被认为总是消耗固定的时间(即 `O(1)`)。但由于 Rust 将不得不从开头遍历到索引位置,来确定那里有多少个有效字符,由此对于在 `String` 上执行索引操作,所消耗的时间是无法确保持续一致的。 +Rust 提供了解析计算机所存储原始字符串数据的多种方法,以便每个程序都能根据需要选择合适的解析方式,而无论数据使用的何种人类语言。 -## 对字符串进行切片操作 +Rust 不允许我们通过索引访问字符串中某个字符的最后一个原因,是索引操作被认为始终会消耗常数时间的复杂度(即 `O(1)` )。但对于 `String` 而言,无法保证这种性能,因为 Rust 必须从开头遍历到索引位置的内容,以确定其间有效字符的数量。 + + +## 字符串切片 **Slicing Strings** +字符串索引操作通常是个糟糕的主意,因为字符串索引操作的返回类型并不明确:是字节值、字符、字素簇,还是个字符串切片。因此,在咱们确实需要使用索引,创建字符串切片时,Rust 会要求咱们更加明确。 -由于字符串索引操作的返回值类型不明朗:可能是字节值、字符、字素簇,或者字符串切片,因此索引到字符串中去,通常是个糟糕的主意。而在确实需要使用索引来创建字符串切片时,那么 Rust 就要求提供更具体的索引。 - -与使用带有单个数字的 `[]` 相比,可使用带有范围的 `[]`,来创建包含一些特定字节的字符串切片: +与其将 `[]` 与单个数字一起使用,咱们可将 `[]` 与某个范围一起使用,创建出包含特定字节的字符串切片: ```rust let hello = String::from("Здравствуйте"); @@ -284,23 +305,30 @@ Rust 不允许索引进入 `String` 来获取某个字符的终极原因,即 let s = &hello[0..4]; ``` -这里的 `s` 将是个包含该字符串头 4 个字节的 `&str`。早先曾提到过,每个的这些字符都是 2 字节,那么这就意味着 `s` 将为 `Зд`。 +这里,`s` 将是个包含着该字符串前四个字节的 `&str`。早先我们提到,这些字符是两个字节,这意味着 `s` 将是 `Зд`。 + +若我们尝试以类似 `&hello[0..1]` 的方式,只切取该某个字符字节部分时,Rust 将以访问某个矢量值中无效索引时的同一方式终止运行: -而在尝试使用类似 `&hello[0..1]` 这样的操作,来对某个字符的那些字节的一部分进行切片时,Rust 就会在运行时,以与在矢量中访问无效索引时同样的方式终止运行: ```console -thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`'... +$ cargo run + Compiling string_demo v0.1.0 (/home/hector/rust-lang-zh_CN/projects/string_demo) + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s + Running `target/debug/string_demo` +thread 'main' panicked at src/main.rs:3:19: +byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте` ``` -由于使用范围来创建出字符串切片这样的操作,可能将程序崩溃掉,因此进行这样操作时应小心谨慎。 +在以范围创建字符串切片时,咱们应谨慎操作,因为此类操作可能导致咱们的程序崩溃。 -## 迭代字符串的一些方法 +## 迭代字符串的方法 **Methods for Iterating Over Strings** -在字符串的各个片段上进行操作的最好方式,就是显示地指明是要字符还是字节。对于单独的 Unicode 标量值,就使用 `chars` 方法。在 `नमस्ते` 上调用 `chars`,就会分理出并返回六个类型 `char` 的值来,进而就可以对结果进行迭代,而访问到各个元素: +对字符串片段进行操作的最佳方式,是明确指定咱们是要字符还是字节。对于单个的 Unicode 标量值,就要使用 `chars` 方法。对 “Зд” 调用 `chars` 方法,会将其拆分并返回两个 `char` 类型的值,咱们就可以遍历结果,访问每个元素: + ```rust for c in "नमस्ते".chars() { @@ -308,7 +336,7 @@ thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З } ``` -该代码将打印下面的东西: +这段代码将打印以下内容: ```rust न @@ -319,50 +347,42 @@ thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З ``` -此外,`bytes` 方法返回的则是各个原始字节,对与特定领域,这方法可能正好: +或者,`bytes` 方法会返回可能适合咱们领域的各个原始字节: ```rust - for b in s.bytes() { - println!("{}", b); + for b in "Зд".bytes() { + println!("{b}"); } ``` 该代码将打印出构成这个 `String` 的 18 个字节来: ```console -224 -164 -168 -224 -164 -174 -224 -164 -184 -224 -165 -141 -224 -164 -164 -224 -165 -135 +208 +151 +208 +180 ``` -不过要确保记住,有效的 Unicode 标量值可能有多余 1 个字节组成。 +但请务必记住,有效的 Unicode 标量值可能由多个字节组成。 + + +从字符串中获取字素簇,如上面对梵文那样,较为复杂,因此标准库未提供此功能。若咱们需要此功能,可在 [crates.io](https://crates.io/) 上找到相关代码箱。 -而从字符串获取字素簇则是复杂的,因此这项功能并未由标准库提供。在需要该功能时,在 [crates.io](https://crates.io/) 上有一些可用的代码箱。 ## 字符串并不简单 **Strings Are Not So Simple** +总而言之,字符串是复杂的。不同编程语言在向程序员呈现这种复杂性方面,会做出不同的选择。Rust 选择将正确处理 `String` 数据,作为所有 Rust 程序的默认行为,这意味着程序员在开发初期,必须更多地考虑如何处理 UTF-8 数据。这种权衡虽然比其他编程语言暴露了更多字符串的复杂性,但他防止了咱们在开发生命周期的后期,不得不处理那些涉及非 ASCII 字符的错误。 -总的来说,字符串是复杂的。不同编程语言,在以何种程度将这种复杂度呈现给编程者上,做出了不同的选择。Rust 选择了将正确处理 `String` 数据,作为所有 Rust 程序的默认行为,这就意味着 Rust 程序员就必须在处理 UTF-9 数据时,要提前投入更多思考。这种权衡暴露了相较于其他编程语言,更多的字符串复杂度,但这防止了在软件开发生命周期后期,将涉及到的非 ASCII 字符的错误处理。 +好消息是,标准库提供了大量基于 `String` 与 `&str` 类型的功能,帮助正确处理这些复杂情况。建议查阅文档,了解一些有用方法,例如用于在字符串中检索的 `contains` 方法,以及用于将字符串的一部分替换为另一字符串的 `replace` 方法等。 -接下来就要换到,有点复杂的东西:哈希图! + + + +让我们转向一个稍微简单一点的主题:哈希图! (End)