当 Iterator::map 返回 Result::Err 时,我该如何停止迭代并返回错误?

217

我有一个返回 Result 的函数:

fn find(id: &Id) -> Result<Item, ItemError> {
    // ...
}

然后另一个人这样使用它:

let parent_items: Vec<Item> = parent_ids.iter()
    .map(|id| find(id).unwrap())
    .collect();

如何处理在任何 map 迭代中出现失败的情况?

我知道可以使用 flat_map,在这种情况下,错误结果将被忽略

let parent_items: Vec<Item> = parent_ids.iter()
    .flat_map(|id| find(id).into_iter())
    .collect();

Result的迭代器根据成功状态具有0个或1个元素,如果为0,则flat_map将过滤掉它。

然而,我不想忽略错误,我想让整个代码块停止,并返回一个新错误(基于映射中出现的错误或直接转发现有的错误)。

在Rust中如何最好地处理这个问题?

5个回答

275

Result 实现了FromIterator,因此您可以将Result移动到外部,迭代器将处理其余部分(包括如果发现错误则停止迭代)。

#[derive(Debug)]
struct Item;
type Id = String;

fn find(id: &Id) -> Result<Item, String> {
    Err(format!("Not found: {:?}", id))
}

fn main() {
    let s = |s: &str| s.to_string();
    let ids = vec![s("1"), s("2"), s("3")];

    let items: Result<Vec<_>, _> = ids.iter().map(find).collect();
    println!("Result: {:?}", items);
}

游乐场


15
太棒了!(这是我答案中的例子转移到此处的链接:http://is.gd/E26iv9) - Dogbert
5
from_iter 方法在 collect 方法中被调用。具体可以参考链接:collect - BurntSushi5
2
使用collect()需要迭代器是有限的,对吗?如果是这样,那么类似但无限的迭代器该如何处理? - U007D
7
如果有多个map(),当第一个map()返回一个Result时,后续的map()也必须接收一个Result,这可能会很麻烦。是否有一种方法可以在map()链的中间实现相同的效果?当然,除了使用.map(...).collect<Result<Vec<_>, _>>()?.into_iter().map(...)这种方式。 - Good Night Nerd Pride
1
Rust By Example有关于这个的章节,也许你可以在回答中链接它: https://doc.rust-lang.org/rust-by-example/error/iter_result.html#fail-the-entire-operation-with-collect - Levi Morrison
显示剩余4条评论

68

接受的答案展示了如何在收集期间停止错误,这很好,因为这是 OP 请求的。如果您需要处理也适用于大型或无限不可靠迭代器,则请继续阅读。

如已经指出的那样,可以使用for来模拟停止错误,但有时这是不优雅的,比如当您想要调用max()或其他消耗迭代器的方法时。在其他情况下,这几乎是不可能的,例如当迭代器被另一个包中的代码(例如 itertoolsRayon1)消耗时。

迭代器消费者:try_for_each

当您控制如何消耗迭代器时,只需使用try_for_each来在第一个错误时停止。它接受返回Result的闭包,如果闭包每次都返回Ok,则try_for_each()将返回Ok(()),并返回第一个错误的Err。这使得闭包可以通过自然方式使用?运算符来检测错误:

use std::{fs, io};

fn main() -> io::Result<()> {
    fs::read_dir("/")?.try_for_each(|e| -> io::Result<()> {
        println!("{}", e?.path().display());
        Ok(())
    })?;
    // ...
    Ok(())
}

如果您需要在闭包调用之间保持状态,也可以使用try_fold。这两种方法都由ParallelIterator实现,因此相同的模式适用于Rayon。

try_for_each()确实要求您控制迭代器的消耗方式。如果由您无法控制的代码完成-例如,如果您将迭代器传递给itertools :: merge()或类似工具,则需要一个适配器。

迭代器适配器:scan

第一次停止错误的尝试是使用take_while

use std::{io, fs};

fn main() -> io::Result<()> {
    fs::read_dir("/")?
        .take_while(Result::is_ok)
        .map(Result::unwrap)
        .for_each(|e| println!("{}", e.path().display()));
    // ...
    Ok(())
}

这样做是可行的,但我们没有任何指示表明出现了错误,迭代只是静默停止。此外,它需要不美观的map(Result::unwrap),这使得程序似乎会在出错时崩溃,实际上不是这种情况,因为我们会在出错时停止。
这两个问题可以通过从take_while切换到更强大的组合器scan来解决,后者不仅支持停止迭代,而且将其回调拥有的项传递给它,允许闭包将错误提取给调用者:
fn main() -> io::Result<()> {
    let mut err = Ok(());
    fs::read_dir("/")?
        .scan(&mut err, |err, res| match res {
            Ok(o) => Some(o),
            Err(e) => {
                **err = Err(e);
                None
            }
        })
        .for_each(|e| println!("{}", e.path().display()));
    err?;
    // ...
    Ok(())
}

如果需要在多个地方使用,该闭包可以抽象成一个实用函数:

fn until_err<T, E>(err: &mut &mut Result<(), E>, item: Result<T, E>) -> Option<T> {
    match item {
        Ok(item) => Some(item),
        Err(e) => {
            **err = Err(e);
            None
        }
    }
}

...在这种情况下,我们可以将其作为.scan(&mut err, until_err)playground)调用。

