Avatar ·

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

Привет, опытные растоводы! Я новичок в Rust и бьюсь над проблемой уже несколько часов. У меня есть вектор строк, которые я хочу преобразовать в числа с помощью `Iterator::map` и `str::parse::()`. Проблема в том, что `parse` возвращает `Result`, и если хоть одна строка не является числом, я хочу немедленно остановить всю итерацию и вернуть ошибку вызывающему коду. Вот мой код: ```rust fn parse_numbers(input: Vec<&str>) -> Vec { input.iter().map(|s| s.parse::().unwrap()).collect() } ``` `unwrap()` паникует, а мне нужно элегантное решение без паники. Я пробовал использовать `filter_map`, но он просто игнорирует ошибки, а мне нужно их пробрасывать. Ещё пробовал обернуть всё в `Result`: ```rust fn parse_numbers(input: Vec<&str>) -> Result, ParseIntError> { input.iter().map(|s| s.parse::()).collect() } ``` Но это не работает, потому что `collect` не знает, как собрать `Vec>` в `Result, _>`. Компилятор ругается на типы. Как правильно остановить итерацию при первой же ошибке и вернуть `Err`? Желательно без внешних крейтов, только стандартная библиотека. Заранее спасибо! <<>>

Решение проблемы: использование collect с Result

Ваша интуиция была верна — collect действительно умеет собирать Iterator> в Result, E>, но только если явно указать тип. Проблема в том, что Rust не может автоматически вывести, что вы хотите Result, а не Vec>. Нужно помочь компилятору с помощью турбо-рыбы или явной аннотации.

use std::num::ParseIntError;

fn parse_numbers(input: Vec<&str>) -> Result<Vec<i32>, ParseIntError> {
    input.iter()
        .map(|s| s.parse::<i32>())
        .collect()  // Ошибка: неоднозначный тип
}

Ошибка возникает, потому что collect может собрать как Vec<Result<i32, ParseIntError>>, так и Result<Vec<i32>, ParseIntError>. Решение — уточнить тип:

fn parse_numbers(input: Vec<&str>) -> Result<Vec<i32>, ParseIntError> {
    input.iter()
        .map(|s| s.parse::<i32>())
        .collect::<Result<Vec<_>, _>>()  // Турбо-рыба!
}

Как это работает под капотом

Стандартная библиотека реализует трейт FromIterator для Result. Когда вы пишете .collect::<Result<Vec<T>, E>>(), итератор вызывает FromIterator::from_iter, который:

  • Создаёт пустой Vec.
  • Проходит по каждому элементу итератора.
  • Если встречает Ok(value) — добавляет значение в вектор.
  • Если встречает Err(e) — немедленно прекращает итерацию и возвращает Err(e).

Таким образом, итерация останавливается на первой же ошибке, и вы получаете Err без паники.

Полный рабочий пример

use std::num::ParseIntError;

fn parse_numbers(input: Vec<&str>) -> Result<Vec<i32>, ParseIntError> {
    input.iter()
        .map(|s| s.parse::<i32>())
        .collect::<Result<Vec<_>, _>>()
}

fn main() {
    let valid = vec!["1", "2", "3"];
    let invalid = vec!["1", "abc", "3"];

    match parse_numbers(valid) {
        Ok(nums) => println!("Успех: {:?}", nums),
        Err(e) => println!("Ошибка: {}", e),
    }

    match parse_numbers(invalid) {
        Ok(nums) => println!("Успех: {:?}", nums),
        Err(e) => println!("Ошибка: {}", e),  // Выведет: Ошибка: invalid digit found in string
    }
}

Альтернативы с явной обработкой

Если вам нужно больше контроля (например, логировать каждую ошибку перед остановкой), можно использовать цикл for с ? оператором:

fn parse_numbers(input: Vec<&str>) -> Result<Vec<i32>, ParseIntError> {
    let mut result = Vec::with_capacity(input.len());
    for s in &input {
        let num = s.parse::<i32>()?;  // ? пробрасывает ошибку наверх
        result.push(num);
    }
    Ok(result)
}

Этот подход даёт ту же семантику "остановиться при первой ошибке", но позволяет вставить дополнительную логику (например, eprintln! или проверки).

Важно: Если вы хотите собрать все ошибки, а не только первую, используйте partition или крейт itertools с методом partition_map, но это уже выходит за рамки стандартной библиотеки.

<<>>

Дополнительный подход: использование try_for_each и try_fold

Помимо collect, у итераторов есть методы try_for_each и try_fold, которые специально созданы для работы с Result и Option. Они останавливают итерацию при первой же ошибке.

use std::num::ParseIntError;

fn parse_numbers(input: Vec<&str>) -> Result<Vec<i32>, ParseIntError> {
    let mut result = Vec::with_capacity(input.len());
    input.iter().try_for_each(|s| {
        let num = s.parse::<i32>()?;
        result.push(num);
        Ok(())
    })?;
    Ok(result)
}

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

Также есть try_fold для аккумуляции с ранним выходом:

fn parse_numbers(input: Vec<&str>) -> Result<Vec<i32>, ParseIntError> {
    input.iter().try_fold(Vec::new(), |mut acc, s| {
        let num = s.parse::<i32>()?;
        acc.push(num);
        Ok(acc)
    })
}

Оба метода эквивалентны по производительности, но try_fold более функциональный и не требует мутабельной переменной вне замыкания. Выбирайте тот, который лучше читается в вашем контексте.