Rustのクロージャの仕組み:Fn、FnMut、FnOnce
Rust プログラミング言語において、クロージャ(closures)は非常に強力で柔軟な機能であり、無名関数を定義し、その環境内の変数をキャプチャすることができます。Rust のクロージャシステムは、Fn、FnMut、FnOnce という 3 つのコアトレイトによって定義されます。これらのトレイトは、クロージャがキャプチャした変数とどのように相互作用するか、何回呼び出せるか、環境を変更できるかを決定します。これらの理解は、Rust のクロージャメカニズムを習得し、効率的で安全なコードを書くために不可欠です。
本記事では、Fn、FnMut、FnOnce の 3 つのトレイトについて詳しく説明し、それぞれの定義、用途、使用方法、適用シナリオを紹介します。また、コード例やベストプラクティスを提供し、これらの知識を総合的に学べるようにします。
Fn、FnMut、FnOnce とは?
Fn、FnMut、FnOnce は、Rust の標準ライブラリで定義されたトレイトであり、クロージャ(または任意の呼び出し可能なオブジェクト)の振る舞いを記述します。これらの主な違いは、クロージャがキャプチャした変数をどのように扱うか、および呼び出し時の所有権のルールにあります。
- FnOnce:クロージャは 一度だけ 呼び出すことができ、呼び出し後に消費(consume)され、再利用できません。
- FnMut:クロージャは 複数回 呼び出せ、キャプチャした変数を 変更 することができます。
- Fn:クロージャは 複数回 呼び出せますが、キャプチャした変数を 変更せずに読み取る だけです。
これらのトレイトは、以下のような 継承関係 を持ちます。
- Fn は FnMut を継承し、FnMut は FnOnce を継承します。
- つまり、あるクロージャが Fn を実装しているならば、自動的に FnMut と FnOnce も実装されます。
- 同様に、FnMut を実装しているならば、自動的に FnOnce も実装されます。
各トレイトの定義
FnOnce
FnOnce トレイトは、次のような call_once
メソッドを定義します。
pub trait FnOnce<Args> {
type Output;
fn call_once(self, args: Args) -> Self::Output;
}
-
特徴:
-
call_once
はself
を受け取る(&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 つの種類 があります。
- 値のキャプチャ(move):変数の所有権を取得する。
- 可変参照のキャプチャ(&mut):変数の可変参照を取得する。
- 不変参照のキャプチャ(&):変数の不変参照を取得する。
どのトレイトを実装するかは、変数の使用方法によって決まります。
- 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
の 所有権を取得 しました。 - 呼び出し時に
s
をdrop
してしまうため、このクロージャは 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")
により、キャプチャした変数を 変更 しています。 -
そのため、このクロージャは
FnMut
とFnOnce
を実装します。
Fn を実装するクロージャ
fn main() {
let s = String::from("hello");
let closure = || {
println!("{}", s); // `s` をただ読むだけ
};
closure(); // 1回目の呼び出し
closure(); // 2回目の呼び出し
}
解析:
- クロージャは
s
の 不変参照(&s) をキャプチャしています。 -
s
を 変更せずに読み取るだけ なので、何回でも呼び出すことができます。 -
このクロージャは
Fn
、FnMut
、FnOnce
のすべてを実装します。
関数の引数としてトレイトを使用する
クロージャは 関数の引数 として渡すことができます。その場合、関数は トレイト境界(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_once
はFnOnce
を受け取るため、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_mut
はFnMut
を受け取るため、複数回呼び出し可能。 - キャプチャした変数を変更できるクロージャに適した設計。
Fn を使用する関数
fn call_fn<F>(f: F)
where
F: Fn(),
{
f();
f();
}
fn main() {
let s = String::from("hello");
call_fn(|| {
println!("{}", s);
});
}
解析:
-
call_fn
はFn
を受け取るため、複数回呼び出し可能で、キャプチャした変数を変更しないクロージャ を渡せます。
どのトレイトを使うべきか?
適切なトレイトを選択するには、クロージャの振る舞いを考慮する必要があります。
FnOnce を使うべき場合
- シナリオ:クロージャを 1 回しか呼び出さない、または キャプチャした変数の所有権を移動する必要がある。
- 例:一度だけ実行される処理や、所有権を別の場所に渡すケース。
FnMut を使うべき場合
- シナリオ:クロージャを 複数回呼び出す 必要があり、かつ キャプチャした変数を変更する。
- 例:カウンターの更新や、状態を変更する処理。
Fn を使うべき場合
- シナリオ:クロージャを 複数回呼び出し たいが、キャプチャした変数を 変更しない。
- 例:ロギングやデータの取得処理。
関数設計の際の考え方
-
最も制約の少ないトレイトを選ぶ:
- FnOnce は最も制約が厳しく、1 回しか呼び出せない。
- FnMut は FnOnce より制約が少なく、可変な状態を扱える。
- Fn は最も制約が少なく、最も汎用的。
-
API 設計では、可能な限り制約の少ないトレイトを使う:
-
FnOnce
だと すべてのクロージャを受け入れられるが、呼び出し回数が制限される。 -
Fn
を指定すると 変更を許可しないが、何度でも呼び出せる。
-
ベストプラクティス
-
優先的に
Fn
を使用する:-
キャプチャした変数を変更しないなら
Fn
を使う。 -
Fn
は 最も制約が少なく、最も汎用的 なので、互換性が高い。
-
キャプチャした変数を変更しないなら
-
変更が必要なら
FnMut
を使用する:-
キャプチャした変数を変更するなら
FnMut
を使う。 - 例:状態を更新するクロージャ。
-
キャプチャした変数を変更するなら
-
1 回だけの処理なら
FnOnce
を使用する:-
クロージャが変数の所有権を取得する場合、
FnOnce
を使う。 - 例:データを移動する処理。
-
クロージャが変数の所有権を取得する場合、
-
API 設計では適切なトレイトを選ぶ:
- 最も制約が少ないトレイトを選ぶことで、柔軟な API を提供できる。
-
ライフタイムに注意する:
- キャプチャした変数のライフタイムが クロージャの実行範囲をカバーしていることを確認する。
- そうしないと、借用エラーが発生する可能性がある。
私たちはLeapcell、Rustプロジェクトのホスティングの最適解です。
Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:
複数言語サポート
- Node.js、Python、Go、Rustで開発できます。
無制限のプロジェクトデプロイ
- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。
比類のないコスト効率
- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。
洗練された開発者体験
- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。
簡単なスケーラビリティと高パフォーマンス
- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。
Xでフォローする:@LeapcellHQ
Discussion