web-dev-qa-db-ja.com

キャプチャされた変数をクロージャー内のクロージャーに移動するにはどうすればよいですか?

このコードは、イテレータから一意のアイテムセットを生成する非効率的な方法です。これを達成するために、私は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
22
Shepmaster

これは少し意外ですが、バグではありません。

flat_map は、クロージャーを複数回呼び出す必要があるため、FnMutを取ります。内部クロージャーにmoveが含まれるコードは、そのクロージャーがinner_numbersごとに1回ずつ作成されるため、失敗します。明示的な形式でクロージャ(つまり、キャプチャとクロージャトレイトの1つの実装を格納する構造体)を記述した場合、コードは(少し)のようになります。

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が確実に実行できないことを確信できません。返されたすべてのイテレータをVec<FilterMap<...>>に格納すると、エイリアスされた&mutsの山が発生します...私は考えますflat_mapのこの特定の使用法はたまたま安全ですが、それが一般的であるかどうかは確かではありませんし、同じスタイルの関数があることは確かですflat_mapとしての署名(例:map)は間違いなくunsafeです。 (実際、コードでflat_mapmapに置き換えると、先ほど説明したVecの状況になります。)

エラーメッセージの場合:selfは効果的です(構造体ラッパーを無視して)&'b mut (&'a mut Vec<i32>)。ここで、'b&mut self参照の存続期間であり、'astruct内の参照の存続期間です。内部の&mutを移動することは違法です。&mutのようなアフィン型を参照から移動することはできません(ただし、&Vec<i32>で動作します)。 reborrowは外部参照を通過するため、それを超えることはできません。つまり、&mut *self.seen reborrowは&'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>に変更する以外は基本的に同じです。この変更は、&'b mut &'a RefCell<Vec<i32>>の "reborrow"が&'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バージョンの方が効率的だと思います。

29
huon

借用チェッカーはネストされたクロージャー+可変借用で混乱しているようです。問題を提出する価値があるかもしれません。 編集:これがバグではない理由については 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);
}
7
Renato Zannon