Rustの基礎 - クロージャ

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動

概要

クロージャとは、環境の変数をキャプチャできる無名関数のことを指す。

通常のメソッドとは異なり、定義されたスコープの変数を参照することができる。
Rustのクロージャは、型推論が可能で、引数と戻り値の型を明示的に指定する必要がない場合が多い。


クロージャの基本

クロージャを定義する基本的な構文を以下に示す。

 let closure_name = |引数| ;


以下の例では、2つの引数を受け取り、その合計を返すクロージャを定義している。

 fn main() {
    let add = |a, b| a + b;
    println!("{}", add(2, 3));
 }
 
 // 出力
 5


複数行のクロージャを定義する場合は、波括弧{}を使用する。

 fn main() {
    let add = |a, b| {
       let result = a + b;
       result
    };
 
    println!("{}", add(2, 3));
 }
 
 // 出力
 5


型を明示的に指定することもできる。

 fn main() {
    let add = |a: i32, b: i32| -> i32 {
       a + b
    };
 
    println!("{}", add(2, 3));
 }
 
 // 出力
 5



環境のキャプチャ

クロージャは、定義されたスコープの変数をキャプチャすることができる。
これが通常のメソッドとの主な違いである。

 fn main() {
    let x = 10;
    let add_x = |y| x + y;
 
    println!("{}", add_x(5));
 }
 
 // 出力
 15


クロージャは、変数を3つの方法でキャプチャできる。

  • 不変参照 (&T) によるキャプチャ
  • 可変参照 (&mut T) によるキャプチャ
  • 所有権の移動 (T) によるキャプチャ


デフォルトでは、クロージャは必要最小限の方法で変数をキャプチャする。

 fn main() {
    let mut count = 0;
 
    // 可変参照でキャプチャ
    let mut increment = || {
       count += 1;
       println!("count: {}", count);
    };
 
    increment();
    increment();
 }
 
 // 出力
 count: 1
 count: 2



メソッドがクロージャを返す

以下の例では、outer_funcメソッドがクロージャを返している。
このクロージャは、outer_funcメソッドの引数aとbをキャプチャしている。

 fn outer_func(a: i32, b: i32) -> impl Fn() -> i32 {
    move || a + b
 }
 
 fn main() {
    let result = outer_func(2, 3);
    println!("{}", result());
 }
 
 // 出力
 5


上記の例では、move キーワードを使用している。
これは、クロージャが変数の所有権を奪うことを明示的に指定している。

moveを使用しないと、クロージャは参照でキャプチャしようとするが、
メソッドから返される場合は、ライフタイムの問題が発生するため、move キーワードが必要となる。

戻り値の型impl Fn() -> i32は、引数を取らずi32を返すクロージャを意味する。


クロージャの実行

クロージャを実行するには、変数に代入した後、関数と同様に丸括弧 () を使用する。

 fn outer_func(a: i32, b: i32) -> impl Fn() -> i32 {
    move || a + b
 }
 
 fn main() {
    let func = outer_func(2, 3);
    println!("{}", func());
 }
 
 // 出力
 5


以下の例では、長方形の面積を計算している。
引数に横(width)を与えるarea_calc メソッドを定義して、縦(height)を引数として面積を計算するクロージャを返している。

 fn area_calc(width: i32) -> impl Fn(i32) -> i32 {
    move |height| width * height
 }
 
 fn main() {
    // widthが25と50の場合を計算
    let ac1 = area_calc(25);
    let ac2 = area_calc(50);
 
    // heightを10として面積を求める
    println!("{}", ac1(10));
    println!("{}", ac2(10));
 }
 
 // 出力
 250
 500



クロージャのトレイト

Rustのクロージャは、以下に示す3つのトレイトのいずれかを実装する。

  • FnOnce
    1度だけ呼び出せるクロージャ。
    キャプチャした変数の所有権を消費する可能性がある。
  • FnMut
    複数回呼び出せるクロージャ。
    キャプチャした変数を可変的に借用する。
  • Fn
    複数回呼び出せるクロージャ。
    キャプチャした変数を不変的に借用する。


