Как в Rust итерироваться по строке посимвольно (по Unicode-символам)?
Проблема: почему строки в Rust нельзя просто индексировать?
В Rust строки хранятся в кодировке UTF-8. Это означает, что один Unicode-символ (называемый графемой или скалярным значением) может занимать от 1 до 4 байт. Например, латинская 'A' — 1 байт, кириллическая 'П' — 2 байта, а эмодзи '🔥' — 4 байта. Поэтому прямой доступ по индексу байта (например, s[0]) может "разрезать" символ посередине, что приведёт к некорректным данным или панике.
Вместо этого Rust предоставляет безопасные методы для работы с символами:
Решение 1: метод .chars() — итерация по Unicode-скалярным значениям
Самый простой и распространённый способ — использовать метод .chars(). Он возвращает итератор, который перебирает каждый Unicode-символ (тип char).
fn main() {
let s = "Привет, мир! 🔥";
for c in s.chars() {
println!("{}", c);
}
// Результат: П, р, и, в, е, т, ,, , м, и, р, !, , 🔥
}
Метод .chars() корректно обрабатывает многобайтовые символы. Вы также можете собрать результат в вектор:
let chars: Vec<char> = s.chars().collect();
println!("{:?}", chars); // ['П', 'р', 'и', 'в', 'е', 'т', ',', ' ', 'м', 'и', 'р', '!', ' ', '🔥']
Решение 2: доступ по индексу с помощью .chars().nth()
Если вам нужно получить символ по определённому порядковому номеру (не байтовому индексу), используйте .nth():
let s = "Привет";
let third_char = s.chars().nth(2); // Индексация с 0
match third_char {
Some(c) => println!("Третий символ: {}", c), // 'и'
None => println!("Символа нет"),
}
Важно: .nth() работает за O(n) — он перебирает символы с начала. Для частого доступа лучше собрать вектор Vec<char>.
Решение 3: итерация по байтам — если нужна производительность
Если вы точно знаете, что работаете только с ASCII-символами (латиница, цифры, базовые знаки), можно использовать .bytes():
let s = "Hello";
for byte in s.bytes() {
println!("{}", byte as char); // H, e, l, l, o
}
Но для кириллицы или эмодзи не используйте .bytes() — вы получите отдельные байты, а не символы.
Решение 4: графемы (например, для эмодзи-последовательностей)
Некоторые символы, как флаги стран (🇷🇺) или эмодзи с модификаторами (👨👩👧), состоят из нескольких Unicode-скалярных значений. Метод .chars() разобьёт их на части. Если вам нужны "полные" символы, используйте крейт unicode-segmentation:
// Добавьте в Cargo.toml: unicode-segmentation = "1.10"
use unicode_segmentation::UnicodeSegmentation;
fn main() {
let s = "🇷🇺 👨👩👧";
for grapheme in s.graphemes(true) {
println!("{}", grapheme);
}
// Результат: 🇷🇺, , 👨👩👧
}
Практический пример: замена гласных на '*'
fn replace_vowels(s: &str) -> String {
s.chars()
.map(|c| match c.to_lowercase().next() {
Some('а' | 'е' | 'ё' | 'и' | 'о' | 'у' | 'ы' | 'э' | 'ю' | 'я') => '*',
_ => c,
})
.collect()
}
fn main() {
let result = replace_vowels("Привет, мир!");
println!("{}", result); // Пр*в*т, м*р!
}
Итог
- Для обычной итерации — используйте
.chars() - Для доступа по индексу символа —
.chars().nth()или соберите вVec<char> - Для сложных Unicode-последовательностей — крейт
unicode-segmentation - Не используйте
s[0]— это вызовет панику
Альтернативный подход: итерация с сохранением индексов байтов
Иногда нужно знать не только символ, но и его позицию в байтовом представлении строки. Например, для замены подстроки или работы с регулярными выражениями. В этом случае полезен метод .char_indices():
fn main() {
let s = "Привет";
for (byte_index, c) in s.char_indices() {
println!("Символ '{}' начинается с байта {}", c, byte_index);
}
// Вывод:
// Символ 'П' начинается с байта 0
// Символ 'р' начинается с байта 2
// Символ 'и' начинается с байта 4
// Символ 'в' начинается с байта 6
// Символ 'е' начинается с байта 8
// Символ 'т' начинается с байта 10
}
Это может быть полезно, если вы хотите, например, разбить строку на части по определённым символам, сохраняя при этом возможность ссылаться на исходные байты.
Когда это нужно?
- При парсинге текста, где важны границы байтов (например, для дальнейшего использования с
&strсрезами). - При работе с большими строками, где создание
Vec<char>потребляет лишнюю память.
Помните: .char_indices() работает так же быстро, как и .chars(), но даёт дополнительную информацию о позиции. Для большинства задач достаточно простого .chars(), но знание этого метода расширяет ваши возможности.