Avatar ·

Как прервать итерацию и вернуть ошибку, если Iterator::map возвращает Result::Err?

Привет, коллеги! Я новичок в Rust и столкнулся с проблемой при работе с итераторами и обработкой ошибок. У меня есть вектор строк, которые нужно распарсить в числа. Я использую `Iterator::map` с `str::parse::()`, который возвращает `Result`. Если парсинг не удался, я хочу немедленно прекратить итерацию и вернуть ошибку из функции, а не продолжать обрабатывать остальные элементы. Вот что я пробую: ```rust use std::num::ParseIntError; fn parse_numbers(input: &[&str]) -> Result, ParseIntError> { input.iter() .map(|s| s.parse::()) .collect() } ``` Этот код работает, но он собирает все результаты в `Result, ParseIntError>`. Если одна строка не парсится, ошибка всё равно возвращается, но я подозреваю, что `collect` обрабатывает все элементы, даже если один из них — `Err`. Мне нужно, чтобы при первой же ошибке итерация прерывалась и ошибка возвращалась сразу, без обработки остальных строк. Как правильно реализовать такое досрочное прерывание? Я слышал, что есть методы вроде `try_for_each` или `filter_map`, но не понимаю, как их применить для возврата ошибки из функции. Буду благодарен за примеры с объяснением. <<>>

Проблема с collect и ранним прерыванием

Ваш текущий код с collect на самом деле уже прерывает итерацию при первой ошибке, если тип результата — Result, ParseIntError>. Это работает благодаря тому, что collect для Iterator> реализован особым образом: он вызывает Try::branch на каждом элементе и при встрече Err немедленно останавливает итерацию, возвращая эту ошибку. Однако, если вы хотите явно контролировать процесс или использовать другие методы, есть альтернативы.

Решение с try_for_each

Метод try_for_each идеально подходит для вашей задачи. Он вызывает замыкание для каждого элемента и останавливается при первой ошибке. Вот пример:

use std::num::ParseIntError;

fn parse_numbers(input: &[&str]) -> Result<Vec<i32>, ParseIntError> {
    let mut result = Vec::new();
    input.iter().try_for_each(|s| {
        let num = s.parse::<i32>()?; // ? немедленно вернёт Err
        result.push(num);
        Ok(())
    })?;
    Ok(result)
}

Здесь try_for_each принимает замыкание, возвращающее Result<(), ParseIntError>. Если парсинг не удался, оператор ? возвращает Err из замыкания, и try_for_each немедленно завершается. Затем мы используем ? на результате try_for_each, чтобы передать ошибку наверх.

Решение с циклом for и ранним выходом

Самый простой и читаемый способ — использовать обычный цикл for с ранним выходом:

fn parse_numbers(input: &[&str]) -> Result<Vec<i32>, ParseIntError> {
    let mut result = Vec::new();
    for s in input {
        let num = s.parse::<i32>()?; // при ошибке сразу возвращаем Err
        result.push(num);
    }
    Ok(result)
}

Этот подход предпочтительнее, если логика обработки сложнее простого маппинга. Он явно показывает, что итерация прерывается при ошибке, и легко поддерживается.

Решение с filter_map (не подходит)

Метод filter_map отфильтровывает None и Err, что не подходит, так как вы хотите вернуть ошибку, а не игнорировать её. Используйте его только если нужно пропускать неудачные элементы без остановки.

Почему collect работает?

Как упоминалось, collect для Result использует типаж FromIterator, который реализован через Try. Это означает, что:

  • При встрече Ok(T) элемент добавляется в вектор.
  • При встрече Err(E) итерация немедленно прекращается, и ошибка возвращается.

Поэтому ваш исходный код уже работает корректно и прерывается при первой ошибке. Но если вы хотите явно видеть логику прерывания, используйте цикл или try_for_each.

Рекомендация

Для простого случая используйте collect — это идиоматично и кратко. Если нужна дополнительная логика (например, логирование или преобразование ошибки), предпочтите цикл for или try_for_each. Избегайте filter_map, если не хотите терять ошибки.

<<>>

Альтернативный подход: использование метода fold с ранним выходом

Метод fold позволяет аккумулировать результат с контролем прерывания. Хотя fold сам по себе не поддерживает досрочный выход, вы можете использовать Result как аккумулятор и оператор ? для передачи ошибки:

use std::num::ParseIntError;

fn parse_numbers(input: &[&str]) -> Result<Vec<i32>, ParseIntError> {
    input.iter().fold(Ok(Vec::new()), |acc, s| {
        let mut vec = acc?; // если была ошибка, возвращаем её
        let num = s.parse::<i32>()?;
        vec.push(num);
        Ok(vec)
    })
}

Здесь fold проходит по всем элементам, но при первой ошибке acc? вернёт Err, и последующие итерации будут работать с этим Err, но формально fold не прерывается — он просто передаёт ошибку дальше. Это менее эффективно, чем try_for_each, но может быть полезно, если вам нужно, например, собрать все ошибки (тогда используйте Result<Vec<T>, Vec<E>>).

Для вашей задачи лучше использовать try_for_each или цикл for, так как они явно прерывают итерацию при ошибке и более читаемы.