はじめに
クロージャ使ってたら色々コンパイラに怒られることがありまして、備忘録程度にまとめておきます。
おそらく中級者以上の方には当たり前すぎる話かと思います。
クロージャの作成
#[derive(Debug)]
struct Foo {}
fn test(x: Foo) {
println!("x: {:?}", x);
}
fn main() {
let bar = Foo {};
let clousure = || test(bar);
clousure();
}
クロージャを作成して実行する簡単なプログラムです。
これは全く問題なく動作します。
次にこのプログラムのクロージャ作成部分を関数化してみます。
#[derive(Debug)]
struct Foo {}
fn test(x: Foo) {
println!("x: {:?}", x);
}
fn make_closure() -> Box<Fn()> {
let bar = Foo {};
Box::new(move || test(bar))
}
fn main() {
let clousure = make_closure();
clousure();
}
クロージャを返す関数であるためBoxingされた形にはなりましたが1、前述のプログラムとさほど変わりはありません。
しかし!これはコンパイルエラーが出ます。
error[E0507]: cannot move out of captured outer variable in an `Fn` closure
--> src\test2.rs:10:25
|
10 | Box::new(move || test(bar))
| ^ cannot move out of captured outer variable in an `Fn` closure
どういうことか?
struct Foo
はCopyトレイトを持っていません。つまり参照でなければ必ずムーブされるということです。
このプログラムでは「クロージャが作られたとき」と「クロージャ内でtest関数を呼び出すとき」の2回、所有権の移動が起こっています。
つまり、クロージャ内でtest関数を呼び出すと、クロージャはbar
の所有権を手放してしまうのです。と、いうことはそれ以上クロージャを呼び出すこと、つまりtest(bar);
を実行することはできないですよね? しかしFnタイプのクロージャは何度も呼び出すことが可能です。矛盾しているわけです。なので、Rustのコンパイラさんは怒っていらっしゃるわけです。
「問題ない」とした最初に挙げたプログラムを見てみましょう。test1.rs
のclousure();
の下にもう一度clousure();
を書き足してみてください。
error[E0382]: use of moved value: `clousure`
--> src\test.rs:12:3
|
11 | clousure();
| -------- value moved here
12 | clousure();
| ^^^^^^^^ value used here after move
|
= note: move occurs because `clousure` has type `[closure@src\main.rs:10:18: 10:30 bar:Foo]`, which does not implement the `Copy` trait
エラーが出てしまいました。実は2回実行していないから大丈夫だったのです23。
また、i32のようなCopyトレイトを持つオブジェクトならCopyが行われるため所有権の移動は起こりません。従って問題なく動作します。
fn test(x: i32) {
println!("x: {:?}", x);
}
fn make_closure() -> Box<Fn()> {
let bar = 10;
Box::new(move || test(bar))
}
fn main() {
let clousure = make_closure();
clousure();
}
明示的なClone
std::cell::CellのようなCopyトレイトは持っていないがCloneを持つオブジェクトの場合は、test(bar.clone());
と明示的にcloneすることで、クロージャがbarの所有権を放棄することなく(ほとんどの場合では)想定された動作を実行できます。
終わりに
クロージャは通常の関数と違い、「環境」を持つため通常の関数よりも所有権の問題が発生しやすく、かつ私のような何も考えていない人間ですと何が起こっているのかも分かりづらいです。
同じような部分で躓いている方の助けになれば幸いです。
今回挙げたような例なら、test関数にリファレンス渡したらよくね?というのはナイショにしておいてください。
-
Boxではなく
std::rc::Rc
などでも構いません。Boxingが必要な理由などについてはプログラミング言語Rustの4.23 クロージャ#クロージャを返すを参照してください。 ↩ -
コンパイラのエラーを見るに、このクロージャはFnではなくFnOnceのようです。従ってbarの所有権問題というよりもselfの所有権の問題です。この点、
test2.rs
の例とは異なりますのでご注意ください。 ↩ -
test2.rs
においてもFnOnceならいけそうだ!と感じる方はいるかもしれません。ところが、どうもFnOnceはまだ機能的に微妙なようで(1.16.0現在)、これもダメです(参照: Rust で Box + FnOnce を使う)。またnightlyならば、FnBoxというものも使えます。これらを利用すれば期待通りの動作を得られるはずです。ただしFnBoxに関しては、FnOnceがまともになったら置き換わる予定です(see issue #28796)。 ↩