Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

我们之前已经见过字符串了,但现在让我们更深入地了解它们。字符串在许多编程语言中是一个令人惊讶的复杂话题,X 语言也不例外。由于 X 语言对内存安全保证的关注,关于字符串的决策比其他语言更多。让我们深入研究!

什么是字符串?

首先,我们需要定义我们所说的字符串是什么意思。X 语言的核心只有一种字符串类型:字符串切片 str,它通常以借用形式 &str 出现。我们在第 3 章中谈到了字符串切片:这些是对存储在别处的 UTF-8 编码字符串数据的引用。例如,字符串字面量存储在程序的二进制文件中,因此是字符串切片。

String 类型由 X 语言标准库提供,而不是由核心语言编码,它是一个可增长、可变、拥有、UTF-8 编码的字符串类型。当 X 语言程序员在 X 语言中说“字符串“时,他们通常指的是 String 类型或字符串切片 &str 类型,而不仅仅是其中一种。尽管本章主要是关于 String 的,但这两种类型在 X 语言标准库中都被大量使用,并且 String&str 都是 UTF-8 编码的。

创建新字符串

许多与 List<T> 可用的相同操作也可用于 String,因为 String 实际上是作为字节向量的包装器实现的,并添加了一些额外的保证、限制和功能。让我们看一个使用 List 的方式也适用于 String 的例子。

我们可以用 new() 函数创建一个新的空字符串,如下所示:

let mut s = String::new()

这行创建了一个名为 s 的新空字符串,然后我们可以向其中加载数据。通常,我们会有一些想要用来启动字符串的初始数据。为此,我们使用 from 函数:

let s = String::from("initial contents")

这行创建了一个包含字符串 "initial contents" 的字符串。

由于字符串被频繁使用,有很多方便的函数可以在各种情况下使用。让我们看看更多。

更新字符串

String 的大小可以增长,其内容可以更改,就像 List 的内容一样,如果你向其中推送更多数据。此外,你可以方便地使用 + 运算符或 format! 宏来组合 String 值。

使用 push_str 和 push 附加到字符串

我们可以使用 push_str 方法附加一个字符串切片来增长 String

let mut s = String::from("foo")
s = s + "bar"  // s 现在是 "foobar"

push 方法接受一个字符作为参数并将其附加到 String

let mut s = String::from("lo")
s = s + "l"  // s 现在是 "lol"

使用 + 运算符或 format! 宏组合

通常,你会想要组合两个现有的字符串。一种方法是使用 + 运算符:

let s1 = String::from("Hello, ")
let s2 = String::from("world!")
let s3 = s1 + &s2  // 注意 s1 在这里被移动,不能再使用

执行这些行后,字符串 s3 将包含 Hello, world!

+ 运算符在幕后调用 add 函数,如下所示:

function add(self: String, s: &str) -> String {
  // ...
}

我们可以在 add 函数的签名中看到 + 运算符使用了 self,这意味着 + 运算符获取了 self 的所有权。这就是为什么 s1 在加法后不再有效。

对于更复杂的字符串组合,我们可以使用 format! 宏:

let s1 = String::from("tic")
let s2 = String::from("tac")
let s3 = String::from("toe")
let s = format!("{}-{}-{}", s1, s2, s3)

执行这些行后,s 将是 tic-tac-toeformat! 宏的工作方式与 println! 类似,但它不是将输出打印到屏幕上,而是返回一个带有结果内容的 String。使用 format! 的版本也更容易阅读,并且不会获取任何参数的所有权。

字符串索引

在许多其他语言中,通过索引引用字符串中的单个字符是有效且常见的操作。然而,在 X 语言中,如果你尝试使用索引语法访问 String 的部分,你会得到一个错误。让我们看看:

let s1 = String::from("hello")
let h = s1[0]  // 错误!

内部表示

StringList<u8> 的包装器。让我们看看:

let len = String::from("Hola").len()

在这里,len 将是 4,这意味着存储字符串 "Hola" 的向量是 4 字节长:这些字母中的每一个在 UTF-8 编码中都占 1 字节。那么下面这个呢?(注意,这个字符串以西里尔字母 Ze 开头,而不是阿拉伯数字 3。)

let len = String::from("Здравствуйте").len()

你可能期望 len 是 12,但实际上是 24:这是 UTF-8 中 “Здравствуйте” 编码所需的字节数,因为每个 Unicode 标量值在该字符串中占 2 字节。因此,字符串字节的索引并不总是与有效的 Unicode 标量值相关。为了演示,考虑以下无效的 X 语言代码:

