如何将捕获的变量移动到闭包内的闭包中?

时间:2015-02-14 23:58:29

标签: closures rust

此代码是一种从迭代器生成唯一项集的低效方法。为实现这一目标,我尝试使用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

2 个答案:

答案 0 :(得分:24)

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

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

这使得非法性更加清晰:试图离开&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| |         })
  | |_________^

删除move会使闭包捕获工作像

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

(出于教学目的,我已在此期间命名&mut self生命周期。)

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

编译器无法确定flat_map不会,例如将所有返回的迭代器存储在Vec<FilterMap<...>>中,这会导致一堆别名&mut ...非常糟糕!我认为 flat_map的这种特定用法恰好是安全的,但我不确定它是否一般,并且肯定有与flat_map具有相同签名风格的函数(例如map)绝对是unsafe。 (事实上​​,在代码中用flat_map替换map会产生我刚刚描述的Vec情况。)

对于错误消息:self实际上是(忽略结构包装器)&'b mut (&'a mut Vec<i32>)其中'b&mut self引用的生命周期,而'astruct中引用的生命周期。移动内部&mut是非法的:不能将&mut之类的仿射类型移出引用(但它可以与&Vec<i32>一起使用),因此唯一的选择是重新扩展。重新生成正在通过外部参考,因此不能超过它,即&mut *self.seen再次展开是&'b mut Vec<i32>,而不是&'a mut Vec<i32>

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

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

特别是,self引用本身的生命周期与返回值之间没有任何关联,因此尝试返回具有该链接的InnerClosure<'b>是非法的。这就是为什么编译器抱怨生命周期太短而无法再次出借的原因。

这与Iterator::next方法非常相似,并且此处的代码失败的原因基本上与迭代器本身拥有的内存引用无法使用迭代器的原因相同。 (我想一个"streaming iterator"&mut selfnext中的返回值之间的链接的迭代器)库将能够提供一个flat_map,它可以使用近乎编写的代码:需要具有类似链接的“封闭”特征。)

解决方法包括:

  • Renato Zannon建议的RefCell,允许seen作为共享&借用。除了将&mut Vec<i32>更改为&Vec<i32>之外,desugared闭包代码基本相同。此更改意味着&'b mut &'a RefCell<Vec<i32>>的“重新展开”只能是&'a ...&mut的副本。这是一个文字副本,因此保留了生命周期。
  • 避免迭代器的延迟,避免返回内部闭包,特别是.collect::<Vec<_>>()在循环内部,以便在返回之前遍历整个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版本效率更高。

答案 1 :(得分:6)

似乎借用检查器在嵌套闭包+可变借用上感到困惑。可能值得提出问题。编辑:请参阅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);
}