コンテンツにスキップ

Rustのクロージャ

クロージャ(closure)は、変数に入れたり、ほかの場所へ渡したりできる、名前のない小さな関数です。ふつうの関数との違いは、自分が作られたスコープにある変数を「キャプチャ」(取り込んで使うこと)できる点にあります。

fn main() {
let add_one = |x: i32| x + 1;
println!("{}", add_one(5));
}

実行結果:

6
▶ 実際に試してみよう

縦棒で囲んだ |x: i32| が、ふつうの関数でいう丸かっこの代わりです。2つを並べて比べてみましょう。

fn add_one_fn(x: i32) -> i32 { x + 1 } // ふつうの関数
let add_one_closure = |x: i32| x + 1; // 同じものをクロージャで書いたもの

Rustは引数や戻り値の型を推論できることも多いので、クロージャはさらに短く書かれているのをよく見かけます。

let add_one = |x| x + 1;

引数のないクロージャは、空の縦棒を使って let say_hi = || println!("やあ"); のように書き、say_hi() で呼び出します。

式が1つで収まらないときは、関数と同じように本体を波かっこで囲みます。

fn main() {
let describe = |n: i32| {
let kind = if n % 2 == 0 { "偶数" } else { "奇数" };
format!("{n}は{kind}です")
};
println!("{}", describe(7));
}

実行結果:

7は奇数です
▶ 実際に試してみよう

format! は println! と同じように使えますが、文字列を表示する代わりに String として返します。ここでは、その String が波かっこの中の最後の式なので、それがクロージャの戻り値になります。

周りのスコープから変数をキャプチャする

Section titled “周りのスコープから変数をキャプチャする”

ここが、ふつうの関数にはない機能です。クロージャは、定義された場所にある変数を、引数として渡さなくてもそのまま使えます。

fn main() {
let discount = 10;
let apply_discount = |price: i32| price - discount;
println!("{}", apply_discount(100)); // 90
println!("{}", apply_discount(50)); // 40
}

実行結果:

90
40
▶ 実際に試してみよう

discount はクロージャの引数ではありません。main のスコープにあるだけの変数ですが、クロージャはそこへ手を伸ばして直接使っています。ふつうの fn ではこれはできず、discount を引数として渡す必要があります。

キャプチャした変数を変更するクロージャ

Section titled “キャプチャした変数を変更するクロージャ”

クロージャは、キャプチャした変数を変更することもできます。ただし、その変数が mut であることが条件です。さらにルールがもう1つあります。クロージャ自体も mut の変数に入れなければなりません。呼び出すと何かが変わるので、それがコードを見てわかるようにしたい、というのがRustの考え方です。

fn main() {
let mut count = 0;
let mut increment = || {
count += 1;
println!("カウントは{count}になりました");
};
increment();
increment();
increment();
println!("最終カウント: {count}");
}

実行結果:

カウントは1になりました
カウントは2になりました
カウントは3になりました
最終カウント: 3
▶ 実際に試してみよう

increment の mut を忘れると、コンパイラは何が問題なのかを正確に教えてくれます。calling increment requires mutable binding due to mutable borrow of count(「count を可変で借用しているので、increment を呼び出すには可変の束縛が必要」)というメッセージです。

知っておきたい落とし穴もあります。クロージャをまだ呼び出せる間は、クロージャが count を変更する権利を持っているので、ほかの誰も count に触れられません。2つの increment() の呼び出しの間に println!("{count}"); を入れると、error[E0502]: cannot borrow count as immutable because it is also borrowed as mutable(「count はすでに可変で借用されているので、不変で借用できない」)というエラーになります。最後の increment() の呼び出しが終われば、クロージャの出番は終わり、count はまた自由に使えるようになります。だから 最終カウント の行は動くのです。このルールの詳しい背景は、借用のページで学びます。

クロージャが、キャプチャしたものの所有権をまるごと受け取る必要があることもあります。たとえば、別のスレッドに渡すときです。そのときは、縦棒の前に move を書きます:move || { ... }。実際の使い方は並行処理のページで見ていきます。

クロージャが実際に使われる場面

Section titled “クロージャが実際に使われる場面”

クロージャを単独で使うことは、あまりありません。本当に役に立つのは、ほかの関数に独自の処理を渡すときです。特に、イテレータのページで出てくるイテレータのメソッドと一緒によく使います。

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);
}

実行結果:

[2, 4, 6, 8, 10]
▶ 実際に試してみよう

ここでは、|n| n * 2 というクロージャを .map() に直接渡しています。小さなクロージャで .map() や .filter() のようなメソッドの動きを決める、というこの形こそ、ふだんクロージャを使う場面です。

tax_percent という変数を20で作ります。次に、価格を受け取り、キャプチャした tax_percent を使って税込み価格を返すクロージャ with_tax を書いてください。価格50と120の結果を表示しましょう。ヒント:税額は price * tax_percent / 100 です。

解答を見る
fn main() {
let tax_percent = 20;
let with_tax = |price: i32| price + price * tax_percent / 100;
println!("50の税込み価格は{}", with_tax(50));
println!("120の税込み価格は{}", with_tax(120));
}

実行結果:

50の税込み価格は60
120の税込み価格は144
▶ 実際に試してみよう

tax_percent は一度も引数として渡していません。クロージャが main からキャプチャしているのです。これこそ、ふつうの fn にはできないことです。

1回目に呼び出すと1、2回目は2、3回目は3……と返すクロージャ next_id を書いてください。3回呼び出して、それぞれのIDを表示しましょう。ヒント:クロージャの外に mut のカウンタを置き、中で1を足します。カウンタをクロージャの最後の式にすれば、それが返されます。

解答を見る
fn main() {
let mut last_id = 0;
let mut next_id = || {
last_id += 1;
last_id
};
println!("1人目のユーザーのIDは{}", next_id());
println!("2人目のユーザーのIDは{}", next_id());
println!("3人目のユーザーのIDは{}", next_id());
}

実行結果:

1人目のユーザーのIDは1
2人目のユーザーのIDは2
3人目のユーザーのIDは3
▶ 実際に試してみよう

last_id と next_id の両方に mut が必要です。last_id は値が変わるから、next_id は呼び出すとキャプチャしたものが変わるからです。

  • クロージャは名前のない小さな関数で、|引数| 式 の形で書きます。
  • 型が推論されることが多いので、同じ働きの fn よりたいてい短く書けます。
  • ふつうの関数と違って、クロージャは定義されたスコープの変数をキャプチャできます。
  • キャプチャした変数を変更するクロージャは mut の変数に入れる必要があり、使われている間はその変数を借用し続けます。
  • クロージャがいちばん役立つのは、ほかの関数、特に .map() のようなイテレータのメソッドに渡すときです。

理解度チェック

1. クロージャでは、関数の丸かっこの代わりに、どんな記号で引数を囲みますか?

2. ふつうの fn にはできなくて、クロージャにできることは何ですか?

3. このページによると、クロージャが実際にいちばんよく使われるのはどんな場面ですか?

4. クロージャが、キャプチャした変数に count += 1 をしています。ほかに何が必要ですか?

スコア: 0 / 4