使用字符串存储 UTF-8 编码的文本
我们在第 4 章中讨论了字符串,但现在我们将更深入地研究它们。 新 Rustacean 通常会卡在三个组合的字符串上 原因:Rust 倾向于暴露可能的错误,字符串是 复杂的数据结构比许多程序员认为的要复杂,并且 UTF-8 的 UTF 格式。这些因素以一种似乎很困难的方式结合在一起,当您 来自其他编程语言。
我们在集合的上下文中讨论字符串,因为字符串是
实现为字节集合,加上一些方法以提供有用的
功能。在本节中,我们将
谈论String
,每个集合类型都具有,例如
创建、更新和读取。我们还将讨论String
与其他集合不同,即索引如何索引到String
是
因人和计算机解释方式的差异而变得复杂String
数据。
什么是字符串?
我们首先定义术语 string 的含义。Rust 只有一个字符串
键入核心语言,即字符串 slicestr
这通常是
以借用的形式&str
.在第 4 章中,我们讨论了字符串 slices,
这些字符串是对存储在其他位置的某些 UTF-8 编码字符串数据的引用。字符串
例如,文本存储在程序的二进制文件中,因此
string 切片。
这String
type,它由 Rust 的标准库提供,而不是
编码为核心语言,是一种可增长、可变、拥有的 UTF-8 编码
string 类型。当 Rustacean 在 Rust 中提到 “strings” 时,它们可能是
引用String
或字符串 slice&str
类型,而不仅仅是一个
这些类型。虽然本节主要是关于String
,两种类型都是
在 Rust 的标准库中大量使用,并且两者String
和字符串切片
是 UTF-8 编码的。
创建新字符串
许多相同的作可用于Vec<T>
可用于String
也是因为String
实际上是作为向量的包装器
字节数,并具有一些额外的保证、限制和功能。示例
的函数中,该函数的工作方式与Vec<T>
和String
是new
函数创建一个实例,如示例 8-11 所示。
fn main() { let mut s = String::new(); }
示例 8-11:创建一个新的、空的String
此行将创建一个名为s
,然后我们可以将
数据。通常,我们会有一些初始数据,我们想要从这些数据开始
字符串。为此,我们使用to_string
方法,该方法可用于任何类型
实现Display
trait 的 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
方法创建String
from 字符串文本
此代码创建一个包含initial contents
.
我们也可以使用函数String::from
要创建一个String
从字符串
字面。示例 8-13 中的代码等同于示例 8-12 中的代码
使用to_string
.
fn main() { let s = String::from("initial contents"); }
示例 8-13:使用String::from
函数创建
一个String
from 字符串文本
因为字符串用于很多事情,所以我们可以使用许多不同的泛型
字符串的 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"); }
示例 8-14:在 字符串
所有这些都是有效的String
值。
更新字符串
一个String
大小可以增大,其内容可以更改,就像内容一样
的Vec<T>
,如果您将更多数据推送到其中。此外,您可以方便地
使用运算符或+
format!
宏进行连接String
值。
附加到 String 中push_str
和push
我们可以种植一个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_str
method 采用
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_str
method 获取了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。因为add
does
不获取s
参数s2
仍将是有效的String
在此作之后。
其次,我们可以在签名中看到add
取得self
因为self
没有 .这意味着&
s1
在示例 8-18 中将是
移动到add
call 调用,之后将不再有效。所以,尽管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}"); }
此代码还将s
自tic-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 如何将字符串存储在 记忆。
内部表示
一个String
是Vec<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
实际上应该是208
但208
不是有效字符
靠自己。返回208
如果用户要求
对于此字符串的第一个字母;然而,这是 Rust
在字节索引 0 处。用户通常不希望返回 byte 值,即使
如果字符串仅包含拉丁字母:如果&"hello"[0]
是有效代码
返回 byte 值,它将返回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 的char
type 是,那些
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}"); } }
此代码将打印以下内容:
З
д
或者,bytes
method 返回每个原始字节,这可能是
适合您的域:
#![allow(unused)] fn main() { for b in "Зд".bytes() { println!("{b}"); } }
此代码将打印组成此字符串的四个字节:
208
151
208
180
但请务必记住,有效的 Unicode 标量值可能由多个 比 1 个字节。
与 Devanagari 脚本一样,从字符串中获取字形簇是 complex,因此 standard 库不提供此功能。箱子 在 crates.io 上可用,前提是这是 您需要的功能。
字符串不是那么简单
总而言之,字符串很复杂。不同的编程语言使
关于如何将这种复杂性呈现给程序员的不同选择。锈
已选择对String
data 的默认行为
对于所有 Rust 程序,这意味着程序员必须投入更多的思考
预先处理 UTF-8 数据。这种权衡暴露了
字符串,但它会阻止您
无需稍后在
开发生命周期。
好消息是,标准库提供了许多构建的功能
从String
和&str
类型来帮助处理这些复杂情况
正确。请务必查看文档以了解有用的方法,例如contains
用于在字符串中搜索,使用replace
用于替换 a 的部分
string 替换为另一个 string。
让我们切换到稍微简单一点的东西:哈希映射!
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准