🔐

Rustのクロージャの仕組み:Fn、FnMut、FnOnce

2025/03/09に公開

表紙

Rust プログラミング言語において、クロージャ(closures)は非常に強力で柔軟な機能であり、無名関数を定義し、その環境内の変数をキャプチャすることができます。Rust のクロージャシステムは、Fn、FnMut、FnOnce という 3 つのコアトレイトによって定義されます。これらのトレイトは、クロージャがキャプチャした変数とどのように相互作用するか、何回呼び出せるか、環境を変更できるかを決定します。これらの理解は、Rust のクロージャメカニズムを習得し、効率的で安全なコードを書くために不可欠です。

本記事では、Fn、FnMut、FnOnce の 3 つのトレイトについて詳しく説明し、それぞれの定義、用途、使用方法、適用シナリオを紹介します。また、コード例やベストプラクティスを提供し、これらの知識を総合的に学べるようにします。

Fn、FnMut、FnOnce とは?

Fn、FnMut、FnOnce は、Rust の標準ライブラリで定義されたトレイトであり、クロージャ(または任意の呼び出し可能なオブジェクト)の振る舞いを記述します。これらの主な違いは、クロージャがキャプチャした変数をどのように扱うか、および呼び出し時の所有権のルールにあります。

  • FnOnce:クロージャは 一度だけ 呼び出すことができ、呼び出し後に消費(consume)され、再利用できません。
  • FnMut:クロージャは 複数回 呼び出せ、キャプチャした変数を 変更 することができます。
  • Fn:クロージャは 複数回 呼び出せますが、キャプチャした変数を 変更せずに読み取る だけです。

これらのトレイトは、以下のような 継承関係 を持ちます。

  • FnFnMut を継承し、FnMutFnOnce を継承します。
  • つまり、あるクロージャが Fn を実装しているならば、自動的に FnMut と FnOnce も実装されます
  • 同様に、FnMut を実装しているならば、自動的に FnOnce も実装されます

各トレイトの定義

FnOnce

FnOnce トレイトは、次のような call_once メソッドを定義します。

pub trait FnOnce<Args> {
    type Output;
    fn call_once(self, args: Args) -> Self::Output;
}
  • 特徴
    • call_onceself を受け取る(&self&mut self ではない)。
    • これは 所有権がクロージャに移動する ことを意味し、クロージャは 1 回だけ 呼び出せる。
  • 用途
    • キャプチャした変数を 移動(move) したり、一度限りの操作を行うクロージャに適している。

FnMut

FnMut トレイトは、次のような call_mut メソッドを定義します。

pub trait FnMut<Args>: FnOnce<Args> {
    fn call_mut(&mut self, args: Args) -> Self::Output;
}
  • 特徴
    • call_mut&mut self を受け取るため、クロージャは複数回呼び出すことができ、キャプチャした変数を変更できる
  • 用途
    • キャプチャした環境の変数を変更するクロージャに適している。

Fn

Fn トレイトは、次のような call メソッドを定義します。

pub trait Fn<Args>: FnMut<Args> {
    fn call(&self, args: Args) -> Self::Output;
}
  • 特徴
    • call&self を受け取るため、クロージャは環境を変更せず、キャプチャした変数を単に読み取るだけ
    • 何度でも呼び出せる
  • 用途
    • キャプチャした変数を変更せずに、繰り返し呼び出すクロージャに適している

クロージャがこれらのトレイトをどのように実装するか?

Rust のコンパイラは、クロージャがキャプチャした変数をどのように使用するかに基づいて、自動的にどのトレイトを実装するかを決定します。クロージャのキャプチャ方法には 3 つの種類 があります。

  1. 値のキャプチャ(move):変数の所有権を取得する。
  2. 可変参照のキャプチャ(&mut):変数の可変参照を取得する。
  3. 不変参照のキャプチャ(&):変数の不変参照を取得する。

どのトレイトを実装するかは、変数の使用方法によって決まります。

  • FnOnce のみを実装:キャプチャした変数を 所有権ごと移動 した場合。
  • FnMut と FnOnce を実装:キャプチャした変数を 変更 した場合。
  • Fn、FnMut、FnOnce をすべて実装:キャプチャした変数を 単に読み取るだけ の場合。

サンプルコード

FnOnce を実装するクロージャ

fn main() {
    let s = String::from("hello");
    let closure = move || {
        drop(s); // `s` をムーブして破棄
    };
    closure(); // 1回だけ呼び出せる
    // closure(); // エラー: クロージャはすでに消費された
}

解析

  • クロージャは move を使用して s所有権を取得 しました。
  • 呼び出し時に sdrop してしまうため、このクロージャは 1 回しか呼び出せません
  • そのため、このクロージャは FnOnce のみを実装します

