Rust入門

【Rust】クロージャの型とトレイト(Fn / FnMut / FnOnce)

【Rust】クロージャの型とトレイト(Fn FnMut FnOnce)
naoki-hn

Rust におけるクロージャ(Closure)の型とトレイト(Fn / FnMut / FnOnce)について初心者にも分かりやすく解説します。

Rust のクロージャの型とトレイト

Rust におけるクロージャ(Closure)とは、「その場で定義できる無名の関数のようなもの」のことです。クロージャの使い方の基本については「クロージャの基本と使い方を分かりやすく解説」を参考にしてください。

この記事では、基本からは一段深く、クロージャの「型」と「トレイト」について踏み込んで紹介します。

以降の内容の理解には型やトレイトの基本的な知識が必要です。基本的な内容については、以下を参考にしてください。

クロージャの型

まずは、クロージャーの本質について確認してみましょう。結論から述べるとクロージャは、コンパイラが生成する固有の型です。

記事冒頭で「その場で定義できる無名の関数のようなもの」と説明しましたが、実際には「関数」ではありません。ここではその違いを具体的なコードで確認します。

関数の型とクロージャの型は異なる

Rust のクロージャは、以下のように使用することができます。

fn main() {
    // クロージャで使用する外部変数
    let base = 1;

    // クロージャーを定義
    let add_one_closure = |x| x + base;
    println!("クロージャーを呼び出す: {}", add_one_closure(5));
}
【実行結果】
クロージャーを呼び出す: 6

この例では、base という外部変数をキャプチャしています。これにより add_one_closure は引数 x に与えられた値に base の値を加算する機能を提供します。

では、このクロージャが関数として扱えるかを試してみましょう。以下の call_function は関数を引数として受け取り呼び出すだけの関数です。比較のために add_one という通常の関数を用意しています。

// 関数を引数として受け取り、呼び出す関数
fn call_function(f: fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

// 1を加算する通常の関数
fn add_one(x: i32) -> i32 {
    x + 1
}

fn main() {
    // クロージャで使用する外部変数
    let base = 1;

    // クロージャーを定義
    let add_one_closure = |x| x + base;
    println!("クロージャーを呼び出す: {}", add_one_closure(5));

    // 通常の関数を呼び出す
    println!(
        "通常の関数を call_function 経由で呼び出す: {}",
        call_function(add_one, 5)
    );

    // クロージャーを call_function に渡す。(これはエラーとなる)
    // println!(
    //     "クロージャーを call_function 経由で呼び出す: {}",
    //     call_function(add_one_closure, 5)
    // );
}

例の call_function は、「fn(i32) -> i32」という型を受け取ります。この型は、関数ポインタ型と呼ばれ、「i32 型を受け取り i32 型の結果を返す」ことを意味しています。

通常関数の add_one も、クロージャである add_one_closure も「i32 型を受け取り i32 型の結果を返す」という形式に一致しています。しかし、例でコメントアウトしているクロージャー add_one_closurecall_function に渡している部分はコンパイルエラーとなります。

理由はシンプルで、クロージャーの型は「fn(i32) -> i32」ではないためです。この結果から分かりますが、クロージャは、コンパイラが生成する固有の型となっています。

トレイト境界でクロージャと関数を同様に扱う

上記で、通常関数とクロージャは型が異なることが分かりました。しかし、関数にクロージャを渡して実行したいケースは非常に多くあります。関数とクロージャーを同じように扱う方法はないのでしょうか?もちろんあります。ポイントとなるのが Fn トレイトです。

次の例のようにトレイト境界を用いた call_function を使用すれば、通常関数もクロージャも両方受け取ることができるようになります。Fn トレイトについては詳細は後述しますので、一旦ここでは、このようにすることで対応できると理解していただければと思います

// クロージャも引数として受け取ることができる関数
fn call_function_closure<F>(f: F, x: i32) -> i32
where
    F: Fn(i32) -> i32,
{
    f(x)
}

// 1を加算する通常の関数
fn add_one(x: i32) -> i32 {
    x + 1
}

fn main() {
    // クロージャで使用する外部変数
    let base = 1;

    // クロージャーを定義
    let add_one_closure = |x| x + base;
    println!("クロージャーを呼び出す: {}", add_one_closure(5));

    // 通常の関数を呼び出す
    println!(
        "通常の関数を call_function 経由で呼び出す: {}",
        call_function_closure(add_one, 5)
    );

    // クロージャーを call_function_closure に渡す
    println!(
        "クロージャーを call_function_closure 経由で呼び出す: {}",
        call_function_closure(add_one_closure, 5)
    );
}

このようにすることで「通常の関数」と「クロージャ」をどちらも同じように扱うことができます。これは、通常の関数もクロージャも、Fn トレイトを通じて扱うことができるためです。

実は、クロージャは、常に同じトレイトを実装するわけではありません。外部変数のキャプチャ方法(不変参照、可変参照、所有権移動)によって実装されるトレイトが変わります。以降では、クロージャのトレイトについて説明していきます。

クロージャのトレイト(Fn / FnMut / FnOnce

Rust では、この違いを表現するために、以下の 3 つのトレイトが用意されています。

  • Fn:不変参照
  • FnMut:可変参照
  • FnOnce:所有権の移動

これらは、クロージャが外部の変数をどのように扱うかによって使い分けられます。以降では、クロージャのトレイトの違いについて詳しく見ていきます。

クロージャの基本記事「クロージャの基本と使い方を分かりやすく解説」で紹介しているクロージャのキャプチャ方法は、この各トレイトに相当しています。基本記事で使用したコードを再掲しながら確認してみましょう。

Fn トレイト(不変参照)

Fn トレイトは、外部の変数を変更せずに利用するクロージャに対して実装されるトレイトです。つまり、キャプチャした変数を「読み取るだけ」の 場合に Fn が実装されます。

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

    // クロージャの呼び出し
    println!("f(5) = {}", f(5));
    // xはまだ使用可能
    println!("x = {}", x);
}

例では、x をクロージャの中で参照しているだけで変更していません。そのため、このクロージャは Fn トレイトを実装します。Fn のクロージャは、状態を変更しないため何度でも安全に呼び出すことができます。

FnMut トレイト(可変参照)

FnMut トレイトは、キャプチャした変数を「変更する」クロージャに対して実装されるトレイトです。

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

    // クロージャを呼び出すとcountが変更される
    g();
    // countは変更されている
    println!("count after: {}", count);
}

