Итерация в Rust: как остановиться при ошибке и вернуть Result::Err

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

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

Начать курс

Введение: почему итератор не хочет останавливаться?

Представьте: вы пишете на Rust программу, которая обрабатывает список чисел — парсит строки, читает файлы, выполняет какие-то преобразования. Вы используете Iterator::map, чтобы пройтись по каждому элементу. И тут — бац! — один из элементов превращается в ошибку Result::Err. Логично было бы немедленно остановить итерацию и вернуть эту ошибку наверх. Но map просто продолжает работать, как ни в чём не бывало. Почему? И главное — как это исправить?

Этот вопрос — один из самых популярных на Stack Overflow среди новичков в Rust. И неспроста. Механизмы обработки ошибок в Rust гибкие, но требуют осознанного подхода. В этой статье мы разберём проблему вдоль и поперёк, рассмотрим несколько решений и выберем самое элегантное. Поехали!

Проблема: map не останавливается

Начнём с классического примера. Допустим, у нас есть вектор строк, которые мы хотим преобразовать в числа:

fn main() {
    let inputs = vec!["42", "hello", "100"];

    let result: Vec<Result<i32, std::num::ParseIntError>> =
        inputs.iter().map(|s| s.parse::<i32>()).collect();

    println!("{:?}", result);
}

Этот код скомпилируется и отработает. Но что мы получим? Вектор из Resultов: [Ok(42), Err(ParseIntError), Ok(100)]. Итерация не остановилась на "hello", а продолжилась дальше. Для многих задач это нормально — вы хотите собрать все ошибки. Но что, если вам нужно первое исключение? Например, при загрузке конфигурации: если один параметр неверен, дальше проверять бессмысленно.

Проблема в том, что map — это ленивая трансформация. Он не знает, что такое "остановка". Он просто говорит: "Дай мне элемент, я применю функцию, верну результат". Всё управление паузой и продолжением лежит на потребителе итератора (collect, for, fold и т.д.). А collect для Vec просто идёт до конца.

Решение 1: collect с FromIterator (самое популярное)

Самый распространённый ответ на Stack Overflow — использовать collect::<Result<Vec<_>, _>>(). Rust знает, что можно собрать итератор Resultов в один Result с вектором внутри. Если встретится Err, сборка прекращается и возвращается эта ошибка.

fn main() -> Result<(), std::num::ParseIntError> {
    let inputs = vec!["42", "hello", "100"];

    let result: Vec<i32> =
        inputs.iter()
              .map(|s| s.parse::<i32>())
              .collect::<Result<Vec<_>, _>>()?; // <-- магия!

    println!("{:?}", result);
    Ok(())
}

Как это работает? Тип Result<Vec<i32>, ParseIntError> реализует трейт FromIterator. Когда collect видит Err, он немедленно возвращает его, не обрабатывая оставшиеся элементы. Если всё хорошо — вы получаете Ok(вектор). Это коротко, идиоматично и эффективно.

Минус: вы теряете информацию о том, на каком элементе произошла ошибка (если не обернёте её в свой тип). Но для быстрых скриптов — самое то.

Решение 2: filter_map для игнорирования ошибок

Иногда вы не хотите останавливаться, а просто хотите проигнорировать неудачные элементы. Например, считать только валидные строки. Для этого идеально подходит filter_map:

fn main() {
    let inputs = vec!["42", "hello", "100", "abc", "7"];

    let numbers: Vec<i32> = inputs.iter()
        .filter_map(|s| s.parse::<i32>().ok()) // .ok() превращает Err в None
        .collect();

    println!("{:?}", numbers); // [42, 100, 7]
}

filter_map принимает замыкание, которое возвращает Option<T>. Если None — элемент пропускается. Если Some(value) — включается в результат. Это чистое, понятное решение для ситуаций, когда ошибки не критичны.

Решение 3: try_for_each для побочных эффектов

Если вам нужно не собирать значения, а выполнять действия (например, запись в файл или отправку запроса) и прерываться при первой же ошибке, используйте try_for_each:

use std::fs::File;
use std::io::Write;

fn main() -> std::io::Result<()> {
    let data = vec!["line1", "line2", "line3"];

    // Представим, что мы пишем строки в разные файлы
    data.iter()
        .try_for_each(|line| -> std::io::Result<()> {
            let mut file = File::create(format!("{}.txt", line))?;
            file.write_all(line.as_bytes())?;
            Ok(())
        })?; // если ошибка — итерация прервётся

    println!("Все файлы созданы успешно!");
    Ok(())
}

try_for_each — это аналог for_each, но для Result. Он вызывает замыкание для каждого элемента. Если замыкание возвращает Err, итерация немедленно прекращается, и ошибка передаётся наверх. Просто и эффективно.

Решение 4: fold с ранним выходом

Для максимального контроля можно использовать fold. Это самый гибкий, но и самый многословный способ. Вы вручную управляете состоянием и решаете, продолжать или нет:

fn main() -> Result<(), std::num::ParseIntError> {
    let inputs = vec!["42", "hello", "100"];

    let result = inputs.iter().try_fold(Vec::new(), |mut acc, s| {
        match s.parse::<i32>() {
            Ok(n) => {
                acc.push(n);
                Ok(acc)
            }
            Err(e) => Err(e), // здесь можно добавить логирование
        }
    })?;

    println!("{:?}", result);
    Ok(())
}

try_fold — это версия fold для Result. Если на любом шаге возвращается Err, свёртка останавливается. Вы можете добавить логирование, изменить ошибку и т.д.

Сравнение подходов: что выбрать?

Давайте сведём всё в таблицу:

  • collect::<Result<Vec<T>, E>>() — когда нужно собрать все успешные значения или получить первую ошибку. Идеально для парсинга.
  • filter_map + ok() — когда ошибки можно игнорировать. Просто и понятно.

Блоги

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