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

我有一个返回 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 我该怎么处理这件事?

37260 次浏览

这个答案与 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!)

演示

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

游乐场

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

如前所述,for可以用于模拟错误停止,但是有时候这样做并不优雅,比如当您想要调用 max()或其他消耗方法时。在其他情况下,这几乎是不可能的,因为当消费方法是在另一个板条箱,如 itertools瑞恩1

迭代器使用者: 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。

这种方法要求您控制迭代器的使用方式。如果这是由不在您控制之下的代码完成的——例如,如果您正在将迭代器传递给 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来解决,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)(游乐场)。

这些示例使用 for_each()简单地耗尽迭代器,但是可以使用任意操作(包括 Rayon 的 par_bridge())将其链接起来。使用 scan(),甚至可以将物品 collect()放入一个容器中,并且可以访问错误发生之前看到的物品,这在收集到 Result<Container, Error>时有时是有用的和不可用的。


当使用 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中也不能达到等效效果。

处理嵌套的 .map()闭合

如果我们有一个 .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将被赋予这个2-D 向量; 或者 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 }