Rustにおけるプログラミングにおいて、柔軟かつ強力なコードを記述するために欠かせない要素の一つがクロージャです。
クロージャは、変数に保存したり、他の関数の引数として渡したりすることができる匿名関数を指します。
一般的な関数とは異なり、クロージャが定義されたスコープ内の変数を「キャプチャ」して利用できるという際立った特徴を持っています。
本記事では、2026年現在のRust開発における標準的なプラクティスを基に、クロージャの基本構文からメモリ管理、そして所有権の仕組みについて詳しく解説します。
Rustのクロージャとは何か
クロージャは、数学的な概念に由来する名前ですが、プログラミングにおいては「環境を閉じ込める関数」と理解すると分かりやすいでしょう。
Rustのクロージャは、実行時に評価されるコードブロックであり、その柔軟性からイテレータの操作や非同期処理、スレッドの生成などで多用されます。
通常の関数 fn と最大の違いは、クロージャが定義された場所にある変数を自由に利用できる点にあります。
これにより、関数呼び出しのたびに大量の引数を渡す手間を省き、コードの可読性と記述効率を大幅に向上させることが可能です。
クロージャの基本構文
クロージャの構文は、パイプ記号 || を使用して引数を囲む形をとります。
最もシンプルなクロージャの例を以下に示します。
// 引数を受け取り、それに1を加算するシンプルなクロージャ
let plus_one = |x: i32| x + 1;
let result = plus_one(10);
println!("Result: {}", result);
Result: 11
上記の例では、plus_one という変数にクロージャを代入しています。
引数は |x: i32| のように記述し、その後に続く式がクロージャの本体となります。
波括弧 {} を使用することで、複数行の処理を記述することも可能です。
let calculate = |x: i32, y: i32| {
println!("Calculating {} + {}", x, y);
x + y
};
let sum = calculate(5, 15);
Calculating 5 + 15
型推論の強力なサポート
Rustのクロージャには、非常に強力な型推論の仕組みが備わっています。
通常の fn 関数では引数や戻り値の型を明示する必要がありますが、クロージャでは多くの場合、型注釈を省略できます。
コンパイラは、クロージャが最初に使用された際の引数の型に基づいて、そのクロージャ全体の型を決定します。
ただし、一度型が決定されると、それ以降は異なる型でそのクロージャを呼び出すことはできません。
以下のコードは、型推論の挙動を確認するための例です。
let example_closure = |x| x;
// 最初にString型で呼び出す
let s = example_closure(String::from("hello"));
// 同じクロージャを整数型で呼び出そうとするとコンパイルエラーになる
// let n = example_closure(5);
このように、クロージャは簡潔さと厳格な型安全性の両立を実現しています。
クロージャによる環境のキャプチャ
クロージャの真骨頂は、その定義スコープにある変数を利用できる「キャプチャ」機能にあります。
Rustでは、クロージャが環境から変数を取得する方法は主に3つ存在します。
3種類のキャプチャ方法
クロージャは、以下のいずれかの方法で変数を借用または所有します。
- 不変の借用 (
&T) - 可変の借用 (
&mut T) - 所有権の移動 (
T)
コンパイラは、クロージャ内で変数がどのように使用されているかを分析し、最も制限の少ない方法を自動的に選択します。
例えば、変数の中身を読み取るだけであれば不変の借用が選択されます。
let list = vec![1, 2, 3];
// listを不変の参照としてキャプチャする
let print_list = || println!("From closure: {:?}", list);
println!("Before closure: {:?}", list);
print_list();
println!("After closure: {:?}", list);
Before closure: [1, 2, 3]
From closure: [1, 2, 3]
After closure: [1, 2, 3]
一方、クロージャ内で変数の値を変更する場合は、可変の借用が必要になります。
let mut list = vec![1, 2, 3];
// listを可変の参照としてキャプチャする
let mut add_to_list = || list.push(4);
add_to_list();
println!("Updated list: {:?}", list);
Updated list: [1, 2, 3, 4]
moveキーワードによる所有権の強制移動
スレッド間でのデータ共有や、クロージャを関数の戻り値として返す場合、元の変数がスコープを抜けた後もクロージャが存続する必要があります。
このようなケースでは、move キーワードを使用して、環境にある変数の所有権を強制的にクロージャ内部へ移動させます。
use std::thread;
let list = vec![1, 2, 3];
// moveを付けないと、threadがlistより長生きする可能性があるためコンパイルエラーになる
let handle = thread::spawn(move || {
println!("Thread list: {:?}", list);
});
handle.join().unwrap();
move を使用すると、元のスコープでその変数を利用できなくなるため、メモリの安全性が保証されます。
クロージャを抽象化する3つのトレイト
Rustの型システムにおいて、クロージャは特定の構造体として実装されています。
どのようなキャプチャを行い、どのように呼び出されるかに応じて、クロージャは以下の3つの Fn トレイトのいずれか、あるいは複数を実装します。
| トレイト名 | キャプチャの方法 | 実行回数と特徴 |
|---|---|---|
Fn | 不変の借用 (&T) | 何度でも呼び出し可能。環境を変更しない。 |
FnMut | 可変の借用 (&mut T) | 何度でも呼び出し可能。環境の状態を変更できる。 |
FnOnce | 所有権の移動 (T) | 一度しか呼び出せない。キャプチャした変数を消費する。 |
FnOnce の役割
FnOnce は、キャプチャした変数の所有権をクロージャ内部で消費する場合に実装されます。
「一度きり (Once)」の名前の通り、環境から奪ったデータを破壊または移動させるため、2回以上の呼び出しは禁止されます。
すべてのクロージャは、少なくとも FnOnce を実装していると見なされます。
FnMut の役割
FnMut は、キャプチャした変数を変更するクロージャに実装されます。
変数の値を書き換えることはできますが、所有権自体は保持し続けるため、複数回の呼び出しが可能です。
ただし、クロージャ自体を保持する変数を mut として宣言する必要があります。
Fn の役割
Fn は最も制限が少ない(利用側にとって便利な)トレイトです。
環境から何もキャプチャしないか、あるいは不変の参照のみをキャプチャする場合に実装されます。
並列処理などにおいて、複数の場所から同時に呼び出しても安全なクロージャはこの Fn トレイトを満たしています。
実践:クロージャを引数に取る関数
ライブラリを設計する際、処理の一部をユーザーに委ねるために、クロージャを引数として受け取る関数を定義することがよくあります。
このとき、ジェネリクスとトレイト境界を使用して、どのようなクロージャを受け入れるかを指定します。
引数として受け取る例
fn apply_operation<F>(value: i32, func: F) -> i32
where
F: Fn(i32) -> i32,
{
func(value)
}
fn main() {
let square = |x| x * x;
let result = apply_operation(5, square);
println!("Result: {}", result);
}
Result: 25
上記の apply_operation 関数は、Fn(i32) -> i32 という境界を持つジェネリクス F を受け取ります。
これにより、整数を受け取って整数を返す任意のクロージャを柔軟に渡すことができます。
クロージャを返す関数の定義
関数からクロージャを返したい場合は、impl Fn 構文を使用するのが一般的です。
クロージャの具体的な型はコンパイラのみが知っている「不透明な型」であるため、トレイトを使用して型を表現します。
fn create_adder(increment: i32) -> impl Fn(i32) -> i32 {
move |x| x + increment
}
fn main() {
let add_five = create_adder(5);
println!("Result: {}", add_five(10));
}
ここでは increment の所有権をクロージャに渡すために move が必要不可欠です。
moveがないと、increment変数が関数終了時に破棄されてしまうため、エラーとなります。
クロージャとイテレータの相性
Rustの標準ライブラリにおいて、クロージャが最も活躍する場面の一つがイテレータ操作です。
map, filter, fold などのメソッドはすべてクロージャを引数に取ります。
let numbers = vec![1, 2, 3, 4, 5];
let even_squares: Vec<i32> = numbers
.into_iter()
.filter(|&n| n % 2 == 0) // 偶数のみ抽出
.map(|n| n * n) // 二乗する
.collect();
println!("Processed: {:?}", even_squares);
Processed: [4, 16]
このように、クロージャを組み合わせることで、複雑なコレクション操作を宣言的かつ簡潔に記述できます。
これは、命令型のループ処理と比較して、バグが混入しにくくメンテナンス性の高いコードを実現するための重要なテクニックです。
パフォーマンスに関する考察
Rustのクロージャは、いわゆる「ゼロコスト抽象化」の一例です。
コンパイラはクロージャごとに独自の型を生成し、その呼び出しをインライン化することが可能です。
そのため、関数ポインタを介した呼び出しのようなランタイムオーバーヘッドは通常発生しません。
静的ディスパッチによって最適化されるため、パフォーマンスが最優先されるシステムプログラミングにおいても安心して利用できます。
ただし、動的ディスパッチ(Box<dyn Fn()> など)を使用する場合は、わずかなコストが発生することを覚えておくと良いでしょう。
まとめ
Rustのクロージャは、単なる便利な匿名関数以上の役割を担っています。
|| というシンプルな構文の裏側には、Rustの核心である所有権、借用、そしてトレイトシステムが密接に関わっています。
環境をキャプチャする仕組みを理解し、Fn, FnMut, FnOnce の違いを意識することで、より高度なライブラリ設計やアプリケーション開発が可能になります。
特に move キーワードの使い方をマスターすることは、スレッドセーフなコードを書く上で非常に重要です。
本記事で紹介した基本原則を土台として、ぜひ日々のRust開発でクロージャを積極的に活用し、その表現力の高さを体感してください。
Rustコンパイラという強力な味方と共に、安全で効率的なコードを構築していきましょう。