全てのクロージャは少なくとも FnOnce を実装する。
キャプチャした変数を消費しないクロージャは FnMut も実装する。
キャプチャした変数を変更しないクロージャは Fn も実装する。

以下の例では、それぞれのトレイトを使用したクロージャを示している。

 fn main() {
    // Fn : 不変借用
    let x = 5;
    let print_x = || println!("x = {}", x);
    print_x();
    print_x();  // 複数回呼び出せる
 
    // FnMut : 可変借用
    let mut count = 0;
    let mut increment = || {
       count += 1;
       count
    };
    println!("{}", increment());
    println!("{}", increment());  // 複数回呼び出せる
 
    // FnOnce: 所有権の移動
    let s = String::from("hello");
    let consume = || {
       drop(s);  // sの所有権を消費
    };
    consume();
    // consume();  // エラー: 2回目は呼び出せない
 }
 
 // 出力
 x = 5
 x = 5
 1
 2



メソッドの引数としてクロージャを受け取る

メソッドの引数としてクロージャを受け取ることができる。
以下の例では、ジェネリクスとトレイト境界を使用してクロージャを受け取っている。

 fn apply<F>(f: F, x: i32, y: i32) -> i32
 where
    F: Fn(i32, i32) -> i32,
 {
    f(x, y)
 }
 
 fn main() {
    let add = |a, b| a + b;
    let multiply = |a, b| a * b;
 
    println!("{}", apply(add, 3, 4));
    println!("{}", apply(multiply, 3, 4));
 }
 
 // 出力
 7
 12


impl Trait 構文を使用して、簡潔に記述することもできる。

 fn apply(f: impl Fn(i32, i32) -> i32, x: i32, y: i32) -> i32 {
    f(x, y)
 }
 
 fn main() {
    let add = |a, b| a + b;
    println!("{}", apply(add, 3, 4));
 }
 
 // 出力
 7



イテレータとクロージャ

Rustでは、イテレータのメソッドでクロージャを頻繁に使用する。
以下に示すものは、代表的な使用例である。

mapメソッドを使用して、各要素を変換する例。

 fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    let doubled: Vec<i32> = numbers.iter().map(|&x| x * 2).collect();
 
    println!("{:?}", doubled);
 }
 
 // 出力
 [2, 4, 6, 8, 10]


以下の例では、filter メソッドを使用して、条件に合う要素のみを抽出している。

 fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6];
    let evens: Vec<i32> = numbers.iter().filter(|&x| x % 2 == 0).copied().collect();
 
    println!("{:?}", evens);
 }
 
 // 出力
 [2, 4, 6]


以下の例では、fold メソッドを使用して、累積計算を行っている。

 fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    let sum = numbers.iter().fold(0, |acc, &x| acc + x);
 
    println!("{}", sum);
 }
 
 // 出力
 15



クロージャと所有権

クロージャは、キャプチャした変数の所有権を扱う方法を柔軟に制御できる。

デフォルトでは、クロージャは必要最小限の方法で変数をキャプチャする。

 fn main() {
    let s = String::from("hello");
 
    // 不変参照でキャプチャ
    let print = || println!("{}", s);
    print();
 
    // sはまだ使用できる
    println!("{}", s);
 }


move キーワードを使用すると、所有権を強制的に移動できる。
これは、クロージャが元のスコープよりも長く生存する場合に必要となる。

 use std::thread;
 
 fn main() {
    let s = String::from("hello");
 
    // 別スレッドで実行するため、moveが必要
    let handle = thread::spawn(move || {
       println!("{}", s);
    });
 
    // println!("{}", s);  // エラー: sの所有権は移動済み
 
    handle.join().unwrap();
 }