【问题标题】:How can I move a captured variable into a closure within a closure?如何将捕获的变量移动到闭包内的闭包中?
【发布时间】:2015-04-15 19:05:57
【问题描述】:

此代码是从迭代器生成一组唯一项的低效方式。为此,我尝试使用Vec 来跟踪我看到的值。我相信这个Vec 需要被最里面的闭包所拥有:

fn main() {
    let mut seen = vec![];
    let items = vec![vec![1i32, 2], vec![3], vec![1]];

    let a: Vec<_> = items
        .iter()
        .flat_map(move |inner_numbers| {
            inner_numbers.iter().filter_map(move |&number| {
                if !seen.contains(&number) {
                    seen.push(number);
                    Some(number)
                } else {
                    None
                }
            })
        })
        .collect();

    println!("{:?}", a);
}

但是,编译失败:

error[E0507]: cannot move out of captured outer variable in an `FnMut` closure
 --> src/main.rs:8:45
  |
2 |     let mut seen = vec![];
  |         -------- captured outer variable
...
8 |             inner_numbers.iter().filter_map(move |&number| {
  |                                             ^^^^^^^^^^^^^^ cannot move out of captured outer variable in an `FnMut` closure

【问题讨论】:

  • 你不需要moves,可变借用就可以了。但由于某种原因,我不完全理解借用检查器不允许它(如果 seen.push 调用被删除,它编译得很好)。我建议尝试将问题简化一下并提出问题。 flat_map 是迄今为止我遇到的大多数令人困惑的借用检查器错误的原因

标签: closures rust


【解决方案1】:

这有点令人惊讶,但不是错误。

flat_map 需要一个FnMut,因为它需要多次调用闭包。内部闭包上带有move 的代码失败,因为该闭包被创建了多次,每个inner_numbers 一次。如果我以显式形式编写闭包(即存储捕获的结构和闭包特征之一的实现),您的代码看起来(有点)像

struct OuterClosure {
    seen: Vec<i32>
}
struct InnerClosure {
    seen: Vec<i32>
}
impl FnMut(&Vec<i32>) -> iter::FilterMap<..., InnerClosure> for OuterClosure {
    fn call_mut(&mut self, (inner_numbers,): &Vec<i32>) -> iter::FilterMap<..., InnerClosure> {
        let inner = InnerClosure {
            seen: self.seen // uh oh! a move out of a &mut pointer
        };
        inner_numbers.iter().filter_map(inner)
    }
}
impl FnMut(&i32) -> Option<i32> for InnerClosure { ... }

这使得非法性更加明显:试图移出&amp;mut OuterClosure 变量。


理论上,仅仅捕获一个可变引用就足够了,因为 seen 只是在闭包内被修改(而不是移动)。但是,事情太懒了,无法正常工作...

error: lifetime of `seen` is too short to guarantee its contents can be safely reborrowed
 --> src/main.rs:9:45
  |
9 |             inner_numbers.iter().filter_map(|&number| {
  |                                             ^^^^^^^^^
  |
note: `seen` would have to be valid for the method call at 7:20...
 --> src/main.rs:7:21
  |
7 |       let a: Vec<_> = items.iter()
  |  _____________________^
8 | |         .flat_map(|inner_numbers| {
9 | |             inner_numbers.iter().filter_map(|&number| {
10| |                 if !seen.contains(&number) {
... |
17| |         })
18| |         .collect();
  | |__________________^
note: ...but `seen` is only valid for the lifetime  as defined on the body at 8:34
 --> src/main.rs:8:35
  |
8 |           .flat_map(|inner_numbers| {
  |  ___________________________________^
9 | |             inner_numbers.iter().filter_map(|&number| {
10| |                 if !seen.contains(&number) {
11| |                     seen.push(number);
... |
16| |             })
17| |         })
  | |_________^

删除moves 使闭包捕获工作起来像

struct OuterClosure<'a> {
    seen: &'a mut Vec<i32>
}
struct InnerClosure<'a> {
    seen: &'a mut Vec<i32>
}
impl<'a> FnMut(&Vec<i32>) -> iter::FilterMap<..., InnerClosure<??>> for OuterClosure<'a> {
    fn call_mut<'b>(&'b mut self, inner_numbers: &Vec<i32>) -> iter::FilterMap<..., InnerClosure<??>> {
        let inner = InnerClosure {
            seen: &mut *self.seen // can't move out, so must be a reborrow
        };
        inner_numbers.iter().filter_map(inner)
    }
}
impl<'a> FnMut(&i32) -> Option<i32> for InnerClosure<'a> { ... }

(出于教学目的,我将&amp;mut self 命名为这一生命周期。)

这种情况肯定更微妙。 FilterMap 迭代器在内部存储闭包,这意味着只要 FilterMap 值被抛出,闭包值中的任何引用(即它捕获的任何引用)都必须有效,并且对于 &amp;mut 引用, 任何引用都必须小心不带别名。

编译器不能确定flat_map 不会,例如将所有返回的迭代器存储在 Vec&lt;FilterMap&lt;...&gt;&gt; 中,这将导致一堆别名 &amp;muts... 非常糟糕!我认为flat_map 的这种特定用法恰好是安全的,但我不确定它是否一般,并且肯定有与flat_map 具有相同签名样式的函数(例如@987654343 @) 肯定是unsafe。 (实际上,将代码中的flat_map 替换为map 就是我刚才描述的Vec 的情况。)

对于错误消息:self 有效(忽略结构包装器)&amp;'b mut (&amp;'a mut Vec&lt;i32&gt;) 其中'b&amp;mut self 引用的生命周期,'astruct 中引用的生命周期。将内部&amp;mut 移出是非法的:不能将&amp;mut 之类的仿射类型移出引用(尽管它可以与&amp;Vec&lt;i32&gt; 一起使用),因此唯一的选择是重新借用。 reborrow 是通过外部引用进行的,因此不能超过它,也就是说,&amp;mut *self.seen reborrow 是 &amp;'b mut Vec&lt;i32&gt;,而不是 &amp;'a mut Vec&lt;i32&gt;

这使得内部闭包具有InnerClosure&lt;'b&gt; 类型,因此call_mut 方法试图返回FilterMap&lt;..., InnerClosure&lt;'b&gt;&gt;。不幸的是,the FnMut traitcall_mut 定义为只是

pub trait FnMut<Args>: FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

特别是,self 引用本身的生命周期与返回值之间没有联系,因此尝试返回具有该链接的InnerClosure&lt;'b&gt; 是非法的。这就是为什么编译器会抱怨生命周期太短而无法重新借用的原因。

这与Iterator::next 方法极为相似,并且这里的代码失败的原因基本相同,即无法在迭代器本身拥有的内存中引用迭代器。 (我想一个"streaming iterator"(在&amp;mut selfnext中的返回值之间有一个链接的迭代器)库将能够提供一个flat_map,它与几乎编写的代码一起工作:需要“闭包”特征类似的链接。)

解决方法包括:

  • Renato Zannon 建议的RefCell,它允许将seen 作为共享&amp; 借用。除了将&amp;mut Vec&lt;i32&gt; 改为&amp;Vec&lt;i32&gt; 之外,脱糖的闭包代码基本相同。此更改意味着&amp;'b mut &amp;'a RefCell&lt;Vec&lt;i32&gt;&gt; 的“重借”可以只是&amp;mut 之外的&amp;'a ... 的副本。它是文字副本,因此保留了生命周期。
  • 避免迭代器的惰性,避免返回内部闭包,特别是循环内部的.collect::&lt;Vec&lt;_&gt;&gt;()ing 在返回之前遍历整个filter_map
fn main() {
    let mut seen = vec![];
    let items = vec![vec![1i32, 2], vec![3], vec![1]];

    let a: Vec<_> = items
        .iter()
        .flat_map(|inner_numbers| {
            inner_numbers
                .iter()
                .filter_map(|&number| if !seen.contains(&number) {
                    seen.push(number);
                    Some(number)
                } else {
                    None
                })
                .collect::<Vec<_>>()
                .into_iter()
        })
        .collect();

    println!("{:?}", a);
}

我想RefCell 版本效率更高。

【讨论】:

  • 很好的答案!很有教育意义。另一种选择是只使用 for 循环编写代码 - 它可以说比两种解决方法都简单,并且可能更有效
【解决方案2】:

似乎借用检查器对嵌套闭包 + 可变借用感到困惑。可能值得提出一个问题。 编辑:请参阅 huon's answer 了解为什么这不是错误。

作为一种解决方法,可以在此处使用RefCell

use std::cell::RefCell;

fn main() {
    let seen = vec![];
    let items = vec![vec![1i32, 2], vec![3], vec![1]];

    let seen_cell = RefCell::new(seen);

    let a: Vec<_> = items
        .iter()
        .flat_map(|inner_numbers| {
            inner_numbers.iter().filter_map(|&number| {
                let mut borrowed = seen_cell.borrow_mut();

                if !borrowed.contains(&number) {
                    borrowed.push(number);
                    Some(number)
                } else {
                    None
                }
            })
        })
        .collect();

    println!("{:?}", a);
}

【讨论】:

    【解决方案3】:

    我在遇到类似问题后遇到了这个问题,使用flat_mapfilter_map。我通过将filter_map 移到flat_map 闭包之外解决了这个问题。

    使用您的示例:

    let a: Vec<_> = items
        .iter()
        .flat_map(|inner_numbers| inner_numbers.iter())
        .filter_map(move |&number| {
            if !seen.contains(&number) {
                seen.push(number);
                Some(number)
            } else {
                None
            }
        })
        .collect();
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-02-02
      • 2016-01-28
      • 2014-03-15
      • 2019-04-01
      • 2020-01-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多