什么是字符串
什么是字符串?
在开始深入这些方面之前,我们需要讨论一下术语 字符串 的具体意义。Rust 的核心语言中只有一种字符串类型:str,字符串 slice,它通常以被借用的形式出现,&str
。第四章讲到了 字符串 slice:它们是一些储存在别处的 UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。
称作 String 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。当谈到 Rust 的 “字符串”时,它们通常指的是
String
和字符串 slice&str
类型,而不仅仅是其中之一。虽然本部分内容大多是关于 String 的,不过这两个类型在 Rust 标准库中都被广泛使用,String 和字符串 slice 都是 UTF-8 编码的。
新建字符串
创建一个空字符串
let mut s = String::new();
从字符串字面量创建 String,下面两种方法皆可。
let s = "initial contents".to_string();
let s = String::from("initial contents");
更新字符串
使用 push_str 和 push
let mut s = String::from("foo");
s.push_str(" bar");
s.push('s');
push_str
函数默认使用 slice
,不使勇参数的所有权。
使用 + 运算符或 format! 宏 拼接字符串
let s1 = String::from("hello,");
let s2 = String::from(" world!");
let s3 = s1 + &s2;
上述代码执行完后,会使s1失效。因为调用 + 运算符时,相当于执行了下面的函数,收回了s1的所有权。
fn add(self, s: &str) -> String {}
对于复杂的字符串连接,我们可以使用 format!
宏!
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);
索引字符串
Rust 中,字符串不支持使用索引访问。为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 是如何在内存中储存字符串的。
String
是一个 Vec<u8>
的封装。看一些例子,首先是这一个:
let len = String::from("Hola").len(); // 4
let len = String::from("Здравствуйте").len(); // 24
第一个字符串的长是4,这里每一个字母的 UTF-8 编码都占用一个字节。
第二个字符串的长度居然不是12 而是 24。这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为每个 Unicode 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。作为演示,考虑如下无效的 Rust 代码:
let hello = "Здравствуйте";
let answer = &hello[0];
answer 的值应该是什么呢?它应该是第一个字符 З 吗?当使用 UTF-8 编码时,З 的第一个字节 208,第二个是 151,所以 answer 实际上应该是 208,不过 208 自身并不是一个有效的字母。返回 208 可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引 0 位置所能提供的唯一数据。用户通常不会想要一个字节值被返回,即便这个字符串只有拉丁字母: 即便 &“hello”[0] 是返回字节值的有效代码,它也应当返回 104 而不是 h。为了避免返回意外的值并造成不能立刻发现的 bug,Rust 根本不会编译这些代码,并在开发过程中及早杜绝了误会的发生。
字节、标量值(char)和字形簇!天呐!
这引起了关于 UTF-8 的另外一个问题:从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中 字母 的概念)。
比如这个用梵文书写的印度语单词 “नमस्ते”,最终它储存在 vector 中的 u8 值看起来像这样:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]
这里有 18 个字节,也就是计算机最终会储存的数据。
如果从 Unicode 标量值的角度理解它们,也就像 Rust 的 char 类型那样,这些字节看起来像这样:
['न', 'म', 'स', '्', 'त', 'े']
这里有六个 char
,不过第四个和第六个都不是字母,它们是发音符号本身并没有任何意义。
最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:
["न", "म", "स्", "ते"]
Rust 提供了多种不同的方式来解释计算机储存的原始字符串数据,这样程序就可以选择它需要的表现方式,而无所谓是何种人类语言。
最后一个 Rust 不允许使用索引获取 String 字符的原因是,索引操作预期总是需要常数时间 (O(1))。但是对于 String 不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。
遍历字符串的方法
幸运的是,rust提供了获取字符串元素的方法。
如果要操作标量值,可以使用 chars()
方法:
for c in "नमस्ते".chars() {
println!("{}", c);
}
न
म
स
्
त
े
bytes
方法返回每一个原始字节:
for b in "नमस्ते".bytes() {
println!("{}", b);
}
224
164
// -- 18个 --
165
135
不过请记住有效的 Unicode 标量值可能会由不止一个字节组成。
从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate。
字符串并不简单
总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理 String 数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何预先处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发生命周期后期免于处理涉及非 ASCII 字符的错误。
现在让我们转向一些不太复杂的集合:哈希 map!