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

Kai*_*ren 56 rust rust-result

我有一个函数返回一个Result:

fn find(id: &Id) -> Result<Item, ItemError> {
    // ...
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

Result迭代器有0或1个项目,具体取决于成功状态,flat_map如果为0则会过滤掉它.

但是,我不想忽略错误,我想改为使整个代码块停止并返回一个新错误(基于映射中出现的错误,或者只是转发现有错误).

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

Bur*_*hi5 80

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);
}
Run Code Online (Sandbox Code Playgroud)

操场

  • +1太棒了!(我的回答的例子移植到这个:http://is.gd/E26iv9) (7认同)
  • @KaiSellgren`from_iter`在[`collect`]中调用(http://doc.rust-lang.org/src/core/home/rustbuild/src/rust-buildbot/slave/nightly-linux/build/src /libcore/iter.rs.html#452-465)方法. (3认同)
  • 使用“collect()”要求迭代器是有限的,对吗?如果是这样,如何处理类似但无限的迭代器? (2认同)
  • 如果有多个“map()”,你会怎么做?如果第一个“map()”返回“Result”,那么后面的“map()”也必须接受“Result”,这可能会很烦人。有没有办法从“map()”链的中间实现相同的目的?当然,不只是执行 `.map(...).collect&lt;Result&lt;Vec&lt;_&gt;, _&gt;&gt;()?.into_iter().map(...)` 。 (2认同)

use*_*342 20

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

如前所述,for可用于模拟 stop-on-error,但这有时是不雅的,例如当您想要调用max()或其他消耗方法时。在其他情况下,这几乎是不可能的,例如当消费方法在另一个板条箱中时,例如itertoolsRayon 1

迭代器消费者: try_for_each

当您控制迭代器的使用方式时,您可以使用try_for_each在第一个错误时停止。它接受一个返回 a 的闭包,如果闭包每次都返回,并且在第一个错误时返回第一个Resulttry_for_each()它将返回。这允许闭包以自然的方式简单地使用运算符来检测错误:Ok(())OkErr?

use std::{fs, io};

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

如果您需要在闭包的调用之间维护状态,您还可以使用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(())
}
Run Code Online (Sandbox Code Playgroud)

这有效,但我们没有得到任何发生错误的迹象,迭代只是默默地停止。它还需要难看的东西map(Result::unwrap),这使得程序看起来会因错误而恐慌,实际上并非如此,因为我们会因错误而停止。

这两个问题都可以通过从take_whileto切换来解决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(())
}
Run Code Online (Sandbox Code Playgroud)

如果在多个地方需要,可以将闭包抽象为一个效用函数:

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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

这些示例用 简单地耗尽了迭代器for_each(),但可以用任意操作链接它,包括 Rayon 的par_bridge()。使用scan()它甚至可以collect()将项目放入容器中并可以访问在错误之前看到的项目,这在收集到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)
}
Run Code Online (Sandbox Code Playgroud)

同样,通过收集到Result.

  • https://docs.rs/itertools/0.9.0/itertools/fn.process_results.html (2认同)

Tod*_*odd 7

处理嵌套.map()Result

如果我们有.map()a 内.map()a 内的 a该怎么办.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(())
}
Run Code Online (Sandbox Code Playgroud)

请注意,外部.collect::<..>()通用表达式指定Result<Vec<Vec<..>>。内部.collect()将生成s,当外部获取内容并生成二维向量时Result,这些 s 会被外部剥离。ResultOk

如果不严重依赖类型推断,内部.collect()泛型表达式将如下所示:

          .collect::<Result<Vec<i32>, _>>()) // <--- Inner.
    .collect::<Result<Vec<Vec<i32>>, _>>()?; // <--- Outer.
Run Code Online (Sandbox Code Playgroud)

使用?语法,变量 ,data将被分配这个二维向量;否则该main()函数将返回源自内部闭包的解析错误。

输出:

[[1, 2, 3, 4], [5, 6, 7, 8]]
Run Code Online (Sandbox Code Playgroud)

更进一步,可以通过这种方式处理嵌套三层深度的解析结果。

[[1, 2, 3, 4], [5, 6, 7, 8]]
Run Code Online (Sandbox Code Playgroud)

输出:

[[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
Run Code Online (Sandbox Code Playgroud)

或者如果一个数字无法解析,我们会得到:

Error: ParseIntError { kind: InvalidDigit }
Run Code Online (Sandbox Code Playgroud)

  • @DulguunOtgon,“Result”以这样的方式实现“FromIterator”,即它可以接收内部“.map()”迭代器的“Result”对象并创建一个新的“Result”对象。如果任何内部迭代器返回错误的“Result”值,它将向上传播到各层。如果有人能更好地解释这一点,请随时插话。 (2认同)