Введение: почему итератор не хочет останавливаться?
Представьте: вы пишете на 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() — когда ошибки можно игнорировать. Просто и понятно.