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

How can I move a captured variable into a closure within a closure?

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

借用检查器似乎对嵌套闭包 + 可变借用感到困惑。可能值得提交一个问题。 编辑:参见 为什么这不是一个错误。

作为解决方法,可以在此处诉诸 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);
}

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

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| |         })
  | |_________^

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

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

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

编译器无法确定 flat_map 不会,例如将所有 returned 迭代器存储在 Vec<FilterMap<...>> 中,这将导致一堆别名 &muts...非常糟糕!我 认为 flat_map 的这种特定用法恰好是安全的,但我不确定它是否一般,而且肯定有与 [= 具有相同签名样式的函数15=](例如 map)肯定是 unsafe。 (事实上​​ ,将代码中的 flat_map 替换为 map 给出了我刚刚描述的 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 方法正在尝试 return 一个 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 引用本身的生命周期与 returned 值之间没有任何联系,因此尝试 return InnerClosure<'b> 其中有 link。这就是编译器抱怨生命周期太短而无法重新借用的原因。

这与 Iterator::next 方法非常相似,这里的代码失败的原因基本上与不能让迭代器访问迭代器本身拥有的内存的引用相同。 (我想象一个 "streaming iterator"(在 &mut selfnext 中的 return 值之间具有 link 的迭代器)库将能够提供一个 flat_map 与几乎编写的代码一起工作:需要 "closure" 具有类似 link 的特征。)

解决方法包括:

  • Renato Zannon 建议的 RefCell,它允许 seen 作为共享 & 借用。除了将 &mut Vec<i32> 更改为 &Vec<i32> 之外,脱糖的闭包代码基本相同。此更改意味着 &'b mut &'a RefCell<Vec<i32>> 的 "reborrow" 可以只是 &mut&'a ... 的副本。是字面复制,所以保留了生命周期。
  • 避免迭代器的懒惰,避免 returning 内部闭包,特别是 .collect::<Vec<_>>()ing 在循环内部到 运行 通过整个 filter_map before returning.
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 版本效率更高。

我在使用 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();