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

She*_*ter 21 closures rust

此代码是从迭代器生成一组唯一项的低效方法.为了实现这一点,我试图用a 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);
}
Run Code Online (Sandbox Code Playgroud)

但是,编译失败了:

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
Run Code Online (Sandbox Code Playgroud)

huo*_*uon 27

这有点令人惊讶,但不是一个bug.

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 { ... }
Run Code Online (Sandbox Code Playgroud)

这使得非法性更加清晰:试图摆脱&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| |         })
  | |_________^
Run Code Online (Sandbox Code Playgroud)

删除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> { ... }
Run Code Online (Sandbox Code Playgroud)

(&mut self出于教学目的,我已将这一命名命名为.)

这种情况肯定更加微妙.该FilterMap迭代器内部存储的封闭,这意味着在封闭值(即,任何引用它捕捉)必须是有效的,只要任何引用FilterMap值被抛向四周,而且,对于&mut引用,任何引用必须要小心被不走样.

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

对于错误消息:self有效(忽略结构包装器)&'b mut (&'a mut Vec<i32>)其中'b&mut self引用的生命周期,并且'a是引用的生命周期struct.移动内部&mut是非法的:不能像&mut引用一样移动仿射类型(&Vec<i32>虽然它可以使用),所以唯一的选择是重新借用.再利用正在通过外部参考,因此不能超过它,也就是说,再&mut *self.seen利用是一个&'b mut Vec<i32>,而不是一个&'a mut Vec<i32>.

这使得内部闭包具有类型InnerClosure<'b>,因此该call_mut方法试图返回a FilterMap<..., InnerClosure<'b>>.不幸的是,这个FnMut特性定义call_mut为公正

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

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

这与该Iterator::next方法非常相似,并且此处的代码失败的原因基本上与迭代器本身拥有的对内存的引用不具有迭代器的原因相同.(我想象一个"流迭代器"(迭代器之间有链接&mut self和返回值next)库将能够提供一个flat_map适用于近乎编写的代码:需要具有类似链接的"闭包"特征.)

解决方法包括:

  • RefCellRenato Zannon 的建议,允许seen作为共享借用&.除了更改&mut Vec<i32>to 之外,desugared闭包代码基本相同&Vec<i32>.这一变化意味着"rebo​​rrow"的&'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);
}
Run Code Online (Sandbox Code Playgroud)

我想这个RefCell版本效率更高.

  • 很棒的答案!很有教育意义 另一种选择是使用for循环编写代码 - 它可以说比两种解决方案都简单,并且可能更有效 (4认同)

Ren*_*non 7

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

作为一种解决方法,可以诉诸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);
}
Run Code Online (Sandbox Code Playgroud)