let hello = String::from("Здравствуйте")
let answer = &hello[0]

answer 应该是什么?它应该是 З,第一个字符吗?当用 UTF-8 编码时,З 的第一个字节是 208,第二个是 151。所以 answer 实际上应该是 208,但 208 本身不是一个有效的字符。返回 208 可能不是用户想要的,但这是 X 语言在字节索引 0 处唯一可用的数据。用户通常不想要字节值,即使字符串只包含拉丁字母:即使 &"hello"[0] 是返回字节值的有效代码,它也应该返回 104,而不是 'h'

因此,X 语言不允许这种语法,以避免意外行为并在早期防止错误。

字节、标量值和字素簇!天哪!

关于 UTF-8 的另一点是,从 X 语言的角度来看,实际上有三种相关的方式来查看字符串:作为字节、作为 Unicode 标量值、以及作为字素簇(最接近人们通常所说的“字符“)。

让我们看一下用天城文书写的印地语单词 “नमस्ते”:

नमस्ते

它最终作为 u8 值的向量存储,如下所示:

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

这是 18 个字节,是计算机最终存储这些数据的方式。如果我们将它们视为 Unicode 标量值,也就是 X 语言的 character 类型,这些字节看起来像这样:

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

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

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

X 语言提供了不同的方式来解释计算机存储的原始字符串数据,以便每个程序都可以选择它需要的解释,无论数据是什么人类语言。

最后一个原因是 X 语言不允许我们索引 String 以获取字符是索引操作预计总是需要常数时间(O(1))。但对于 String 不可能保证这种性能,因为 X 语言必须从开头到索引遍历内容来确定有多少有效字符。

切片字符串

索引字符串通常是一个坏主意,因为不清楚字符串索引操作的返回类型应该是什么:字节值、字符、字素簇或字符串切片。因此,X 语言要求你在真正需要创建字符串切片时更具体,而不是使用 [] 带单个数字进行索引。

为了更具体地说明你想要的内容,而不是使用带单个数字的 [] 进行索引,你可以使用带范围的 [] 来创建包含特定字节的字符串切片:

let hello = "Здравствуйте"
let s = &hello[0..4]

在这里,s 将是一个包含字符串前 4 个字节的 &str;之前我们提到这些字符中的每一个都是 2 字节,所以这意味着 s 将是 “Зд”。

如果我们尝试使用 &hello[0..1] 只切片一个字节会发生什么?答案是:X 语言会在运行时 panic,就像我们访问列表中的无效索引时一样:

let hello = "Здравствуйте"
let s = &hello[0..1]  // 这会 panic!

因此,你应该谨慎使用范围来创建字符串切片,因为这样做可能会导致程序崩溃。

遍历字符串的方法

好消息是,你可以通过其他方式访问字符串中的元素。

如果你需要对单个 Unicode 标量值执行操作,最好的方法是使用 chars 方法。对 “नमस्ते” 调用 chars 会分离并返回六个 character 类型的值,你可以遍历结果以访问每个元素:

for c in "नमस्ते".chars() {
  println("{}", c)
}

这将打印:

न
म
स
्
त
े

bytes 方法返回每个原始字节,这可能适合你的领域:

for b in "नमस्ते".bytes() {
  println("{}", b)
}

这将打印组成此字符串的 18 个字节:

224
164
168
// --snip--

请记住,有效的 Unicode 标量值可能由超过 1 个字节组成。

从字符串中获取字素簇很复杂,因此标准库中没有提供此功能。如果你需要此功能,可以在 crates.io 上找到可用的 crate。

字符串并不简单

总而言之,字符串很复杂!不同的语言对程序员如何处理这种复杂性做出不同的选择。X 语言选择将正确处理 String 数据作为所有 X 语言程序的默认行为,这意味着程序员必须花更多时间提前思考如何处理 UTF-8 数据。这种权衡比起其他编程语言更多地暴露了字符串的复杂性,但它将防止你在开发生命周期的后期处理涉及非 ASCII 字符的错误。

好消息是,标准库提供了许多基于 String&str 类型的功能,以帮助正确处理这些情况。确保查看这些函数的文档,如用于安全地附加到字符串的 push_str 等。

总结

字符串是复杂的,X 语言让你正确处理它们!这是一个权衡,但它在开发生命周期的早期暴露了可能的 UTF-8 问题,而不是允许可能导致错误的假设持续到生产中。在处理非 UTF-8 数据时,这可能会更困难,但这是值得的额外努力!