使用字符串存储 UTF-8 编码的文本

我们在第 4 章中讨论了字符串,但现在我们将更深入地研究它们。 新 Rustacean 通常会卡在三个组合的字符串上 原因:Rust 倾向于暴露可能的错误,字符串是 复杂的数据结构比许多程序员认为的要复杂,并且 UTF-8 的 UTF 格式。这些因素以一种似乎很困难的方式结合在一起,当您 来自其他编程语言。

我们在集合的上下文中讨论字符串,因为字符串是 实现为字节集合,加上一些方法以提供有用的 功能。在本节中,我们将 谈论String,每个集合类型都具有,例如 创建、更新和读取。我们还将讨论String与其他集合不同,即索引如何索引到String是 因人和计算机解释方式的差异而变得复杂String数据。

什么是字符串?

我们首先定义术语 string 的含义。Rust 只有一个字符串 键入核心语言,即字符串 slicestr这通常是 以借用的形式&str.在第 4 章中,我们讨论了字符串 slices, 这些字符串是对存储在其他位置的某些 UTF-8 编码字符串数据的引用。字符串 例如,文本存储在程序的二进制文件中,因此 string 切片。

Stringtype,它由 Rust 的标准库提供,而不是 编码为核心语言,是一种可增长、可变、拥有的 UTF-8 编码 string 类型。当 Rustacean 在 Rust 中提到 “strings” 时,它们可能是 引用String或字符串 slice&str类型,而不仅仅是一个 这些类型。虽然本节主要是关于String,两种类型都是 在 Rust 的标准库中大量使用,并且两者String和字符串切片 是 UTF-8 编码的。

创建新字符串

许多相同的作可用于Vec<T>可用于String也是因为String实际上是作为向量的包装器 字节数,并具有一些额外的保证、限制和功能。示例 的函数中,该函数的工作方式与Vec<T>Stringnew函数创建一个实例,如示例 8-11 所示。

fn main() {
    let mut s = String::new();
}

示例 8-11:创建一个新的、空的String

此行将创建一个名为s,然后我们可以将 数据。通常,我们会有一些初始数据,我们想要从这些数据开始 字符串。为此,我们使用to_string方法,该方法可用于任何类型 实现Displaytrait 的 intent 中,就像字符串字面量一样。示例 8-12 显示 两个例子。

fn main() {
    let data = "initial contents";

    let s = data.to_string();

    // the method also works on a literal directly:
    let s = "initial contents".to_string();
}

示例 8-12:使用to_string方法创建Stringfrom 字符串文本

此代码创建一个包含initial contents.

我们也可以使用函数String::from要创建一个String从字符串 字面。示例 8-13 中的代码等同于示例 8-12 中的代码 使用to_string.

fn main() {
    let s = String::from("initial contents");
}

示例 8-13:使用String::from函数创建 一个Stringfrom 字符串文本

因为字符串用于很多事情,所以我们可以使用许多不同的泛型 字符串的 API,为我们提供了很多选择。他们中的一些似乎 多余,但他们都有自己的位置!在这种情况下,String::fromto_string做同样的事情,所以你选择哪一个是一个风格问题,并且 可读性。

请记住,字符串是 UTF-8 编码的,因此我们可以包含任何正确编码的 data 中,如示例 8-14 所示。

fn main() {
    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");
}

示例 8-14:在 字符串

所有这些都是有效的String值。

更新字符串

一个String大小可以增大,其内容可以更改,就像内容一样 的Vec<T>,如果您将更多数据推送到其中。此外,您可以方便地 使用运算符或+format!宏进行连接String值。

附加到 String 中push_strpush

我们可以种植一个String通过使用push_str附加字符串 slice 的方法, 如示例 8-15 所示。

fn main() {
    let mut s = String::from("foo");
    s.push_str("bar");
}

示例 8-15:将字符串 slice 附加到String使用push_str方法

在这两行之后,s将包含foobar.这push_strmethod 采用 string slice 的 Fragment 中,因为我们不一定想获得 参数。例如,在示例 8-16 的代码中,我们希望能够使用s2将其内容附加到s1.

fn main() {
    let mut s1 = String::from("foo");
    let s2 = "bar";
    s1.push_str(s2);
    println!("s2 is {s2}");
}

示例 8-16:在附加字符串 slice 后使用 contents 添加到String

如果push_strmethod 获取了s2,我们将无法打印 它在最后一行的值。但是,此代码的工作方式符合我们的预期!

push方法将单个字符作为参数,并将其添加到String.示例 8-17 将字母 l 添加到 aString使用push方法。

