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

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

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

什么是字符串?

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

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

创建新字符串

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

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

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

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

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 方法从字符串字面量创建 String

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

我们还可以使用该函数从字符串创建 字面。示例 8-13 中的代码等同于示例 8-12 中的代码 使用 .String::fromStringto_string

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

示例 8-13:使用 String::from 函数创建 来自字符串文本的 String

因为字符串用于很多事情,所以我们可以使用许多不同的泛型 字符串的 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

更新字符串

A 的大小可以增长,其内容可以更改,就像内容一样 ,如果你将更多数据推送到其中。此外,您可以方便地 使用 Operator 或 Macro 连接值。StringVec<T>+format!String

使用 push_strpush 追加到 String

我们可以通过使用该方法来附加一个字符串 slice, 如示例 8-15 所示。Stringpush_str

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

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

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

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_strs2

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

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

示例 8-17:向 String 值添加一个字符 使用 Push

因此,将包含 .slol

使用 + 运算符或 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:使用 + 运算符将两个 String 值组合成一个新的 String

字符串将包含 .原因不再是 有效,并且我们使用对 的引用的原因 ,必须 替换为我们使用运算符时调用的方法的签名。 运算符使用该方法,其签名类似于 这:s3Hello, world!s1s2++add

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

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

第一个 , 具有 ,这意味着我们正在添加第二个 string 设置为第一个字符串。这是因为函数中的参数:我们只能将 a 添加到 a ;我们不能将两个值相加。但是等等 — 的类型是 ,而不是 ,因为 在第二个参数中指定为 。那么,为什么示例 8-18 会编译呢?s2&sadd&strStringString&s2&String&stradd

我们能够在调用 to 的原因是编译器 可以将参数强制转换为 .当我们调用该方法时,Rust 使用 deref 强制转换,这里变成了 . 我们将在第 15 章中更深入地讨论 deref coercion。因为 not take ownership 的参数,此作后仍将有效。&s2add&String&stradd&s2&s2[..]addss2String

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

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

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

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

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

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

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

此代码也设置为 .宏的工作方式与 类似,但它不是将输出打印到屏幕上,而是返回包含内容的 a。使用的代码版本很多 更易于阅读,并且宏生成的代码使用引用 ,因此此调用不会获得其任何参数的所有权。stic-tac-toeformat!println!Stringformat!format!

索引到字符串中

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

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 如何将字符串存储在 记忆。

内部表示

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

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");
}

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

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];

您已经知道 ,不会是 ,第一个字母。编码时 在 UTF-8 中,第一个字节是 ,第二个字节是 ,因此它将 似乎 实际上应该是 ,但 不是有效字符 靠自己。如果用户提出要求,返回可能不是他们想要的 对于此字符串的第一个字母;然而,这是 Rust 在字节索引 0 处。用户通常不希望返回 byte 值,即使 如果字符串仅包含拉丁字母:如果 were 有效代码 返回 byte 值,它将返回 ,而不是 。answerЗЗ208151answer208208208&"hello"[0]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 的类型,那些 bytes 如下所示:char

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

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

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

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

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

切片字符串

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

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

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

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

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

如果我们尝试用类似 的东西只切分字符字节的一部分,Rust 会在运行时 panic 就像一个无效的 index 的 S S S&hello[0..1]

$ 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 标量值,请使用 method.调用 “Зд” 会分离出来并返回两个值 type 中,你可以迭代结果以访问每个元素:charscharschar

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

此代码将打印以下内容:

З
д

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

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

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

208
151
208
180

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

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

字符串不是那么简单

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

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

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

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