Rust中字符串迭代:底层工作原理
您是否曾想过,当您写下for c in "Привет".chars()时实际发生了什么?如果您来自Python或JavaScript,那里的字符串只是字符数组,Rust可能像一位严格的老师,迫使您在每次操作前思考。这很棒!
Stack Overflow上关于"How do you iterate over a string by character"的问题获得了数千次浏览。这并非偶然。Rust中没有简单的string[i]——这是有充分理由的。让我们了解如何正确、高效且无意外地遍历字符串。
为什么Rust不允许您直接通过索引获取字符?
这一切都源于UTF-8编码。与许多语言不同,Rust将字符串存储为UTF-8格式,而非ASCII或UCS-2。这意味着一个字符可能占用1到4个字节。例如:
'a'— 1字节'é'— 2字节'中'— 3字节'😊'— 4字节
如果Rust允许string[0],那么"😊"[0]返回的将不是表情符号,而是其字节表示的一半——垃圾数据。因此,Rust在编译器层面保护您免受此类错误的影响。
遍历字符串的主要方法
Rust提供了三种主要的迭代方法,各有其用途。选择取决于您具体需要什么:字符、字节还是字素簇。
1. 按字符迭代(char)
最常用的方法是.chars()。它返回一个Unicode字符(char类型)的迭代器。
fn main() { let text = "Привет, мир! 🌍"; for c in text.chars() { println!("{}", c); } // 输出: // П // р // и // в // е // т // , // (空格) // м // и // р // ! // (空格) // 🌍}在底层,.chars()解析UTF-8字节序列并返回每个有效的字符。如果字符串中包含无效字节,迭代器可能返回std::char::REPLACEMENT_CHARACTER('�')。
重要提示:.chars()的时间复杂度为O(n)。迭代器遍历所有字节,即时解码。对于短字符串,这几乎是瞬间完成的,但对于长字符串(兆字节级别的文本),可能会比较明显。
2. 按字节迭代(bytes)
如果您需要原始字节,请使用.bytes()。这是一种快速的方法,但您得到的是字节,而不是字符。
fn main() { let text = "Hello"; for byte in text.bytes() { println!("{}", byte); } // 输出: // 72 (H) // 101 (e) // 108 (l) // 108 (l) // 111 (o)}对于英文文本,这等同于按字符迭代。但对于俄语或表情符号则不然。例如,"Привет"在字节表示中占用12个字节(每个字符2个字节),而"🌍"占用4个字节。
何时使用:当您处理二进制数据、协议或希望在不进行UTF-8解码的情况下获得最大性能时。
3. 按字素簇迭代(grapheme clusters)
这就更有趣了。有时,一个视觉上的"字符"由多个Unicode字符组成。例如,国家旗帜或带变音符号的字母:"é"可以表示为单个字符é(U+00E9),也可以表示为e + ◌́(U+0065 + U+0301)的组合。
要处理字素簇,需要添加crate unicode-segmentation:
// Cargo.toml// [dependencies]// unicode-segmentation = "1.10"
use unicode_segmentation::UnicodeSegmentation;
fn main() { let text = "é"; // chars() 将返回1个字符