fn main() {
    let mut s = String::from("lo");
    s.push('l');
}

示例 8-17:向String价值 用push

因此,s将包含lol.

与 Operator 或+format!

通常,您需要合并两个现有字符串。一种方法是使用 运算符,如示例 8-18 所示。+

fn main() {
    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used
}

示例 8-18:使用 operator 将两个+String值转换为新的String价值

字符串s3将包含Hello, world!.原因s1不再 valid 的,以及我们使用对s2,必须 替换为我们使用运算符时调用的方法的签名。 该运算符使用++add方法,其签名类似于 这:

fn add(self, s: &str) -> String {

在标准库中,您将看到add使用泛型和关联的 类型。在这里,我们替换了具体类型,这就是当我们 使用String值。我们将在第 10 章讨论泛型。 这个签名为我们提供了理解棘手之处所需的线索 运算符的位。+

第一s2具有 ,表示我们正在添加第二个 string 设置为第一个字符串。这是因为&s参数中的add功能:我们只能添加一个&str更改为String;我们不能添加两个String值一起。但是等等 - 类型&s2&String&str如 在第二个参数中指定为add.那么,为什么示例 8-18 会编译呢?

我们能够使用&s2在对add是编译器 可以强制&String参数转换为&str.当我们调用add方法,Rust 使用解引用强制转换,这里将&s2&s2[..]. 我们将在第 15 章中更深入地讨论 deref coercion。因为adddoes 不获取s参数s2仍将是有效的String在此作之后。

其次,我们可以在签名中看到add取得self因为self没有 .这意味着&s1在示例 8-18 中将是 移动到addcall 调用,之后将不再有效。所以,尽管let s3 = s1 + &s2;看起来它会复制两个字符串并创建一个新字符串, 此声明实际上拥有s1附加内容的副本 之s2,然后返回结果的所有权。换句话说,它看起来 就像它制作了很多副本,但事实并非如此;实现方式更 比复制更有效。

如果我们需要连接多个字符串,则运算符 变得笨拙:+

fn main() {
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = s1 + "-" + &s2 + "-" + &s3;
}

此时,s将是tic-tac-toe.面对所有的 and 角色,很难看出发生了什么。用于组合字符串 更复杂的方式,我们可以改用+"format!宏:

fn main() {
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = format!("{s1}-{s2}-{s3}");
}

此代码还将stic-tac-toe.这format!macro 的工作原理类似于println!,但它不是将输出打印到屏幕,而是返回一个String与内容物。使用format!很多 更易于阅读,并且由format!宏使用引用 ,因此此调用不会获得其任何参数的所有权。

索引到字符串中

在许多其他编程语言中,访问 string 是一种有效且常见的作。然而 如果您尝试访问String使用 Rust 中的索引语法,你将 收到错误。考虑示例 8-19 中的无效代码。

fn main() {
    let s1 = String::from("hello");
    let h = s1[0];
}

示例 8-19:尝试将索引语法与 字符串

此代码将导致以下错误:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
error[E0277]: the type `str` cannot be indexed by `{integer}`
 --> src/main.rs:3:16
  |
3 |     let h = s1[0];
  |                ^ string indices are ranges of `usize`
  |
  = help: the trait `SliceIndex<str>` is not implemented for `{integer}`, which is required by `String: Index<_>`
  = note: you can use `.chars().nth()` or `.bytes().nth()`
          for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings>
  = help: the trait `SliceIndex<[_]>` is implemented for `usize`
  = help: for that trait implementation, expected `[_]`, found `str`
  = note: required for `String` to implement `Index<{integer}>`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `collections` (bin "collections") due to 1 previous error

错误和注释说明了问题:Rust 字符串不支持索引。但 为什么不呢?要回答这个问题,我们需要讨论 Rust 如何将字符串存储在 记忆。

内部表示

一个StringVec<u8>.让我们看看我们的一些适当的 示例 8-14 中编码的 UTF-8 示例字符串。首先,这个:

fn main() {
    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");
}

在这种情况下,len将是4,这意味着存储字符串"Hola"长度为 4 字节。这些字母中的每一个在编码时都占用一个字节 UTF-8 的 UTF 格式。但是,以下行可能会让您感到惊讶(请注意,此字符串 以大写的西里尔字母 Ze 开头,而不是数字 3):

fn main() {
    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");
}

如果有人问你字符串有多长,你可能会说 12。事实上,Rust 的 答案是 24:这是在 UTF-8,因为该字符串中的每个 Unicode 标量值占用 2 个字节的 存储。因此,字符串字节的索引并不总是相关的 设置为有效的 Unicode 标量值。为了演示,请考虑这个无效的 Rust 法典:

let hello = "Здравствуйте";
let answer = &hello[0];

您已经知道了answer不会З、第一个字母。编码时 在 UTF-8 中,第一个字节З208第二个是151,因此 看起来answer实际上应该是208208不是有效字符 靠自己。返回208如果用户要求 对于此字符串的第一个字母;然而,这是 Rust 在字节索引 0 处。用户通常不希望返回 byte 值,即使 如果字符串仅包含拉丁字母:如果&"hello"[0]是有效代码 返回 byte 值,它将返回104h.

那么,答案是,为了避免返回意外值并导致 可能不会立即发现的 bug,Rust 不会编译此代码 完全可以防止在开发过程的早期产生误解。

字节和标量值以及字形集群!天哪!

关于 UTF-8 的另一点是,实际上有三种相关的方式 从 Rust 的角度来看字符串:字节、标量值和字素 簇(最接近我们所说的字母的东西)。

如果我们看一下梵文书写的印地语单词“नमस्ते”,它是 存储为u8值,如下所示:

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]

