【问题标题】:Pass an iterable to a function and iterate twice in rust将一个可迭代对象传递给一个函数并在 rust 中迭代两次
【发布时间】:2021-11-08 21:18:31
【问题描述】:

我有一个看起来像的函数

fn do_stuff(values: HashSet<String>) {
  // Count stuff
  for s in values.iter() {
    prepare(s);
  }
  // Process stuff 
  for s in values.iter() {
    process(s);
  }
}

这很好用。对于单元测试,我想传递一个两值集合,其中元素以已知顺序传递。 (以其他顺序处理它们不会测试我要测试的情况。)HashSet 不保证顺序,所以我想通过 Vec 代替。

我想将参数更改为Iterable,但似乎只有IntoIter 存在。我试过了

fn do_stuff<C>(values: C)
where C: IntoIterator<Item=String>
 {
  // Count stuff
  for s in values {
    prepare(s);
  }
  // Process stuff 
  for s in values {
    process(s);
  }
}

失败是因为第一次迭代消耗了values。编译器建议借用values,但是

fn do_stuff<C>(values: C)
where C: IntoIterator<Item=String>
 {
  // Count stuff
  for s in &values {
    prepare(s);
  }
  // Process stuff 
  for s in values {
    process(s);
  }
}

失败是因为

特征 Iterator 没有为 &amp;C 实现

我可能会用clone 做一些工作,但实际的集合会很大,如果可能的话我想避免复制它。

考虑一下,签名可能应该是do_stuff(values: &amp;C),所以如果这样可以使问题更简单,那么这是一个可以接受的解决方案。

SO 建议将Writing a generic function that takes an iterable container as parameter in Rust 作为一个相关问题,但这是一个终身问题。我的生命周期没有问题。

看起来How to create an `Iterable` trait for references in Rust? 实际上可能是解决方案。但我无法编译它。

我的第一次尝试是


pub trait Iterable {
    type Item;
    type Iter: Iterator<Item = Self::Item>;
    fn iterator(&self) -> Self::Iter;
}

impl Iterable for HashSet<String> {
    type Item = String;
    type Iter = HashSet<String>::Iterator;
    fn iterator(&self) -> Self::Iter {
        self.iter()
    }
}

失败了

error[E0223]: ambiguous associated type
   --> src/file.rs:178:17
    |
178 |     type Iter = HashSet<String>::Iterator;
    |                 ^^^^^^^^^^^^^^^^^^^^^^^^^ help: use fully-qualified syntax: `<HashSet<std::string::String> as Trait>::Iterator`

遵循该建议:

impl Iterable for HashSet<String> {
    type Item = String;
    type Iter = <HashSet<std::string::String> as Trait>::Iterator;
    fn iterator(&self) -> Self::Iter {
        self.iter()
    }
}

失败

error[E0433]: failed to resolve: use of undeclared type `Trait`
   --> src/file.rs:178:50
    |
178 |     type Iter = <HashSet<std::string::String> as Trait>::Iterator;
    |                                                  ^^^^^ use of undeclared type `Trait`

rust 文档似乎没有将Trait 列为已知类型。如果我将Trait 替换为HashSet,它不会将IteratorIntoIter 识别为表达式中的最终值。

接受答案的实施

尝试实现@eggyal 答案,我能够编译它

use std::collections::HashSet;

fn do_stuff<I>(iterable: I)
where
    I: IntoIterator + Copy,
    I::Item: AsRef<str>,
{
    // Count stuff
    for s in iterable {
        prepare(s.as_ref());
    }
    // Process stuff
    for s in iterable {
        process(s.as_ref());
    }
}

fn prepare(s: &str) {
    println!("prepare: {}", s)
}
fn process(s: &str) {
    println!("process: {}", s)
}

#[cfg(test)]
mod test_cluster {
    use super::*;

    #[test]
    fn doit() {
        let vec: Vec<String> = vec!["a".to_string(), "b".to_string(), "c".to_string()];
        let set = vec.iter().cloned().collect::<HashSet<_>>();
        do_stuff(&vec);
        do_stuff(&set);
    }
}

有这个输出

 ---- simple::test_cluster::doit stdout ----
prepare: a
prepare: b
prepare: c
process: a
process: b
process: c
prepare: c
prepare: b
prepare: a
process: c
process: b
process: a

【问题讨论】:

    标签: rust


    【解决方案1】:

    IntoIterator 不仅由集合类型本身实现,而且在大多数情况下(包括VecHashSet它也由它们的借用实现 (产生借用项目的迭代器)。此外,不可变借用总是Copy。所以你可以这样做:

    fn do_stuff<I>(iterable: I)
    where
        I: IntoIterator + Copy,
        I::Item: AsRef<str>,
    {
        // Count stuff
        for s in iterable {
            prepare(s);
        }
        // Process stuff 
        for s in iterable {
            process(s);
        }
    }
    

    然后这将通过传入相关集合的借用来调用:

    let vec = vec!["a", "b", "c"];
    let set = vec.iter().cloned().collect::<HashSet<_>>();
    do_stuff(&vec);
    do_stuff(&set);
    

    Playground.

    但是,根据您的要求(是否必须首先准备所有项目才能处理任何项目),在这种情况下,可以将准备和处理合并到迭代器的单次传递中。

    【讨论】:

    • 如果您使用s.as_ref() 而不是s,这似乎可行。 s 本身是否可以直接使用,而无需 as_ref()?如果我尝试使用它,我会收到有关关联类型的错误以及限制 &lt;I as IntoIterator&gt;::Item 的建议
    • @TroyDaniels:这个答案的previous revision 明确需要&amp;&amp;str 项目类型,这不需要.as_ref();但是,这也会将您限制为具有该项目类型的迭代器,而这是更通用的。由你决定。
    【解决方案2】:

    如果您想对容器进行两次迭代,可以克隆容器上的迭代器,因此接受 IntoIterator + Clone 应该适合您。示例代码:

    fn do_stuff<I>(values: I)
    where
        I: IntoIterator + Clone,
    {
        // Count stuff
        for s in values.clone() {
            prepare(s);
        }
        // Process stuff
        for s in values {
            process(s);
        }
    }
    

    您现在可以传入例如可以是哈希集,也可以是向量,两者都可以迭代两次:

    let vec = vec!["a", "b", "c"];
    let set: HashSet<_> = vec.iter().cloned().collect();
    do_stuff(vec);
    do_stuff(set);
    

    (Playground)

    【讨论】:

    • OP 不想克隆容器
    • 可以安全地克隆HashSetVec 的迭代器吗?
    • 这不需要克隆容器。我选择传入容器本身,因此它们被克隆,但您也可以在容器上传入迭代器,例如 vec.iter()set.iter()
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多