Индексация строк в Rust: 5 фатальных ошибок и как их избежать

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему Rust не даёт вам просто написать s[0]?

Вы только начинаете изучать Rust, пишете свой первый "Hello, world!" и вдруг... ошибка компиляции. Вы пытаетесь получить первый символ строки, как делали это сотни раз в Python или JavaScript, а компилятор выбрасывает что-то вроде the type 'String' cannot be indexed by '{integer}'. Знакомо?

Этот вопрос — один из самых популярных на Stack Overflow. Тысячи разработчиков спотыкаются об эту кажущуюся "странность" Rust. Но на самом деле за этим стоит гениальное и продуманное решение, которое спасает ваш код от трудноуловимых багов. В этой статье мы разберём топ ошибок при индексации строк в Rust и, самое главное, научимся делать это правильно.

Ошибка №1: Попытка прямой индексации как в C или Python

Самая первая и самая распространённая ошибка — это попытка написать let c = my_string[0];. В Rust строки — это не просто массивы символов ASCII. Они хранятся в кодировке UTF-8. И это меняет всё.

Почему это не работает?

Представьте, что у вас есть строка "Привет". В ней 6 "символов" (с точки зрения человека). Но в памяти компьютера она занимает 12 байт (каждая буква кириллицы — 2 байта). Если бы Rust разрешил my_string[0], что бы он вам вернул? Первый байт (который сам по себе ничего не значит) или первый символ? А если бы строка содержала эмодзи, которые занимают 4 байта? Получился бы полный хаос.

Rust гарантирует безопасность на уровне компиляции. Индексация по целому числу всегда работает за O(1) и возвращает один элемент. Но для UTF-8 это невозможно: один "символ" (char) может занимать от 1 до 4 байт. Поэтому компилятор просто запрещает такую операцию, чтобы вы случайно не получили "мусор" или не сломали кодировку.

// ❌ Ошибка компиляции!
fn main() {
    let s = String::from("Привет");
    // let first = s[0]; // error[E0277]: the type 'String' cannot be indexed by '{integer}'
    println!("Привет, мир!");
}

Ошибка №2: Наивное использование срезов (slicing) без учёта границ символов

Да, срезы в Rust работают. Вы можете написать &s[0..5]. Но если вы попадёте "внутрь" многобайтового символа, ваша программа упадёт с паникой во время выполнения. Это называется runtime panic.

Пример фатальной ошибки

fn main() {
    let s = String::from("Здравствуйте"); // 12 символов, но 24 байта
    // Попробуем взять первые 3 байта
    let slice = &s[0..3]; // 'З' занимает 2 байта, 'д' — 2 байта.
    // 3 байта — это половина второго символа!
    println!("{}", slice); // thread 'main' panicked at 'byte index 3 is not a char boundary'
}

Это не просто предупреждение. Это полноценный краш программы. Rust не прощает небезопасной работы с памятью, даже в рантайме. Если вы работаете с пользовательским вводом (никнеймы, комментарии), вы гарантированно получите такой panic при первом же кириллическом или азиатском символе.

Как избежать: Используйте метод char_indices() для безопасного поиска границ или работайте через chars().

Ошибка №3: Путаница между байтами, char и графемами

Это самая коварная ошибка. Даже если вы научились получать char из строки, вы всё ещё можете получить не то, что ожидали. Потому что char в Rust — это Unicode Scalar Value. А то, что человек считает "символом" (например, "é" или "🇷🇺"), может состоять из нескольких char.

Разбор на примере

fn main() {
    let s = String::from("é"); // Это может быть один char (U+00E9)
    // или составной: 'e' (U+0065) + '◌́' (U+0301)
    let composed = String::from("e\u{0301}"); // то же самое 'é', но из двух char
    println!("Длина s: {} байт, {} char", s.len(), s.chars().count());
    println!("Длина composed: {} байт, {} char", composed.len(), composed.chars().count());
    // Вывод: обе строки выглядят одинаково, но имеют разную длину!
}

А теперь представьте, что вы пытаетесь взять первый char из строки с флагом России "🇷🇺". Этот флаг состоит из двух Unicode-символов (REGIONAL INDICATOR SYMBOL LETTER R и REGIONAL INDICATOR SYMBOL LETTER U). chars().next() вернёт вам только первый из них, что сломает логику отображения.

Вывод: Для большинства реальных задач (поиск слов, подсчёт видимых символов, обрезка строк) вам нужны графемы (grapheme clusters), а не char.

Правильные способы работы со строками в Rust

Хорошая новость: Rust предлагает элегантные и безопасные инструменты. Вам просто нужно знать, когда какой использовать.

Способ 1: Итерация по символам (char) — для анализа

Если вам нужно проверить каждый видимый символ (например, для валидации пароля), используйте метод .chars().

fn main() {
    let s = String::from("Rust 🦀");
    for c in s.chars() {
        println!("{}", c);
    }
    // Вывод: R, u, s, t,  , 🦀
    // Получить первый char:
    let first_char = s.chars().next().unwrap(); // 'R'
    println!("Первый символ: {}", first_char);
}

Способ 2: Доступ к байтам — для низкоуровневых операций

Если вы пишете сетевой протокол или работаете с бинарными данными, используйте .as_bytes().

fn main() {
    let s = String::from("Hello");
    let bytes = s.as_bytes();
    println!("Первый байт: {}", bytes[0]); // 72 (код 'H')
}

Способ 3: Безопасные срезы с char_indices()

Если вам нужно взять подстроку, но вы не уверены в границах, используйте char_indices(). Это вернёт вам список пар (индекс, символ).

fn safe_slice(s: &str, start_char: usize, end_char: usize) -> &str {
    let indices: Vec<usize> = s.char_indices().map(|(i, _)| i).collect();
    if start_char <                

Блоги

Рекомендаций по книгам