使用字符串存储 UTF-8 编码的文本
我们在第 4 章中讨论了字符串,但现在我们将更深入地研究它们。 新 Rustacean 通常会卡在三个组合的字符串上 原因:Rust 倾向于暴露可能的错误,字符串是 复杂的数据结构比许多程序员认为的要复杂,并且 UTF-8 的 UTF 格式。这些因素以一种似乎很困难的方式结合在一起,当您 来自其他编程语言。
我们在集合的上下文中讨论字符串,因为字符串是
实现为字节集合,加上一些方法以提供有用的
功能。在本节中,我们将
讨论每个集合类型都有的作,例如
创建、更新和读取。我们还将讨论 is 与其他集合的不同之处,即 a 的索引是怎样的
由于人和计算机解释数据的方式不同而变得复杂。String
String
String
String
什么是字符串?
我们首先定义术语 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 编码的。String
String
&str
String
String
创建新字符串
许多可用的相同作也可用于 as,因为实际上是作为 vector 的包装器实现的
字节数,并具有一些额外的保证、限制和功能。示例
的函数,该函数的工作方式与 和 是创建实例的函数,如示例 8-11 所示。Vec<T>
String
String
Vec<T>
String
new
fn main() { let mut s = String::new(); }
此行创建一个名为 的新空字符串,然后我们可以将
数据。通常,我们会有一些初始数据,我们想要从这些数据开始
字符串。为此,我们使用 Job,该方法可用于任何类型
实现 trait,就像字符串字面量一样。示例 8-12 显示
两个例子。s
to_string
Display
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(); }
此代码创建一个包含 .initial contents
我们还可以使用该函数从字符串创建
字面。示例 8-13 中的代码等同于示例 8-12 中的代码
使用 .String::from
String
to_string
fn main() { let s = String::from("initial contents"); }
因为字符串用于很多事情,所以我们可以使用许多不同的泛型
字符串的 API,为我们提供了很多选择。他们中的一些似乎
多余,但他们都有自己的位置!在这种情况下,并做同样的事情,所以你选择哪一个是一个风格和
可读性。String::from
to_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"); }
所有这些都是有效值。String
更新字符串
A 的大小可以增长,其内容可以更改,就像内容一样
,如果你将更多数据推送到其中。此外,您可以方便地
使用 Operator 或 Macro 连接值。String
Vec<T>
+
format!
String
使用 push_str
和 push
追加到 String
我们可以通过使用该方法来附加一个字符串 slice,
如示例 8-15 所示。String
push_str
fn main() { let mut s = String::from("foo"); s.push_str("bar"); }
在这两行之后,将包含 .该方法采用
string slice 的 Fragment 中,因为我们不一定想获得
参数。例如,在示例 8-16 的代码中,我们希望能够在将其内容附加到 .s
foobar
push_str
s2
s1
fn main() { let mut s1 = String::from("foo"); let s2 = "bar"; s1.push_str(s2); println!("s2 is {s2}"); }
如果该方法具有 的所有权,我们将无法打印
它在最后一行的值。但是,此代码的工作方式符合我们的预期!push_str
s2
该方法将单个字符作为参数,并将其添加到 .示例 8-17 使用该方法将字母 l 添加到 a 中。push
String
String
push
fn main() { let mut s = String::from("lo"); s.push('l'); }
因此,将包含 .s
lol
使用 +
运算符或 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 }
字符串将包含 .原因不再是
有效,并且我们使用对 的引用的原因 ,必须
替换为我们使用运算符时调用的方法的签名。
运算符使用该方法,其签名类似于
这:s3
Hello, world!
s1
s2
+
+
add
fn add(self, s: &str) -> String {
在标准库中,您将看到 defined using generics 和关联的
类型。在这里,我们替换了具体类型,这就是当我们
使用 values 调用此方法。我们将在第 10 章讨论泛型。
这个签名为我们提供了理解棘手之处所需的线索
运算符的位。add
String
+
第一个 , 具有 ,这意味着我们正在添加第二个
string 设置为第一个字符串。这是因为函数中的参数:我们只能将 a 添加到 a ;我们不能将两个值相加。但是等等 — 的类型是 ,而不是 ,因为
在第二个参数中指定为 。那么,为什么示例 8-18 会编译呢?s2
&
s
add
&str
String
String
&s2
&String
&str
add
我们能够在调用 to 的原因是编译器
可以将参数强制转换为 .当我们调用该方法时,Rust 使用 deref 强制转换,这里变成了 .
我们将在第 15 章中更深入地讨论 deref coercion。因为
not take ownership 的参数,此作后仍将有效。&s2
add
&String
&str
add
&s2
&s2[..]
add
s
s2
String
其次,我们可以在签名中看到,它拥有 because 的所有权没有 .这意味着在示例 8-18 中将是
已移至呼叫中,之后将不再有效。因此,尽管看起来它会复制两个字符串并创建一个新字符串,
此语句实际上获得了 的所有权,并附加了内容的副本
of ,然后返回结果的所有权。换句话说,它看起来
就像它制作了很多副本,但事实并非如此;实现方式更
比复制更有效。add
self
self
&
s1
add
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; }
此时,将为 。面对所有的 and 角色,很难看出发生了什么。用于组合字符串
更复杂的方式,我们可以改用宏:s
tic-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。使用的代码版本很多
更易于阅读,并且宏生成的代码使用引用
,因此此调用不会获得其任何参数的所有权。s
tic-tac-toe
format!
println!
String
format!
format!
索引到字符串中
在许多其他编程语言中,访问
string 是一种有效且常见的作。然而
如果你尝试在 Rust 中访问 using indexing 语法的部分内容,你将
收到错误。考虑示例 8-19 中的无效代码。String
fn main() {
let s1 = String::from("hello");
let h = s1[0];
}
此代码将导致以下错误:
$ 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 示例字符串。首先,这个:String
Vec<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):len
4
"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
З
З
208
151
answer
208
208
208
&"hello"[0]
104
h
那么,答案是,为了避免返回意外值并导致 可能不会立即发现的 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 来确定有多少个有效字符。String
String
切片字符串
索引到字符串中通常是一个坏主意,因为不清楚 字符串索引作的返回类型应为:一个字节值、一个 字符、字形簇或字符串切片。如果您确实需要使用 indices 来创建字符串 slices,因此,Rust 要求您更具体。
您可以与
range 创建包含特定字节的字符串切片:[]
[]
#![allow(unused)] fn main() { let hello = "Здравствуйте"; let s = &hello[0..4]; }
这里,将是包含字符串前 4 个字节的 a。
前面我们提到过,这些字符中的每一个都是两个字节,这意味着将是 。s
&str
s
Зд
如果我们尝试用类似 的东西只切分字符字节的一部分,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 中,你可以迭代结果以访问每个元素:chars
chars
char
#![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
&str
contains
replace
让我们切换到稍微简单一点的东西:哈希映射!
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准