例では、クロージャの中で count を変更しています。そのため、このクロージャは FnMut トレイトを実装します。

FnMut のクロージャは、キャプチャした変数を変更する可能性があります。そのため、呼び出し時には、クロージャ自身が変更される可能性があるため、クロージャを変数に束縛するには「let mut g」のように mut をつける必要があります。

FnOnce トレイト(所有権の移動)

FnOnce トレイトは、「キャプチャした値の所有権を消費する」クロージャに対して実装されるトレイトです。

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

    // 実行時に s は消費されて使用できなくなる
    h();
    // sはmoveで所有権が移動されているため以降は使用できない
    // println!("{s}"); // コンパイルエラー
}

例では、move を使用して s の所有権をクロージャに移動しています。このような場合、クロージャはキャプチャした値を消費する可能性があるため、1 回しか安全に呼び出すことができません。そのため、このようなクロージャは FnOnce トレイトを実装します。

move について】

例では、明示的に move を付与して実装していますが、Rust はコンパイラがクロージャの使われ方で判断するため、move の指定がなくても FnOnce となる場合があります。代表的な例は、以下のようにクロージャの中で drop() を使用するような場合です。

let s = String::from("Hello");
let h = || drop(s);

Fn / FnMut / FnOnce の関係性

上記で紹介したトレイトは、次のような関係になっています。

Fn ⊂ FnMut ⊂ FnOnce
Fn / FnMut / FnOnce の関係性
Fn / FnMut / FnOnce の関係性

上記図は「FnFnMut のサブトレイト」「FnMutFnOnce のサブトレイト」という意味を表しています。つまり、Fn を実装するクロージャは FnMut としても扱うことができ、FnMut を実装するクロージャは FnOnce としてもあつかえるという関係になっています。

記事の前半で Fn をトレイト境界に持つ関数の例を紹介しましたが、この call_function は、FnMut のクロージャを受け取ることができません。一方で、FnMut をトレイト境界に持つ関数は、Fn トレイトのクロージャを受け取ることが可能となります。

ここまでで紹介してきたように、Rust では、クロージャの振る舞いに応じて Fn / FnMut / FnOnce のトレイトが自動的に決定されます。このような仕組みとなっていることをしっかり理解していただければと思います。

まとめ

Rust におけるクロージャ(Closure)の型とトレイト(Fn / FnMut / FnOnceについて解説しました。

クロージャは、通常の関数とは異なり、コンパイラが生成する固有の型です。一方で、トレイト境界を使うことで通常の関数と同じように扱うことができます。

また、クロージャは、キャプチャする外部変数の扱い方に応じて、Fn / FnMut / FnOnce のいずれかのトレイトを実装します。これらの違いを理解しておくことで、クロージャを引数として受け取る関数や所有権が関わるコードをより正確に理解して実装ができるようになります。

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

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

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

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