这些示例使用for_each()轻松耗尽迭代器,但可以与任意操作链接,包括Rayon的par_bridge()。使用scan()甚至可以将项目收集到容器中并访问之前看到的项目,在将其收集到Result<Container,Error>时有时很有用且不可用。


1 当使用Rayon并行处理流式数据时,需要使用par_bridge()

fn process(input: impl BufRead + Send) -> std::Result<Output, Error> {
    let mut err = Ok(());
    let output = lines
        .input()
        .scan(&mut err, until_err)
        .par_bridge()
        .map(|line| ... executed in parallel ... )
        .reduce(|item| ... also executed in parallel ...);
    err?;
    ...
    Ok(output)
}

同样的,通过收集到Result中无法轻松实现等效效果。


2
https://docs.rs/itertools/0.9.0/itertools/fn.process_results.html - Shepmaster
当您想要对“Ok”项进行sum()操作时,这已经在标准库中实现了,使用的是与itertools中的process_results方法相同的技术。 - Shepmaster
1
@Shepmaster 我不知道 process_results(),谢谢。 它的好处是不需要单独的错误变量。 它的缺点是它只作为顶层函数可用,并调用您(在同时迭代多个事物时可能会出现问题),并且它需要一个外部 crate。 此答案中的代码相对较短,使用 stdlib 工作,并参与迭代器链接。 - user4815162342

5

处理嵌套的.map()闭包中的Result

如果我们在一个.map()里面有另一个.map(),再在其中有一个.map()怎么办?

以下是一个特定情况的示例,其中.map()操作是嵌套的。它解决的问题是如何传播最内部闭包的失败,同时避免使用.unwrap(),因为使用该函数会中止应用程序。

这种方法还可以在外层使用?语法来捕获错误(如果发生错误),或者解开结果以分配到一个变量(如果没有错误发生)。但是从闭包内部不能使用?

.parse()如下所示将返回Result<T,ParseIntError>

use std::error::Error;

const DATA: &str = "1 2 3 4\n5 6 7 8";

fn main() -> Result<(), Box<dyn Error>>
{
    let data = DATA.lines().map(|l| l.split_whitespace()
                                     .map(|n| n.parse() /* can fail */)
                                     .collect())
                           .collect::<Result<Vec<Vec<i32>>, _>>()?;
    println!("{:?}", data);
    Ok(())
}

请注意,外部的.collect::<..>()泛型表达式指定了Result<Vec<Vec<..>>。内部的.collect()将产生Result,这些Result会通过外部的Result被剥离掉,并且取出Ok内容并生成二维向量。
不依赖于类型推断,内部的.collect()泛型表达式将如下所示:
          .collect::<Result<Vec<i32>, _>>()) // <--- Inner.
    .collect::<Result<Vec<Vec<i32>>, _>>()?; // <--- Outer.

使用 ? 语法,变量 data 将被赋值为这个二维向量;否则 main() 函数会返回一个源自内部闭包的解析错误。
[[1, 2, 3, 4], [5, 6, 7, 8]]

更进一步的,可以用这种方式处理嵌套三层深度的解析结果。
type Vec3D<T, E> = Result<Vec<Vec<Vec<T>>>, E>;

