【问题标题】:How do I stop iteration and return an error when Iterator::map returns a Result::Err?当 Iterator::map 返回 Result::Err 时,如何停止迭代并返回错误?
【发布时间】:2014-12-09 16:31:17
【问题描述】:

我有一个返回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 中最好地处理这个问题?

【问题讨论】:

    标签: rust rust-result


    【解决方案1】:

    Resultimplements 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);
    }
    

    Playground

    【讨论】:

    • +1 这太棒了! (我的答案中的示例移植到此:is.gd/E26iv9
    • @KaiSellgren 是的,你可以应用同样的技巧。关键在collect的类型签名中,它在返回类型上是多态的,必须实现FromIterator。我不知道您所说的“它可以以更广泛的方式应用”是什么意思。 Rust 支持多态返回类型......所以,是吗? (有关返回类型多态性的更多示例,请参阅 RngDefault 特征。)
    • @KaiSellgren from_itercollect 方法中被调用。
    • 使用collect() 要求迭代器是有限的,对吗?如果是这样,如何处理类似但无限的迭代器?
    • 如果有多个map()s,你会怎么做?如果第一个map() 返回Result,那么后面的map() 也必须接受Result,这可能很烦人。有没有办法从map() 链的中间实现相同的目标?当然,不只是做.map(...).collect&lt;Result&lt;Vec&lt;_&gt;, _&gt;&gt;()?.into_iter().map(...)
    【解决方案2】:

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

    如前所述,for 可用于模拟错误停止,但这有时并不优雅,例如当您想调用max() 或其他消耗方法时。在其他情况下,这几乎是不可能的,例如消费方法在另一个 crate 中,例如 itertoolsRayon1

    迭代器消费者:try_for_each

    当您控制迭代器的使用方式时,您可以使用try_for_each 在第一个错误时停止。它接受一个返回Result 的闭包,如果闭包每次都返回Oktry_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。

    这种方法要求您控制迭代器的使用方式。如果这是由不受您控制的代码完成的 - 例如,如果您将迭代器传递给 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(&amp;mut err, until_err) (playground)。

    这些示例用for_each() 简单地耗尽了迭代器,但可以使用任意操作将其链接起来,包括 Rayon 的par_bridge()。使用 scan() 甚至可以将 collect() 项目放入容器中,并可以访问在错误之前看到的项目,这在收集到 Result&lt;Container, Error&gt; 时有时很有用且不可用。


    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 无法轻易实现等效效果。

    【讨论】:

    • 当你想sum() [...] Ok 项目时 - 这已经在标准库中实现,使用与@987654360相同的技术itertools 中的@方法。
    • @Shepmaster 我不知道process_results(),谢谢。它的好处是它不需要单独的错误变量。它的缺点是它只能作为调用您的顶级函数使用(在并行迭代多个事物时可能会出现问题),并且它需要一个外部板条箱。此答案中的代码相当短,可与 stdlib 一起使用,并参与迭代器链接。
    【解决方案3】:

    此答案适用于 1.0 之前的 Rust 版本,并且已删除所需的功能

    您可以为此使用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

    【讨论】:

      【解决方案4】:

      处理嵌套.map()闭包Result

      如果我们在.map() 中有一个.map() 和一个.map() 怎么办?

      以下是嵌套 .map() 操作的特定情况的示例。它解决的问题是如何从最里面的闭包传播失败,同时避免使用.unwrap() 中止应用程序。

      这种方法还可以在外层使用? 语法在发生错误时捕获错误,或者在未发生错误时解包结果以分配给变量。 ? 不能在闭包内部使用。

      .parse() 在下面使用时将返回Result&lt;T, ParseIntError&gt;

      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::&lt;..&gt;() 泛型表达式指定Result&lt;Vec&lt;Vec&lt;..&gt;&gt;。内部.collect() 将生成Results,它们被外部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 }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-10-04
        • 2020-04-29
        相关资源
        最近更新 更多