132

我有一个返回 a 的函数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 项,具体取决于成功状态,flat_map如果为 0,则将其过滤掉。

但是,我不想忽略错误,而是想让整个代码块停止并返回一个新错误(基于地图中出现的错误,或者只是转发现有错误)。

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

4

4 回答 4

167

Result implementsFromIterator,因此您可以移动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);
}

操场

于 2014-10-14T21:43:24.790 回答
30

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

如前所述,for可用于模拟错误停止,但这有时并不优雅,例如当您想要调用max()或其他使用方法时。itertools在其他情况下,这几乎是不可能的,例如当消费方法在另一个 crate 中时,例如Rayon 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(())
}

如果您需要在闭包的调用之间维护状态,您也可以使用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),这使得程序看起来会因为错误而恐慌,实际上并非如此,因为我们会因错误而停止。

这两个问题都可以通过切换 from 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(())
}

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

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>.


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

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

于 2020-07-27T17:03:10.073 回答
2

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

演示

于 2014-10-14T19:53:46.203 回答
1

处理嵌套.map()Result

如果我们有一个inside a.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()将生成Results,外部将其剥离,Result因为它获取Ok内容并生成 2-D 向量。

在不严重依赖类型推断的情况下,内部.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 }
于 2021-09-06T04:14:16.870 回答