Rustの基礎 - クロージャ
概要
クロージャとは、環境の変数をキャプチャできる無名関数のことを指す。
通常のメソッドとは異なり、定義されたスコープの変数を参照することができる。
Rustのクロージャは、型推論が可能で、引数と戻り値の型を明示的に指定する必要がない場合が多い。
クロージャの基本
クロージャを定義する基本的な構文を以下に示す。
let closure_name = |引数| 式;
以下の例では、2つの引数を受け取り、その合計を返すクロージャを定義している。
fn main() {
let add = |a, b| a + b;
println!("{}", add(2, 3));
}
// 出力
5
複数行のクロージャを定義する場合は、波括弧{}を使用する。
fn main() {
let add = |a, b| {
let result = a + b;
result
};
println!("{}", add(2, 3));
}
// 出力
5
型を明示的に指定することもできる。
fn main() {
let add = |a: i32, b: i32| -> i32 {
a + b
};
println!("{}", add(2, 3));
}
// 出力
5
環境のキャプチャ
クロージャは、定義されたスコープの変数をキャプチャすることができる。
これが通常のメソッドとの主な違いである。
fn main() {
let x = 10;
let add_x = |y| x + y;
println!("{}", add_x(5));
}
// 出力
15
クロージャは、変数を3つの方法でキャプチャできる。
- 不変参照 (
&T) によるキャプチャ - 可変参照 (
&mut T) によるキャプチャ - 所有権の移動 (
T) によるキャプチャ
デフォルトでは、クロージャは必要最小限の方法で変数をキャプチャする。
fn main() {
let mut count = 0;
// 可変参照でキャプチャ
let mut increment = || {
count += 1;
println!("count: {}", count);
};
increment();
increment();
}
// 出力
count: 1
count: 2
メソッドがクロージャを返す
以下の例では、outer_funcメソッドがクロージャを返している。
このクロージャは、outer_funcメソッドの引数aとbをキャプチャしている。
fn outer_func(a: i32, b: i32) -> impl Fn() -> i32 {
move || a + b
}
fn main() {
let result = outer_func(2, 3);
println!("{}", result());
}
// 出力
5
上記の例では、move キーワードを使用している。
これは、クロージャが変数の所有権を奪うことを明示的に指定している。
moveを使用しないと、クロージャは参照でキャプチャしようとするが、
メソッドから返される場合は、ライフタイムの問題が発生するため、move キーワードが必要となる。
戻り値の型impl Fn() -> i32は、引数を取らずi32を返すクロージャを意味する。
クロージャの実行
クロージャを実行するには、変数に代入した後、関数と同様に丸括弧 () を使用する。
fn outer_func(a: i32, b: i32) -> impl Fn() -> i32 {
move || a + b
}
fn main() {
let func = outer_func(2, 3);
println!("{}", func());
}
// 出力
5
以下の例では、長方形の面積を計算している。
引数に横(width)を与えるarea_calc メソッドを定義して、縦(height)を引数として面積を計算するクロージャを返している。
fn area_calc(width: i32) -> impl Fn(i32) -> i32 {
move |height| width * height
}
fn main() {
// widthが25と50の場合を計算
let ac1 = area_calc(25);
let ac2 = area_calc(50);
// heightを10として面積を求める
println!("{}", ac1(10));
println!("{}", ac2(10));
}
// 出力
250
500
クロージャのトレイト
Rustのクロージャは、以下に示す3つのトレイトのいずれかを実装する。
FnOnce- 1度だけ呼び出せるクロージャ。
- キャプチャした変数の所有権を消費する可能性がある。
FnMut- 複数回呼び出せるクロージャ。
- キャプチャした変数を可変的に借用する。
Fn- 複数回呼び出せるクロージャ。
- キャプチャした変数を不変的に借用する。
全てのクロージャは少なくとも FnOnce を実装する。
キャプチャした変数を消費しないクロージャは FnMut も実装する。
キャプチャした変数を変更しないクロージャは Fn も実装する。
以下の例では、それぞれのトレイトを使用したクロージャを示している。
fn main() {
// Fn : 不変借用
let x = 5;
let print_x = || println!("x = {}", x);
print_x();
print_x(); // 複数回呼び出せる
// FnMut : 可変借用
let mut count = 0;
let mut increment = || {
count += 1;
count
};
println!("{}", increment());
println!("{}", increment()); // 複数回呼び出せる
// FnOnce: 所有権の移動
let s = String::from("hello");
let consume = || {
drop(s); // sの所有権を消費
};
consume();
// consume(); // エラー: 2回目は呼び出せない
}
// 出力
x = 5
x = 5
1
2
メソッドの引数としてクロージャを受け取る
メソッドの引数としてクロージャを受け取ることができる。
以下の例では、ジェネリクスとトレイト境界を使用してクロージャを受け取っている。
fn apply<F>(f: F, x: i32, y: i32) -> i32
where
F: Fn(i32, i32) -> i32,
{
f(x, y)
}
fn main() {
let add = |a, b| a + b;
let multiply = |a, b| a * b;
println!("{}", apply(add, 3, 4));
println!("{}", apply(multiply, 3, 4));
}
// 出力
7
12
impl Trait 構文を使用して、簡潔に記述することもできる。
fn apply(f: impl Fn(i32, i32) -> i32, x: i32, y: i32) -> i32 {
f(x, y)
}
fn main() {
let add = |a, b| a + b;
println!("{}", apply(add, 3, 4));
}
// 出力
7
イテレータとクロージャ
Rustでは、イテレータのメソッドでクロージャを頻繁に使用する。
以下に示すものは、代表的な使用例である。
mapメソッドを使用して、各要素を変換する例。
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|&x| x * 2).collect();
println!("{:?}", doubled);
}
// 出力
[2, 4, 6, 8, 10]
以下の例では、filter メソッドを使用して、条件に合う要素のみを抽出している。
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<i32> = numbers.iter().filter(|&x| x % 2 == 0).copied().collect();
println!("{:?}", evens);
}
// 出力
[2, 4, 6]
以下の例では、fold メソッドを使用して、累積計算を行っている。
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let sum = numbers.iter().fold(0, |acc, &x| acc + x);
println!("{}", sum);
}
// 出力
15
クロージャと所有権
クロージャは、キャプチャした変数の所有権を扱う方法を柔軟に制御できる。
デフォルトでは、クロージャは必要最小限の方法で変数をキャプチャする。
fn main() {
let s = String::from("hello");
// 不変参照でキャプチャ
let print = || println!("{}", s);
print();
// sはまだ使用できる
println!("{}", s);
}
move キーワードを使用すると、所有権を強制的に移動できる。
これは、クロージャが元のスコープよりも長く生存する場合に必要となる。
use std::thread;
fn main() {
let s = String::from("hello");
// 別スレッドで実行するため、moveが必要
let handle = thread::spawn(move || {
println!("{}", s);
});
// println!("{}", s); // エラー: sの所有権は移動済み
handle.join().unwrap();
}