Avatar ·

Как в Rust итерироваться по строке посимвольно (по Unicode-символам)?

Всем привет! Я только начал изучать Rust, и у меня возникла проблема с итерацией по строке. В других языках (например, Python или C++) я привык, что строка — это массив символов, и я могу просто пройтись по индексу или использовать цикл for. Но в Rust всё сложнее. Вот что я пробовал: ```rust let s = "Привет, мир!"; for c in s { println!("{}", c); } ``` Компилятор выдаёт ошибку: `error[E0277]: the trait bound \`str: Iterator\` is not satisfied`. Я понимаю, что `str` — это не итератор, но как тогда получить каждый символ? Я также пробовал обращаться по индексу: ```rust let s = "Привет"; println!("{}", s[0]); // Ошибка! ``` И снова ошибка: строки в Rust нельзя индексировать напрямую, потому что символы Unicode могут занимать разное количество байт. В итоге мне нужно: взять строку, пройтись по каждому символу (включая кириллицу, эмодзи и спецсимволы) и что-то сделать с каждым из них. Например, посчитать количество символов или заменить все гласные на '*'. Подскажите, как правильно организовать итерацию по строке в Rust? Буду благодарен за примеры кода и объяснение, почему это работает именно так. <<>>

Проблема: почему строки в 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(), но знание этого метода расширяет ваши возможности.