【问题标题】:What is an idiomatic way to define a function taking an iterator without consuming in Rust? [duplicate]在 Rust 中定义一个采用迭代器而不消耗的函数的惯用方法是什么? [复制]
【发布时间】:2021-03-14 10:54:53
【问题描述】:

我一直在使用 Rust 完成 2020 年 Advent of Code,试图产生涉及相当通用函数的解决方案,这些函数(理论上)可以在其他地方重用,而不必过多担心类型。对于第 2 天,问题涉及检查文件的每一行以确保它是符合某些给定条件的字符串。我目前的做法是定义一个类似于以下的函数:

fn filter_lines<'a>(lines: impl IntoIterator<Item = &'a str>) -> Vec<&'a str> {
    lines
        .into_iter()
        .filter(|line| verify_line(line))
        .collect()
}

这里,verify_line 只是一些函数,它接受 &amp;str 并根据行是否符合特定规范生成布尔值。

问题是这个函数似乎必然会消耗迭代器。我尝试将其重写如下以使用对lines 的引用,并在对此进行操作之前克隆lines,但此代码无法编译:

fn filter_lines<'a>(lines: &impl IntoIterator<Item = &'a str>) -> Vec<&'a str> {
    let lines = lines.clone();
    lines
        .into_iter()
        .filter(|line| verify_line(line))
        .collect()
}

编译器产生以下错误:

error[E0507]: cannot move out of `*lines` which is behind a shared reference
  --> src/main.rs:40:5
   |
40 |     lines
   |     ^^^^^ move occurs because `*lines` has type `impl IntoIterator<Item = &'a str>`, which does not implement the `Copy` trait

我想我理解为什么不允许这样做,但我不确定如何惯用地定义一个不会消耗迭代器的函数,例如 filter_lines。那么,有什么更好的方法来实现这个filter_lines 函数,以便可以如下调用它?

let some_str: String = get_file_contents();
let lines = some_str.lines();
println!("Filtered lines: {:?}", filter_lines(&lines));
println!("All lines: {:?}", lines.collect::<Vec<&str>>());

【问题讨论】:

  • 我应该澄清一下:我已经解决了 Advent of Code 中提出的问题,我并不是想“欺骗”解决它,我的问题只是为了我自己的利益 :)跨度>
  • 请注意,IntoIterator 明确设计用于创建使用的迭代器。比较 into_iter()iter() 方法,例如在 Vec 上。 iter() 不消耗输入,但实际上由通用切片类型定义。这是因为如果你不使用迭代器,你只是引用它,因此它需要一个所有者。

标签: rust iterator


【解决方案1】:

对我来说,问题是这个函数似乎必然会消耗迭代器。

这在任何情况下都会发生,因为IntoIterator 就是这样做的。因此,如果您采用 IntoIterator 输入,则由 调用者 来正确设置它,例如Vec 有impl IntoIterator for Vec&lt;T&gt;impl IntoIterator for &amp;[T],所以调用者可以传入&amp;v——尽管无论如何这都需要一些调整,因为&amp;[T]IntoIterator 产生Iterator&lt;Item=&amp;T&gt;,所以这里是Iterator&lt;Item=&amp;&amp;str&gt;,它不会'没用。

但是由于每个Iterator 也实现了IntoIterator,所以这并不是一个真正的问题:调用者可以做一些调整。无论如何,我的观点是函数按原样工作,调用者需要更改。

现在至于具体的sn-p:

let some_str: String = get_file_contents();
let lines = some_str.lines();
println!("Filtered lines: {:?}", filter_lines(&lines));
println!("All lines: {:?}", lines.collect::<Vec<&str>>());

那个不能工作,因为Lines 是一个Iterator它将被任何迭代消耗:Rust迭代器不是“可重放的”,它只是可能的“可迭代”。但是尝试和重用lines() 也没有多大意义,那还能做什么?

  • 它可以重做整个事情,但只需调用两次some_str.lines() 并并行创建两个迭代器

  • 或者它可以记住整个事情,但随后只需 collect()Lines 并在随后的 Vec 上迭代,但是你需要,这基本上是你所期望的,因为你正在收集到一个vec 无论如何都要打印所有行,你不妨先这样做:

    let some_str: String = String::from("foo\nbar\nbaz");
    let lines = some_str.lines().collect::<Vec<_>>();
    // Vec<&str> -> Iterator<Item=&&str> -> Iterator<Item=&str>
    println!("Filtered lines: {:?}", filter_lines(lines.iter().map(|s| *s)));
    println!("All lines: {:?}", lines);
    

【讨论】:

  • 非常感谢您的有见地的回答,我真的很感激!这完全有道理,我认为我事先对迭代器的直觉有点偏离。
【解决方案2】:

并不是说这是执行此操作的惯用方式,但在您的 sn-p 中,您实际上是在克隆引用,因此它不起作用。

fn filter_lines<'a>(lines: &impl IntoIterator<Item = &'a str>) -> Vec<&'a str> {
    // Here you are creating a clone of the reference, not iterator
    let lines = lines.clone();
    lines
        .into_iter()
        .filter(|line| verify_line(line))
        .collect()
}

要克隆迭代器,您还需要为Clone 施加一个特征绑定:

fn filter_lines<'a>(lines: &(impl IntoIterator<Item = &'a str> + Clone)) -> Vec<&'a str> {
    let lines = lines.clone();
    lines
        .into_iter()
        .filter(|line| verify_line(line))
        .collect()
}

因为Lines implements Clone 也是如此,所以这将起作用。

Playground

【讨论】:

  • 啊,谢谢!由于 Masklinn 的回答,我将稍微改变我对这个问题的处理方法,但这绝对有助于理解编译器提出的错误:) 你知道为什么自动取消引用在这里不适用吗?
  • @user14749127, lines.clone() 将在这里工作,只要您为Clone 指定边界。引用和你的迭代器都实现了Clone。在第二种情况下,当您为Clone 指定边界时,由于类型推断,rust 得出的结论是您的意思是迭代器的克隆。但在第一种情况下,由于您的迭代器没有任何 clone() 方法,因此它坚持使用引用的克隆。
猜你喜欢
  • 2017-03-12
  • 1970-01-01
  • 2014-02-17
  • 2020-12-23
  • 2020-04-23
  • 2016-07-21
  • 2011-06-01
  • 2021-01-18
  • 2016-05-25
相关资源
最近更新 更多