Сплит строки в Rust: 5 фатальных ошибок и как их избежать

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

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

Начать курс

Введение: почему split() в Rust — это минное поле для новичка

Если вы перешли на Rust из Python или JavaScript, первое, что вы попытаетесь сделать — это написать "hello,world".split(","). И, скорее всего, сразу получите не то, что ожидали. На Stack Overflow вопрос «How do I split a string in Rust?» набрал десятки тысяч просмотров. Проблема не в том, что Rust «сломан», а в том, что его система владения и заимствования требует иного подхода к, казалось бы, тривиальной операции.

В этой статье мы разберём топ-5 ошибок, которые совершают разработчики при split строки в Rust, и покажем, как их избежать. Вы научитесь не просто делить строку, а делать это эффективно, безопасно и красиво.

Ошибка №1: Игнорирование типа возвращаемого значения

Самый частый сценарий: вы пишете let parts = s.split(","); и пытаетесь использовать parts как вектор. Rust же отвечает: expected struct 'std::str::Split'.

В чём подвох?

Метод split() возвращает итератор, а не коллекцию. Это сделано для эффективности: итератор не выделяет память под все части сразу, а выдаёт их по одной. Но новичок ждёт привычный список.

// ❌ Так не работает
let text = "apple,banana,cherry";
let parts = text.split(",");
println!("{}", parts[0]); // Ошибка! Split не реализует Index

Правильное решение

Соберите итератор в вектор с помощью .collect():

// ✅ Правильно
let text = "apple,banana,cherry";
let parts: Vec<&str> = text.split(",").collect();
println!("Первый элемент: {}", parts[0]); // apple

Важно: тип переменной parts — Vec<&str>. Это срез исходной строки, а не новые строки. Если исходная строка удалится, ссылки станут невалидными. Об этом — в следующей ошибке.

Ошибка №2: Забыли про время жизни (lifetime)

Вы получили вектор &str, но пытаетесь вернуть его из функции. Компилятор кричит про lifetime. Рассмотрим классический сценарий:

// ❌ Ошибка: возвращаем ссылку на локальную строку
fn split_csv_bad(line: &str) -> Vec<&str> {
    let cleaned = line.trim().to_string(); // новая строка
    cleaned.split(",").collect() // ссылки на cleaned, который умрёт
}

Компилятор Rust умнее вас: он видит, что cleaned будет уничтожен при выходе из функции, и ссылки в векторе повиснут в воздухе.

Как избежать

Есть два пути:

  • Работать с исходной строкой, не создавая новых владельцев.
  • Клонировать данные, превратив &str в String.
// ✅ Вариант 1: принимаем &str и возвращаем Vec<&str> с тем же lifetime
fn split_csv_good(line: &str) -> Vec<&str> {
    line.split(",").collect()
}

// ✅ Вариант 2: создаём собственные строки
fn split_csv_owned(line: &str) -> Vec<String> {
    line.split(",").map(|s| s.to_string()).collect()
}

Выбирайте первый вариант, если данные нужны только внутри текущего контекста. Второй — если планируете передавать их в другие потоки или хранить долго.

Ошибка №3: Неправильная обработка пустых частей

Представьте, что вы парсите CSV с пропущенными значениями: "apple,,cherry". Метод split() честно вернёт пустую строку между двумя запятыми. Но часто это ломает логику.

let data = "apple,,cherry";
let parts: Vec<&str> = data.split(",").collect();
println!("{:?}", parts); // ["apple", "", "cherry"]

Как быть?

  • Если пустые части — ошибка: используйте split_terminator() или фильтруйте:
let parts: Vec<&str> = data.split(",")
    .filter(|s| !s.is_empty())
    .collect(); // ["apple", "cherry"]
  • Если нужны только непустые и без пробелов: комбинируйте с map и trim:
let messy = " apple , , cherry ";
let clean: Vec<&str> = messy.split(",")
    .map(|s| s.trim())
    .filter(|s| !s.is_empty())
    .collect(); // ["apple", "cherry"]

Ошибка №4: Пытаетесь изменить строку через split

Новички часто думают, что split возвращает изменяемые ссылки. Увы, это не так. split() возвращает неизменяемые срезы &str. Если вам нужно модифицировать части, придётся копировать.

// ❌ Ошибка: нельзя изменить через split
let mut text = String::from("hello world");
for word in text.split(" ") {
    word.make_ascii_uppercase(); // Ошибка! word — &str, а не &mut str
}

Правильный путь

Создайте новый вектор с изменёнными строками, а затем соедините обратно:

// ✅ Рабочий вариант
let text = "hello world";
let uppercased: Vec<String> = text.split(" ")
    .map(|w| w.to_uppercase())
    .collect();
let result = uppercased.join(" ");
println!("{}", result); // HELLO WORLD

Если вы работаете с String и хотите менять исходную строку (например, удалять слова), используйте split_off() или drain() — но это уже отдельная тема.

Ошибка №5: Забыли про Unicode и многобайтовые символы

Rust по умолчанию работает с UTF-8. split() оперирует байтами, но если вы ищете символ, который в UTF-8 занимает несколько байт (например, кириллица, эмодзи), всё работает корректно. Проблема возникает, когда вы пытаетесь использовать split_at() или индексы.

// ❌ Опасный код
let text = "Привет, мир!";
let first_byte = &text[..2]; // Паника! 2 байта — это не символ

С split() такой проблемы нет, но если вы работаете с границами слов, помните: split(" ") ищет именно пробел (U+0020), а не любой пробельный символ.

Блоги

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