FnMut を実装するクロージャ

fn main() {
    let mut s = String::from("hello");
    let mut closure = || {
        s.push_str(" world"); // `s` を変更
    };
    closure(); // 1回目の呼び出し
    closure(); // 2回目の呼び出し
    println!("{}", s); // 出力: "hello world world"
}

解析

  • クロージャは s可変参照(&mut s) をキャプチャしています。
  • s.push_str(" world") により、キャプチャした変数を 変更 しています。
  • そのため、このクロージャは FnMutFnOnce を実装します

Fn を実装するクロージャ

fn main() {
    let s = String::from("hello");
    let closure = || {
        println!("{}", s); // `s` をただ読むだけ
    };
    closure(); // 1回目の呼び出し
    closure(); // 2回目の呼び出し
}

解析

  • クロージャは s不変参照(&s) をキャプチャしています。
  • s変更せずに読み取るだけ なので、何回でも呼び出すことができます。
  • このクロージャは FnFnMutFnOnce のすべてを実装します

関数の引数としてトレイトを使用する

クロージャは 関数の引数 として渡すことができます。その場合、関数は トレイト境界(trait bound) を指定する必要があります。

FnOnce を使用する関数

fn call_once<F>(f: F)
where
    F: FnOnce(),
{
    f();
}

fn main() {
    let s = String::from("hello");
    call_once(move || {
        drop(s);
    });
}

解析

  • call_onceFnOnce を受け取るため、1 回だけ クロージャを呼び出せます。
  • キャプチャした変数をムーブするクロージャに適した設計 です。

FnMut を使用する関数

fn call_mut<F>(mut f: F)
where
    F: FnMut(),
{
    f();
    f();
}

fn main() {
    let mut s = String::from("hello");
    call_mut(|| {
        s.push_str(" world");
    });
    println!("{}", s); // 出力: "hello world world"
}

解析

  • call_mutFnMut を受け取るため、複数回呼び出し可能
  • キャプチャした変数を変更できるクロージャに適した設計

Fn を使用する関数

fn call_fn<F>(f: F)
where
    F: Fn(),
{
    f();
    f();
}

fn main() {
    let s = String::from("hello");
    call_fn(|| {
        println!("{}", s);
    });
}

解析

  • call_fnFn を受け取るため、複数回呼び出し可能で、キャプチャした変数を変更しないクロージャ を渡せます。

どのトレイトを使うべきか?

適切なトレイトを選択するには、クロージャの振る舞いを考慮する必要があります。

FnOnce を使うべき場合

  • シナリオ:クロージャを 1 回しか呼び出さない、または キャプチャした変数の所有権を移動する必要がある
  • :一度だけ実行される処理や、所有権を別の場所に渡すケース。

FnMut を使うべき場合

  • シナリオ:クロージャを 複数回呼び出す 必要があり、かつ キャプチャした変数を変更する
  • :カウンターの更新や、状態を変更する処理。

Fn を使うべき場合

  • シナリオ:クロージャを 複数回呼び出し たいが、キャプチャした変数を 変更しない
  • :ロギングやデータの取得処理。

関数設計の際の考え方

  • 最も制約の少ないトレイトを選ぶ
    • FnOnce は最も制約が厳しく、1 回しか呼び出せない。
    • FnMut は FnOnce より制約が少なく、可変な状態を扱える。
    • Fn は最も制約が少なく、最も汎用的。
  • API 設計では、可能な限り制約の少ないトレイトを使う
    • FnOnce だと すべてのクロージャを受け入れられるが、呼び出し回数が制限される
    • Fn を指定すると 変更を許可しないが、何度でも呼び出せる

ベストプラクティス

  1. 優先的に Fn を使用する

    • キャプチャした変数を変更しないなら Fn を使う
    • Fn最も制約が少なく、最も汎用的 なので、互換性が高い。
  2. 変更が必要なら FnMut を使用する

    • キャプチャした変数を変更するなら FnMut を使う
    • 例:状態を更新するクロージャ。
  3. 1 回だけの処理なら FnOnce を使用する

    • クロージャが変数の所有権を取得する場合、FnOnce を使う
    • 例:データを移動する処理。
  4. API 設計では適切なトレイトを選ぶ

    • 最も制約が少ないトレイトを選ぶことで、柔軟な API を提供できる
  5. ライフタイムに注意する

    • キャプチャした変数のライフタイムが クロージャの実行範囲をカバーしていることを確認する
    • そうしないと、借用エラーが発生する可能性がある。

私たちはLeapcell、Rustプロジェクトのホスティングの最適解です。

Leapcell

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • Node.js、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。
  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。
  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

Discussion