在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果你尝试使用索引语法访问 String 的一部分,会出现一个错误。
let s1 = String::from("hello");
let h = s1[0];
# }
这段代码会导致如下错误:
Compiling collections v0.1.0 (file:///projects/collections)
error[E0277]: the type `String` cannot be indexed by `{integer}`
--> src/main.rs:3:13
|
3 | let h = s1[0];
| ^^^^^ `String` cannot be indexed by `{integer}`
|
= help: the trait `Index<{integer}>` is not implemented for `String`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `collections` due to previous error
错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 是如何在内存中储存字符串的。
内部表现
String 是一个 Vec 的封装。首先是这一个:
# 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” 的 Vec 的长度是四个字节:这里每一个字母的 UTF-8编码都占用一个字节。那下面这个例子又如何呢?(注意这个字符串中的首字母是西里尔字母的 Ze 而不是阿拉伯数字 3 。)
# 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 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。作为演示,考虑如下无效的 Rust 代码:
let answer = &hello[0];
我们已经知道 answer 不是第一个字符 З。当使用 UTF-8 编码时,З 的第一个字节 208,第二个是 151,所以 answer 实际上应该是 208,不过 208 自身并不是一个有效的字母。返回 208 可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引 0 位置所能提供的唯一数据。用户通常不会想要一个字节值被返回,即便这个字符串只有拉丁字母:即便 &”hello”[0] 是返回字节值的有效代码,它也应当返回 104 而不是 h。
为了避免返回意外的值并造成不能立刻发现的 bug,Rust 根本不会编译这些代码,并在开发过程中及早杜绝了误会的发生。
字节、标量值和字形簇!天呐!
这引起了关于 UTF-8 的另外一个问题:从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中 字母的概念)。
比如这个用梵文书写的印度语单词 ” नमस◌्त◌े”,最终它储存在 vector 中的 u8 值看起来像这样:
224, 165, 135]
这里有 18 个字节,也就是计算机最终会储存的数据。如果从 Unicode 标量值的角度理解它们,也就像Rust 的 char 类型那样,这些字节看起来像这样:
这里有六个 char,不过第四个和第六个都不是字母,它们是发音符号本身并没有任何意义。最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:
Rust 提供了多种不同的方式来解释计算机储存的原始字符串数据,这样程序就可以选择它需要的表现方式,而无所谓是何种人类语言。
最后一个 Rust 不允许使用索引获取 String 字符的原因是,索引操作预期总是需要常数时间 (O(1))。但是对于 String 不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。