Rust:如何在第一个 Err 或 None 上短路退出迭代器方法链?

Posted

技术标签:

【中文标题】Rust:如何在第一个 Err 或 None 上短路退出迭代器方法链?【英文标题】:Rust: How to short-circuit exit from a chain of iterator methods on first Err or None? 【发布时间】:2020-09-01 14:01:24 【问题描述】:

考虑一个迭代器方法链:

.iter().a().b().c()

其中a 产生Option(或Result)类型的值。只要a 产生None(或Err(_)),有没有办法让整个链返回None(或(Err(_))?

详细示例

给定函数valid(识别无意义的输入)和accept(一个 任意选择标准):

type T = u8;
type ERR = u8;

fn valid(x: &T) -> Result<T, ERR> 
    if *x < 10  Ok(*x)  else  Err(*x) 


fn accept(x: &T) -> bool 
    if *x > 9  panic!(" should have been rejected by validator")  
    *x % 2 == 0

我想写一个函数

fn count_accepted(data: &[T]) -> Result<usize, ERR>

哪个

一旦遇到第一个无效元素就返回Err(ERR) 输入数据

如果所有元素都有效,则返回包含值计数的Ok(usize) 满足accept 标准

这是一个使用循环的解决方案:

fn count_loop(data: &[T]) -> Result<usize, ERR> 
    let mut count = 0;
    for item in data 
        valid(&item)?;
        if accept(&item)  count += 1 
    
    Ok(count)

正如这些测试所证明的那样,这似乎可以按要求工作:

macro_rules! testem 
    ($count:path) => 
        #[test] fn empty()                  assert_eq!($count(&[])           , Ok(0)) 
        #[test] fn all_ok_and_accepted()    assert_eq!($count(&[2,6])        , Ok(2)) 
        #[test] fn all_ok_some_rejected()   assert_eq!($count(&[2,3])        , Ok(1)) 
        #[test] fn one_invalid()            assert_eq!($count(&[12])         , Err(12)) 
        #[test] fn stop_on_first_invalid()  assert_eq!($count(&[2,13,6,12,5]), Err(13)) 
    


mod test_loop testem!super::count_loop

我想了解是否/如何使用 迭代器而不是循环。

考虑一个相关但更简单的问题:如果任何数据无效,则保释 立即取出,否则将所有数据收集到一个向量中。换一种说法, 从上一个问题中删除accept 条件。

这个问题有一个比较满意的解决方案,因为FromIterator Result 的实现负责提前终止:

fn related(data: &[T]) -> Result<Vec<T>, ERR> 
    data.iter()
        .map(valid)
        .collect()


mod test_related 
    #[test]
    fn stop_on_first_invalid()  assert_eq!(super::related(&[2,13,6,12,5]), Err(13))

这是related 的扩展,它通过了与count_loop 相同的测试:

fn count_via_vec(data: &[T]) -> Result<usize, ERR> 
     Ok(data
        .iter()
        .map(valid)
        .filter(|x| x.is_err() || accept(&x.unwrap()))
        .collect::<Result<Vec<T>, ERR>>()?
        .len())


mod test_vvec testem!super::count_via_vec

但是:这个解决方案相对于count_loop 有很多缺点:

过滤条件非常嘈杂。

当第一个无效项有 已识别(与原始循环实现不同):出现? 比它应该晚 2 行...如果有意义的话。

向量被不必要地填充(除非 Rust 执行一些很酷的 我还没有意识到的优化),所以空间复杂度从 O(1) 到 O(N)。

最后一点通常可以通过替换来解决 .collect::&lt;Result&lt;Vec&lt;T&gt;, ERR&gt;&gt;()?.len()).count(),但这有 取消对无效案件的承认的进一步不利影响:他们 被简单地算作成功,正如测试失败所证明的那样 实现:

fn count_iterate(data: &[T]) -> Result<usize, ERR> 
    Ok(data
       .iter()
       .map(valid)
       .filter(|x| x.is_err() ||  accept(&x.unwrap()))
       .count())


mod test_iter testem!super::count_iterate

您能否建议一些在迭代器方法链中提前返回的机制 在这种情况下可以使用吗?

【问题讨论】:

您尝试过try_&lt;function&gt; 迭代器方法吗? try_for_each 似乎与您要查找的内容相似。 doc.rust-lang.org/std/iter/… 您最终找到任何干净的解决方案了吗?谢谢! 我发现了一些朝着正确方向迈出的一步,但是由于我已经远离 Rust 几个月了,记忆非常模糊。第一个是 docs.rs/fallible-iterator/0.2.0/fallible_iterator/index.html 。另一个是,在某些情况下,Error 的实现实际上可以很好地做到这一点(在某些博客文章中可能对此进行了讨论,也许是 Michael Snoyman 的?)这似乎是非常 i> 接近我想要的,但我目前无法找到它:-( 【参考方案1】:

您可能正在寻找具有impl&lt;T, U, E&gt; Sum&lt;Result&lt;U, E&gt;&gt; for Result&lt;T, E&gt; where T: Sum&lt;U&gt;std::iter::Sum,而对于所有基本整数,Sum 也有 impls。

所以下面的就行了:

fn valid(x: &u32) -> Result<u32, u32> 
    if *x < 10  Ok(1)  else  Err(*x) 


fn count(x: &[u32]) -> Result<u32, u32> 
    x.iter()
     .map(valid)
     .sum()


fn main() 
    println!(":?", count(&[13,1,2,3]));

正如Sum 上的文档所说,如果遇到错误,这将使迭代器短路。这将包括短路链式迭代器。

【讨论】:

ResultSum 实现很有趣,谢谢。但是,修改valid 标准的需要需要使用适配器,到那时它就变得比循环方法丑得多。此外,该解决方案忽略了下游链(一般示例中为.b().c(),具体示例中为filter(accept)),我不知道如何解决。即使它可以修复,它也是对这一类别中一个问题的临时解决方案,而不是对问题顶部提出的问题的解决方案:让它为 `iter.a::( ).b().c()" 一般。

以上是关于Rust:如何在第一个 Err 或 None 上短路退出迭代器方法链?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Rust 中关闭 Unix 套接字?

如何“使用”或导入本地 Rust 文件? [复制]

如何在 rust 中迭代 vec 或 slice 的前缀或后缀?

使用rust的openssl库加密/解密大文本

Twitter 流 API 从 None 给出 JSONDecodeError("Expecting value", s, err.value)

如何在rust中迭代str或String的前缀和后缀?