Rust入門

【Rust入門】クロージャの基本と使い方を分かりやすく解説

【Rust入門】クロージャの基本と使い方を分かりやすく解説
naoki-hn

Rust におけるクロージャ(Closure)の基本を初心者にも分かりやすく解説します。

クロージャ(Closure)とは

Rust におけるクロージャ(Closure)とは、「その場で定義できる無名の関数のようなもの」のことです。

通常の fn で定義する関数との大きな違いは、外部のスコープにある変数を利用できることです。この「外側の変数を取り込んで使う」ことを「キャプチャ(capture)」と呼びます。

共通的な変数をクロージャの中に取り込んでおくことで、他の可変な値と組み合わせながら、柔軟で再利用可能な処理を定義することができます。

この記事では、Rust におけるクロージャの基本的な使い方や考え方について紹介します。

【無名関数とクロージャ】

厳密には、クロージャという言葉は「外部の変数をキャプチャした関数」のことを言います。しかし、Rust では、外部の変数をキャプチャしていない場合でも、すべてクロージャと呼びます。

Python などの他の言語では、「無名関数」と「クロージャ」を分けて説明されることがあります。一方で、Rust コミュニティでは「無名関数」という用語はほとんど使われず、一貫してクロージャという言葉で説明がされる点を覚えておくとよいでしょう。

クロージャの基本

クロージャの基本的な使い方

基本的なクロージャの使い方を紹介します。

|引数1, 引数2, ...| 式

クロージャの構文は非常にシンプルです。上記のように || 内に引数を列挙し、クロージャの返却値となる式をその後に記載します。引数なしのクロージャー (|| 式) でも問題ありません。

また、複数の文を書く場合は、以下のように {} のブロックを使用します。

|引数1, 引数2, ...| {
    文;
    ...;
    式
}

最後の式は、クロージャの戻り値になります。最後に ; (セミコロン) をつけると戻り値は () となります。

それでは、簡単な具体例で確認してみましょう。

単一式の例

fn main() {
    // ===== 単一の式のクロージャ例
    let add = |a, b| a + b;
    println!("add(2, 3) = {}", add(2, 3));
}
【実行結果】
add(2, 3) = 5

この例では、変数 ab を受け取り a + b という単一の式を計算するクロージャを定義して add という変数名に束縛しています。a + b がクロージャの戻り値になります。

クロージャを使用する時には「add(2, 3)」のように使用します。もちろん、add(4, 5)、 …のように任意の引数での再利用が可能です。

単一の式の場合であれば、{} を使用しなくても上記のように簡潔に書くことができます。

引数なしの例

fn main() {
    // ===== 引数なしのクロージャ例
    let greet = || "Hello, Rust!";
    println!("greet() = {}", greet());
}
【実行結果】
greet() = Hello, Rust!

クロージャは、引数がなくても使用できます。引数がない場合、|| のように何も書かずに定義します。この例では、"Hello, Rust!" という文字列リテラルがそのまま戻り値になります。

ブロック形式の例

fn main() {
    // ===== ブロック形式のクロージャ例
    let multiply = |a, b| {
        println!("calculating {a} * {b} ...");
        // 最後の式がクロージャの戻り値になる
        a * b
    };
    println!("multiply(3, 5) = {}", multiply(3, 5));
}
【実行結果】
calculating 3 * 5 ...
multiply(3, 5) = 15

複数の文を実行したい場合には、{} ブロックを使用します。例では、計算前に println! でメッセージを表示し、その後に a * b を評価しています。

Rust では、ブロックの最後の式が戻り値になるというルールがあります。そのため、a * b; (セミコロン) をつけないことが重要です。; を付けた場合の返却値は () となります。

外側の変数をキャプチャする例

ここまでの例は、関数とほぼ同じような使い方でした。しかし、クロージャの本質は「外側の変数を利用できること」です。以下の例で、外側の変数をキャプチャする例を見てみましょう。

fn main() {
    // ===== 外側の変数をキャプチャするクロージャ例
    let base = 10;

    let add_to_base = |x| base + x;
    println!("add_to_base(5) = {}", add_to_base(5));
}
【実行結果】
add_to_base(5) = 15

この例では、base という変数をクロージャの外側で定義しています。add_to_base は、引数 x だけを受け取っていますが、計算で base も利用しています。このように add_to_base を用意しておくと、様々な x に対して base を足した結果を再利用して計算できます。

あたかも add_to_base が base 変数を捕捉した状態で使いまわせるため「キャプチャする」という表現が使われています。

このように、外側のスコープにある変数を取り込んで使用することができるのがクロージャの本質です。ここまでの例を振り返るとクロージャーは次のような特徴があることが分かります。

  • 関数のように振る舞う
  • fn を使わなくても、その場で定義することができる
  • 外側の変数を利用することができる(キャプチャできる)

つまり、単なる無名関数というだけではなく、定義された環境設定ごと関数を扱える仕組みであることがクロージャの本当の価値です。

クロージャの型注釈と型推論

引数の型注釈

クロージャーは型推論がされるため、基本的には型注釈を入れる必要はありません。以下のように型注釈を入れたクロージャも使うことができます。

fn main() {
    // ===== 単一の式のクロージャ例 (型注釈あり)
    let subtract = |a: i32, b: i32| a - b;
    println!("subtract(5, 2) = {}", subtract(5, 2));
}
【実行結果】
subtract(5, 2) = 3

例のように a: i32, b: i32 といった形で型注釈を書くことができます。なお、戻り値については、let subtract = |a: i32, b: i32| -> i32 { a - b }; のように書くことも可能ですが、引数の型が決まれば、戻り値の型は式から自動推論できるため、通常は記載しません。

クロージャの型推論