这是 18 字节,是计算机最终存储这些数据的方式。如果我们看一下 它们作为 Unicode 标量值,这就是 Rust 的chartype 是,那些 bytes 如下所示:

['न', 'म', 'स', '्', 'त', 'े']

共有 6 个char值,但第四个和第六个不是字母: 它们是变音符号,本身没有意义。最后,如果我们看一下 它们作为字素簇,我们会得到人们所说的四个字母 组成印地语单词:

["न", "म", "स्", "ते"]

Rust 提供了不同的方法来解释计算机 store 中,以便每个程序都可以选择它需要的解释,无论 数据采用的人类语言。

最后一个原因 Rust 不允许我们索引到String要获取 字符是索引作应始终花费恒定时间 (O(1)).但是,无法保证使用String, 因为 Rust 必须从头开始遍历内容到 index 来确定有多少个有效字符。

切片字符串

索引到字符串中通常是一个坏主意,因为不清楚 字符串索引作的返回类型应为:一个字节值、一个 字符、字形簇或字符串切片。如果您确实需要使用 indices 来创建字符串 slices,因此,Rust 要求您更具体。

您可以与 range 创建包含特定字节的字符串切片:[][]

#![allow(unused)]
fn main() {
let hello = "Здравствуйте";

let s = &hello[0..4];
}

这里s将是一个&str,其中包含字符串的前四个字节。 前面我们提到过,这些字符中的每一个都是两个字节,这意味着s将是Зд.

如果我们尝试用类似&hello[0..1]时,Rust 会在运行时 panic,就像一个无效的 index 的 S S S

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/collections`
thread 'main' panicked at src/main.rs:4:19:
byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

在创建具有范围的字符串切片时,您应该小心,因为执行 所以可能会使您的程序崩溃。

迭代字符串的方法

对字符串片段进行作的最佳方法是明确说明 您需要字符或字节。对于单个 Unicode 标量值,请使用chars方法。叫chars在 “Зд” 上分离并返回两个值 类型char,您可以迭代结果以访问每个元素:

#![allow(unused)]
fn main() {
for c in "Зд".chars() {
    println!("{c}");
}
}

此代码将打印以下内容:

З
д

或者,bytesmethod 返回每个原始字节,这可能是 适合您的域:

#![allow(unused)]
fn main() {
for b in "Зд".bytes() {
    println!("{b}");
}
}

此代码将打印组成此字符串的四个字节:

208
151
208
180

但请务必记住,有效的 Unicode 标量值可能由多个 比 1 个字节。

与 Devanagari 脚本一样,从字符串中获取字形簇是 complex,因此 standard 库不提供此功能。箱子 在 crates.io 上可用,前提是这是 您需要的功能。

字符串不是那么简单

总而言之,字符串很复杂。不同的编程语言使 关于如何将这种复杂性呈现给程序员的不同选择。锈 已选择对Stringdata 的默认行为 对于所有 Rust 程序,这意味着程序员必须投入更多的思考 预先处理 UTF-8 数据。这种权衡暴露了 字符串,但它会阻止您 无需稍后在 开发生命周期。

好消息是,标准库提供了许多构建的功能 从String&str类型来帮助处理这些复杂情况 正确。请务必查看文档以了解有用的方法,例如contains用于在字符串中搜索,使用replace用于替换 a 的部分 string 替换为另一个 string。

让我们切换到稍微简单一点的东西:哈希映射!

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准