rustのクロージャを理解する

rust

クロージャをひとことで

周りの環境の変数をキャプチャ出来る関数

周り環境の変数とは?

周りの環境の変数と書きましたが、その場所から普通にアクセスできる変数のことです。以下の例のように同じスコープ内にある変数bをクロージャを使ってキャプチャできます。

fn main(){
    let b = 10;
    let a = || println!("{}", b);
    a();
}
$ cargo run
10

普通の関数では周りの環境をキャプチャできない

普通の関数でこれをやろうとするとコンパイルエラーになります。

fn main() {
    let b = 10;
    fn a() {
        println!("{}", b); // <- not allowed
    }
    a();
}

キャプチャとは?

キャプチャとは、クロージャ内に変数を保持することを指しています。変数の保持の仕方は以下の3種類あります。

  • 不変参照を持つ
  • 可変参照を持つ
  • 所有権を持つ

これは普通の変数と同じです。変数の参照、所有権については、以下をご参考ください。

不変参照をキャプチャするクロージャ

以下のコードは変数vの不変参照をクロージャ内にキャプチャしています。不変参照をキャプチャしているだけなので、元々の変数vもクロージャ内の変数vもアクセス可能です。

fn main() {
    let v = vec![1, 2, 3];
    let a = || {
        println!("{:?}", v);
    };
    a();
    println!("{:?}", v);
    a();
}
$ cargo run
[1, 2, 3]
[1, 2, 3]
[1, 2, 3]

可変参照をキャプチャするクロージャ

以下のコードは変数vの可変参照をクロージャ内にキャプチャしています。可変参照を持つときは、同時に他の参照を作ることはできません。したがって、以下の2回目のaの呼び出しは許されません。これがあると、キャプチャしたvの可変参照の有効範囲がv.push(5)を跨いでしまいます。間にあるv.push(5)は可変参照を取る命令なので、可変参照が同時に2つになってしまいます。

fn main() {
    let mut v = vec![1, 2, 3];
    let mut a = || {
        v.push(4);
        println!("{:?}", v);
    };
    a();
    v.push(5);
    //a(); // <- not allowed
}
$ cargo run
[1, 2, 3, 4]

所有権ごと変数をキャプチャするクロージャ

以下のコードはmoveキーワードを使用することにより、変数vの所有権ごとクロージャ内にキャプチャします。変数vの所有権はクロージャa内に移動したので、もはや元の変数vを使用してVecにアクセスすることはできません。

fn main() {
    let mut v = vec![1, 2, 3];
    let mut a = move || {
        v.push(4);
        println!("{:?}", v);
    };
    a();
    //v.push(5); // <- not allowed
    //a(); // <- not allowed
}
$ cargo run
[1, 2, 3, 4]

クロージャの動作概要

クロージャを定義した時、rustが裏でどんな動作をするのか、以下のサイトに少し書いてありました。

クロージャを受け取る関数 - Rust By Example 日本語版
Rust by Example (RBE) is a collection of runnable examples that illustrate various Rust concepts and standard libraries.

大雑把に書くと以下のような感じだと思います。

  1. キャプチャした変数を要素として持つことができる無名の構造体を作る
  2. その構造体へクロージャ内の関数を実装する
  3. キャプチャした変数を要素として、インスタンスを作成する

以下の様なコードについて

let a = || {
    println!("{:?}", v);
};

構造体にしてみると以下の様なイメージでしょうか。。

struct Hoge {
    v: Vec<i32>,
}
impl Hoge {
    fn fuga(&self) {
        println!("{:?}", self.v)
    }
}

最後に

今回はクロージャの種類、それぞれの性質について書きました。クロージャは一種のインスタンスのように扱われるので、関数への引数にしたり、戻り値にしたりすることができます。それについては、別の記事で書こうと思います。

ご覧いただきありがとうございました。

コメント

タイトルとURLをコピーしました