クロージャは型推論をすると記載しましたが、どこで型が確定するかというと「最初にクロージャが使用されたときの型」で確定となります。そのため、先ほどの例で一度 add(2, 3) と使用した後に以下のように、floating-point で使用しようとするとコンパイルエラーとなります。

println!("add(1.0, 2.0) = {}", add(1.0, 2.0));
【エラー例】
error[E0308]: arguments to this function are incorrect
  --> rust-basic\closure-basic\examples\closure_basic.rs:28:36
   |
28 |     println!("add(1.0, 2.0) = {}", add(1.0, 2.0));
   |                                    ^^^ ---  --- expected integer, found floating-point number
   |                                        |
   |                                        expected integer, found floating-point number

クロージャのキャプチャの方法

ここでは、クロージャのキャプチャについて少し詳しく見てみましょう。キャプチャとは、クロージャが外側のスコープにある変数を取り込んで利用することでした。

Rust のクロージャでは、外側の変数をどのように扱うかに応じて、キャプチャ方法をコンパイラが自動で選択します。主なキャプチャの方法には以下の 3 種類です。

  1. 不変参照でのキャプチャ
  2. 可変参照でのキャプチャ
  3. move による所有権の移動でのキャプチャ

不変参照でのキャプチャ

外側の変数を「読み取るだけ」であればクロージャは、変数を不変参照でキャプチャします。この場合、外側の変数は所有権を失わないため、クロージャ呼び出し後も引き続き利用できます。

fn main() {
    // ===== 不変参照でのキャプチャ例
    let x = 10;
    // x は不変参照でキャプチャされる
    let f = |y| x + y;

    // クロージャの呼び出し
    println!("f(5) = {}", f(5));
    // xはまだ使用可能
    println!("x = {}", x);
}
【実行結果】
f(5) = 15
x = 10

例のクロージャでは、x を不変参照しているだけで変更していません。そのため、クロージャを呼び出した後でも x は使用できます。

可変参照でのキャプチャ

クロージャの中で外側の変数を変更する場合、クロージャはその変数を可変参照でキャプチャします。以下は、count 変数をクロージャ内部でカウントアップしているような例です。

fn main() {
    // ===== 可変参照でのキャプチャ例
    let mut count = 0;
    println!("count before: {}", count);
    // countは可変参照でキャプチャされる
    let mut g = || count += 1;

    // クロージャを呼び出すとcountが変更される
    g();
    // countは変更されている
    println!("count after: {}", count);
}
【実行結果】
count before: 0
count after: 1

可変参照でキャプチャする場合は、外側の変数は let mut により、変更可能(ミュータブル)な変数として定義する必要があります。また、クロージャを変数(上記例では g)に束縛する場合は、g についても let mut で変更可能な形で定義する必要があります。

move による所有権の移動でのキャプチャ

これまでの例では、外側の変数は参照としてキャプチャされていました。以下のようにクロージャの定義の前に move をつけることで、外側の変数は所有権ごとクロージャに移動します。

fn main() {
    // ===== move による所有権の移動でのキャプチャ例
    let s = String::from("Hello");
    // s は move で所有権がクロージャに移動される
    let h = move || println!("{s} World!");

    // 実行時に s は消費されて使用できなくなる
    h();
    // sはmoveで所有権が移動されているため以降は使用できない
    // println!("{s}"); // コンパイルエラー
}
【実行結果】
Hello World!
【move 後のコメントアウト部分を外した場合のコンパイルエラー 一部抜粋】
error[E0382]: borrow of moved value: `s`
  --> rust-basic\closure-basic\examples\closure_capture.rs:31:16
   |
24 |     let s = String::from("Hello");
   |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
25 |     // s は move で所有権がクロージャに移動される
26 |     let h = move || println!("{s} World!");
   |             -------            - variable moved due to use in closure
   |             |
   |             value moved into closure here
...
31 |     println!("{s}"); // コンパイルエラー
   |                ^ value borrowed here after move

例のように、move を付けた時点で変数 s の所有権はクロージャに移動します。そのため、以降外側のスコープでは s は使用できません。使用しようとするとコンパイルエラーとなります。(例でコメントアウトしている部分を外して試してみてください。)

move は、外側の値の所有権をクロージャ側に移して、クロージャがその値を保持できるようにしたい場合に使います。(例:スレッドに渡す、関数の外へ変数を持ち出す など)

所有権の基本については以下を参考にしてください。

所有権と借用の基本を分かりやすく解説

まとめ

Rust におけるクロージャ(Closure)の基本を解説しました。

クロージャは 「 |引数1, 引数2, …| 式」の形式で、その場で定義できる無名の関数のような仕組みです。ブロック形式の場合は、最後の式が戻り値になります。また、型注釈は必要に応じて記載できますが、多くの場合は型推論に任せることができます。

さらに、クロージャの本質である「キャプチャ」について、不変参照・可変参照・move による所有権移動の 3 パターンを紹介しました。特に move を使うと、外側の値の所有権がクロージャに移動し、以降は外側のスコープでは利用できなくなる点が重要です。

クロージャは、その場で処理を定義できる非常に便利な機能で、Rust ではさまざまな場面で利用されます。まずは、基本的な書き方とキャプチャの考え方をしっかり押さえておきましょう。

あわせて読みたい
Rust プログラミング入門
Rust プログラミング入門
ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。
私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は大手電機メーカーでシステム開発に携わっていました。

これまでの業務を通じてさまざまなプログラミング言語や技術に触れてきましたが、その中でもRustの設計思想に惹かれ、この言語についてもっと深く学びたい、そしてその魅力を発信していきたいと思い、このサイトを立ち上げました。

自身の学びを整理しつつ、同じようにRustに興味を持つ方のお役に立てるような情報を発信していければと思っています。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました