1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

表紙

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


ブログでこの記事を読む

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?