📖

Rustのクロージャの型について

2023/02/06に公開

Rustのクロージャの型は個人的に混乱しがちだったので、まとめて見ました。
雑な記事で恐縮ですが、質問・コメントいただけるととても喜びます。

Rustのクロージャの使い分け

Rustは以下の3つのクロージャが存在する。使い分けはキャプチャした値の所有権をどのようにするかで変わる。(キャプチャの挙動についてはここでは言及しない。)

  1. Fn
  2. FnMut
  3. FnOnce

FnはFnMutの、FnMutはFnOnceのサブトレイトである。(詳しくは実装を参照)
つまり、それぞれのトレイトの自由度は

FnOnce > FnMut > Fn

の順になる。

ここで、それぞれの挙動(キャプチャした値の使い方)を見ていく。

1. Fn

Fnはキャプチャした値を参照として受け取る型である。
キャプチャした値を変更も消費もしないので、何度でも同じクロージャを呼ぶことができるし、キャプチャした値を後で参照することができる(しかもキャプチャした値は不変)。

fn main() {
    let x = 1;
    let f = || x + 2;
    test_closure(f);
    test_closure(f);
    test_closure(f);
    assert_eq!(1, x);
}
fn test_closure<F: Fn() -> i32>(f: F) {
    assert_eq!(3, f());
}

もしキャプチャした値を変更 or 消費しようとするとコンパイルエラーになる。

  • キャプチャした値を変更しようとした場合
fn main() {
    let x = 1;
    let f = || x += 2;  // コンパイルエラー
    test_closure(f);
}
fn test_closure<F: Fn()>(f: F) {
    f();
}
  • キャプチャした値をmoveしようとした場合(CopyしないようにサンプルはStringにしている。)
fn main() {
    let x = String::from("a");
    let f = move || x;    // コンパイルエラー
    test_closure(f);
}
fn test_closure<F: Fn() -> String>(f: F) {
    assert_eq!("a", f());
}

※ クロージャのmoveセマンティクスはキャプチャした値の所有権をクロージャ内に移動させる。

2. FnMut

FnMutはキャプチャした値を可変参照として受け取る型である。
キャプチャしたデータを消費はしないが、データを編集することができる。

fn main() {
    let mut x = 1;
    let f = || x += 2;
    test_closure(f);
    assert_eq!(3, x);
}
fn test_closure<F: FnMut()>(mut f: F) {
    f();
}

当然だが、キャプチャする値はmutな参照なので、同じスコープ内で重複してキャプチャすることはできない。

fn main() {
    let mut x = 1;
    let f = || x += 2;
    let g = || x += 3; // コンパイルエラー
    test_closure(f);
    test_closure(g);
}
fn test_closure<F: FnMut()>(mut f: F) {
    f();
}

上記のケースでコンパイルを通したいなら、NLLを意識した実装にする必要がある。(NLLについての説明は割愛)

fn main() {
    let mut x = 1;
    let f = || x += 2;
    test_closure(f);

    let g = || x += 3;
    test_closure(g);

    assert_eq!(6, x);
}
fn test_closure<F: FnMut()>(mut f: F) {
    f();
}

これも当然だが、クロージャの型にFnを指定するとコンパイルエラーになる。

fn main() {
    let mut x = 1;
    let f = || x += 2;  // コンパイルエラー
    test_closure(f);
    assert_eq!(6, x);
}
fn test_closure<F: Fn()>(mut f: F) {
    f();
}

キャプチャした値を変更しないようなクロージャ(要するにFn)だとコンパイルエラーは出ない。

fn main() {
    let mut x = 1;
    let f = || println!("{x:?}");
    test_closure(f);
    assert_eq!(6, x);
}
fn test_closure<F: FnMut()>(mut f: F) {
    f();
}

キャプチャした値をmoveする場合はエラーになる。

fn main() {
    let mut s = String::from("x");
    let f = move || s;  // コンパイルエラー
    test_closure(f);
}
fn test_closure<F: FnMut() -> String>(mut f: F) {
    f();
}

FnMutの実験をしていて気づいたのだが、以下の場合はコンパイルが通らない。
クロージャ内でキャプチャした値を破壊的変更する場合、クロージャがCopyでなくなるためにクロージャのcopyが出来ずmoveしてしまうのが原因らしい。

fn main() {
    let mut x = 1;
    let f = || x += 2;
    test_closure(f);
    test_closure(f); // コンパイルエラー
    assert_eq!(5, x);
}
fn test_closure<F: FnMut()>(mut f: F) {
    f();
}

これをコンパイル通すには、クロージャを&mutな参照にする必要がある。
コンパイラの指摘通りに愚直に修正すると以下の通りとなる。

fn main() {
    let mut x = 1;
    let mut f = || x += 2;
    test_closure(&mut f);
    test_closure(&mut f);
    assert_eq!(5, x);
}
fn test_closure<F: FnMut()>(f: &mut F) {
    f();
}

ちなみに、キャプチャした値に対して破壊的変更を行わない場合はコンパイルエラーにはならない。
(この場合はクロージャがCopyとして扱われる)

fn main() {
    let x = 1;
    let f = || println!("{x:?}");
    test_closure(f);
    test_closure(f);
    assert_eq!(5, x);
}
fn test_closure<F: FnMut()>(mut f: F) {
    f();
}

FnOnceはキャプチャしたデータをmoveする。
キャプチャしたデータの所有権がクロージャにmoveするので、データに変更を加えたり消費(drop)してもエラーが発生しない。

  • 変更する場合
fn main() {
    let mut s = String::from("x");
    let f = move || s = "y".to_string();
    test_closure(f);
}

fn test_closure<F: FnOnce()>(f: F) {
    f();
}

  • moveする場合
fn main() {
    let s = String::from("x");
    let f = move || s;
    test_closure(f);
}

fn test_closure<F: FnOnce() -> String>(f: F) {
    f();
}

  • 消費する場合
fn main() {
    let s = String::from("x");
    let f = || drop(s);
    test_closure(f);
}

fn test_closure<F: FnOnce()>(f: F) {
    f();
}

当然ながら、値をmoveするのでキャプチャした値は後から参照できない。

fn main() {
    let s = String::from("x");
    let f = move || s;
    test_closure(f);
    assert_eq!("x".to_string(), s);  // コンパイルエラー
}

fn test_closure<F: FnOnce() -> String>(f: F) {
    f();
}

クロージャを2回以上呼ぶこともできない。(以下の場合はsがmoveされているのでfはCopyが動かない)

fn main() {
    let s = String::from("x");
    let f = move || s;
    test_closure(f);
    test_closure(f);  // コンパイルエラー
}

fn test_closure<F: FnOnce() -> String>(f: F) {
    f();
}

FnOnceを使うケースは、以下の3パターンがあると考えられる。

1.並行処理でクロージャを渡す時
2. クロージャの使用を一回だけにしたい時
3. キャプチャしたデータを消費するような処理を行いたい時

おそらく、一番多いケースは1.のパターンだと思われる。
以下のドキュメントにもある通り、Rustの並行処理はFnOnceなクロージャをspawnすることで実行される。(これはtokioでも同様)
https://doc.rust-lang.org/std/thread/fn.spawn.html

おわりに

いかがでしたでしょうか?
この記事がRustの勉強の一助になれば幸いです。

繰り返しになりますが、もし質問等あればコメントいただけると私も勉強になるのでとても嬉しいです!お待ちしております。

Discussion