const DATA: &str = "1 2 | 3 4\n5 6 | 7 8";

fn main() -> Result<(), Box<dyn Error>>
{
    let data = DATA.lines()
                   .map(|a| a.split("|")
                             .map(|b| b.split_whitespace()
                                       .map(|c| c.parse()) // <---
                                       .collect())
                             .collect())
                   .collect::<Vec3D<i32,_>>()?;
    println!("{:?}", data);
    Ok(())
}

输出:

[[[1, 2], [3, 4]], [[5, 6], [7, 8]]]

或者如果无法解析数字,则会收到以下消息:

Error: ParseIntError { kind: InvalidDigit }

外部结果如何消除内部结果?对我来说,这有点像魔术。底层发生了什么? - Dulguun Otgon
1
@DulguunOtgon,编译器的类型推断功能使用声明为Vec3D的类型来解析嵌套的.map()返回值。请注意在lines迭代器上调用的.collect(),它指定了返回类型。 - Todd
1
@DulguunOtgon,“Result”以这样的方式实现了“FromIterator”,它可以接收内部“.map()”迭代器的“Result”对象并创建一个新的“Result”对象。如果任何一个内部迭代器返回错误的“Result”值,它将向上传播到更高层级。如果有人能更好地解释这个问题,请随时参与讨论。 - Todd
哦,你是指这个链接吗?https://doc.rust-lang.org/std/result/enum.Result.html#impl-FromIterator%3CResult%3CA,+E%3E%3E-for-Result%3CV,+E%3E - Dulguun Otgon

2

此答案适用于 Rust 的 1.0 版本之前,所需函数已被删除

您可以使用std::result::fold函数来实现。它在遇到第一个 Err 后停止迭代。

这是我刚写的一个示例程序:

fn main() {
  println!("{}", go([1, 2, 3]));
  println!("{}", go([1, -2, 3]));
}

fn go(v: &[int]) -> Result<Vec<int>, String> {
    std::result::fold(
        v.iter().map(|&n| is_positive(n)),
        vec![],
        |mut v, e| {
            v.push(e);
            v
        })
}

fn is_positive(n: int) -> Result<int, String> {
    if n > 0 {
        Ok(n)
    } else {
        Err(format!("{} is not positive!", n))
    }
}

输出:

Ok([1, 2, 3])
Err(-2 is not positive!)

Demo


0
rust-by-example改编而来。
我使用map_while()来停止迭代并在迭代器映射返回结果时返回错误。
"map_while()接受一个闭包作为参数。它会对迭代器的每个元素调用该闭包,并在闭包返回Some(_)时产生元素。"
取消注释行let numbers: Vec<i32> = str_to_i32_v2(&strings); 并注释行let numbers: Vec<i32> = str_to_i32(&strings); 以查看差异。
use std::{error::Error, process};

/// From:
/// <https://doc.rust-lang.org/rust-by-example/error/iter_result.html#fail-the-entire-operation-with-collect>

fn main() -> Result<(), Box<dyn Error>> {
    let s01 = vec!["-3", "0", "1", "2"];
    let s02 = vec!["-1", "-2", "0"];
    let s03 = vec!["3", "4", "tofu", "5"];
    let s04 = vec!["6", "7", "8", "9"];

    for (index, strings) in [s01, s02, s03, s04].iter().enumerate() {
        let numbers: Vec<i32> = str_to_i32(&strings);
        //let numbers: Vec<i32> = str_to_i32_v2(&strings);
        println!("s{:02}: {:?}", index + 1, numbers);
    }

    Ok(())
}

#[allow(dead_code)]
fn str_to_i32(strings: &[&str]) -> Vec<i32> {
    strings
        .iter()
        .map(|s| s.parse::<i32>())
        //.map_while(Result::ok)
        .map_while(|result| match result {
            Ok(n) => Some(n),
            Err(why) => {
                eprintln!("Error: {why}");
                eprintln!("strings: {strings:?}");
                process::exit(1);
            }
        })
        .collect()
}

#[allow(dead_code)]
fn str_to_i32_v2(strings: &[&str]) -> Vec<i32> {
    strings
        .iter()
        .map(|s| s.parse::<i32>())
        .map_while(Result::ok)
        .collect()
}

看看Rust Playground

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接