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

pgpifvop  于 2023-02-08  发布在  其他
关注(0)|答案(4)|浏览(178)

我有一个函数返回一个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将过滤掉它。
然而,我不想 * 忽略 * 错误,我想让整个代码块停止并返回一个新的错误(基于Map中出现的错误,或者只是转发现有的错误)。
在拉斯特我该怎么处理

7lrncoxx

7lrncoxx1#

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

Playground

50pmv0ei

50pmv0ei2#

可接受的答案显示了如何在 collecting 时在出错时停止,这很好,因为这是OP所要求的,如果您需要处理大型或无限的易出错迭代器,请继续阅读。
如前所述,for可以用来模拟错误停止,但有时候这并不优雅,比如当你想调用max()或其他使用迭代器的方法时,在其他情况下这几乎是不可能的,比如迭代器被另一个机箱中的代码使用,比如itertoolsRayon 1。

迭代器使用者: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也可以使用相同的模式。
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来解决,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()甚至可以将项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)
}

同样,通过收集到Result中不能轻易地实现等效效果。

bjp0bcyl

bjp0bcyl3#

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

如果.map()中的.map()中有.map(),会怎么样?
下面是一个嵌套.map()操作的例子,它解决的问题是如何从最里面的闭包传播失败,同时避免使用.unwrap(),因为.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在获取Ok内容并生成2-D向量时将其剥离。
如果不严重依赖类型推断,内部.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 }
7hiiyaii

7hiiyaii4#

此答案与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

相关问题