クロージャ: 環境をキャプチャできる匿名関数
Rustのクロージャは、変数に保存したり、引数として他の関数に渡すことのできる匿名関数です。 ある場所でクロージャを生成し、それから別の文脈でクロージャを呼び出して評価することができます。 関数と異なり、呼び出されたスコープの値をクロージャは、キャプチャすることができます。 これらのクロージャの機能がコードの再利用や、動作のカスタマイズを行わせてくれる方法を模擬しましょう。
クロージャで動作の抽象化を行う
クロージャを保存して後々使用できるようにするのが有用な場面の例に取り掛かりましょう。その過程で、 クロージャの記法、型推論、トレイトについて語ります。
以下のような架空の場面を考えてください: カスタマイズされたエクササイズのトレーニングプランを生成するアプリを作るスタートアップで働くことになりました。 バックエンドはRustで記述され、トレーニングプランを生成するアルゴリズムは、アプリユーザの年齢や、 BMI、運動の好み、最近のトレーニング、指定された強弱値などの多くの要因を考慮します。 実際に使用されるアルゴリズムは、この例では重要ではありません; 重要なのは、この計算が数秒要することです。 必要なときだけこのアルゴリズムを呼び出し、1回だけ呼び出したいので、必要以上にユーザを待たせないことになります。
リスト13-1に示したsimulated_expensive_calculation
関数でこの仮定のアルゴリズムを呼び出すことをシミュレートし、
この関数はcalculating slowly
と出力し、2秒待ってから、渡した数値をなんでも返します。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; fn simulated_expensive_calculation(intensity: u32) -> u32 { // ゆっくり計算します println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); intensity } }
次は、この例で重要なトレーニングアプリの部分を含むmain
関数です。この関数は、
ユーザがトレーニングプランを要求した時にアプリが呼び出すコードを表します。
アプリのフロントエンドと相互作用する部分は、クロージャの使用と関係ないので、プログラムへの入力を表す値をハードコードし、
その出力を表示します。
必要な入力は以下の通りです:
- ユーザの強弱値、これはユーザがトレーニングを要求して、低強度のトレーニングか、 高強度のトレーニングがしたいかを示したときに指定されます。
- 乱数、これはトレーニングプランにバリエーションを起こします。
出力は、推奨されるトレーニングプランになります。リスト13-2は使用するmain
関数を示しています。
ファイル名: src/main.rs
fn main() { let simulated_user_specified_value = 10; let simulated_random_number = 7; generate_workout( simulated_user_specified_value, simulated_random_number ); } fn generate_workout(intensity: u32, random_number: u32) {}
簡潔性のために、変数simulated_user_specified_value
は10、変数simulated_random_number
は7とハードコードしました;
実際のプログラムにおいては、強弱値はアプリのフロントエンドから取得し、乱数の生成には、第2章の数当てゲームの例のように、rand
クレートを使用するでしょう。
main
関数は、シミュレートされた入力値とともにgenerate_workout
関数を呼び出します。
今や文脈ができたので、アルゴリズムに取り掛かりましょう。リスト13-3のgenerate_workout
関数は、
この例で最も気にかかるアプリのビジネスロジックを含んでいます。この例での残りの変更は、
この関数に対して行われるでしょう:
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; fn simulated_expensive_calculation(num: u32) -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num } fn generate_workout(intensity: u32, random_number: u32) { if intensity < 25 { println!( // 今日は{}回腕立て伏せをしてください! "Today, do {} pushups!", simulated_expensive_calculation(intensity) ); println!( // 次に、{}回腹筋をしてください! "Next, do {} situps!", simulated_expensive_calculation(intensity) ); } else { if random_number == 3 { // 今日は休憩してください!水分補給を忘れずに! println!("Take a break today! Remember to stay hydrated!"); } else { println!( // 今日は、{}分間走ってください! "Today, run for {} minutes!", simulated_expensive_calculation(intensity) ); } } } }
リスト13-3のコードには、遅い計算を行う関数への呼び出しが複数あります。最初のif
ブロックが、
simulated_expensive_calculation
を2回呼び出し、外側のelse
内のif
は全く呼び出さず、
2番目のelse
ケースの内側にあるコードは1回呼び出しています。
generate_workout
関数の期待される振る舞いは、まずユーザが低強度のトレーニング(25より小さい数値で表される)か、
高強度のトレーニング(25以上の数値)を欲しているか確認することです。
低強度のトレーニングプランは、シミュレーションしている複雑なアルゴリズムに基づいて、 多くの腕立て伏せや腹筋運動を推奨してきます。
ユーザが高強度のトレーニングを欲していれば、追加のロジックがあります: アプリが生成した乱数がたまたま3なら、 アプリは休憩と水分補給を勧めます。そうでなければ、ユーザは複雑なアルゴリズムに基づいて数分間のランニングをします。
このコードは現在、ビジネスのほしいままに動くでしょうが、データサイエンスチームが、
simulated_expensive_calculation
関数を呼び出す方法に何らかの変更を加える必要があると決定したとしましょう。
そのような変更が起きた時に更新を簡略化するため、simulated_expensive_calculation
関数を1回だけ呼び出すように、
このコードをリファクタリングしたいです。また、その過程でその関数への呼び出しを増やすことなく無駄に2回、
この関数を現時点で呼んでいるところを切り捨てたくもあります。要するに、結果が必要なければ関数を呼び出したくなく、
それでも1回だけ呼び出したいのです。
関数でリファクタリング
多くの方法でトレーニングプログラムを再構築することもできます。
1番目にsimulated_expensive_calculation
関数への重複した呼び出しを変数に抽出しようとしましょう。リスト13-4に示したように。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; fn simulated_expensive_calculation(num: u32) -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num } fn generate_workout(intensity: u32, random_number: u32) { let expensive_result = simulated_expensive_calculation(intensity); if intensity < 25 { println!( "Today, do {} pushups!", expensive_result ); println!( "Next, do {} situps!", expensive_result ); } else { if random_number == 3 { println!("Take a break today! Remember to stay hydrated!"); } else { println!( "Today, run for {} minutes!", expensive_result ); } } } }
この変更によりsimulated_expensive_calculation
の呼び出しが単一化され、
最初のif
ブロックが無駄に関数を2回呼んでいた問題を解決します。不幸なことに、これでは、
あらゆる場合にこの関数を呼び出し、その結果を待つことになり、結果値を全く使用しない内側のif
ブロックでもそうしてしまいます。
プログラムの1箇所でコードを定義したいですが、結果が本当に必要なところでだけコードを実行します。 これは、クロージャのユースケースです!
クロージャでリファクタリングして、コードを保存する
if
ブロックの前にいつもsimulated_expensive_calculation
関数を呼び出す代わりに、
クロージャを定義し、関数呼び出しの結果を保存するのではなく、そのクロージャを変数に保存できます。リスト13-5のようにですね。
simulated_expensive_calculation
の本体全体を実際に、ここで導入しているクロージャ内に移すことができます。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; let expensive_closure = |num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; expensive_closure(5); }
クロージャ定義が=
に続き、変数expensive_closure
に代入されています。クロージャを定義するには、
1組の縦棒から始め、その内部にクロージャの仮引数を指定します; この記法は、SmalltalkやRubyのクロージャ定義と類似していることから、
選択されました。このクロージャには、num
という引数が1つあります: 2つ以上引数があるなら、
|param1, param2|
のように、カンマで区切ります。
引数の後に、クロージャの本体を保持する波括弧を配置します(これはクロージャ本体が式一つなら省略可能です)。
波括弧の後、クロージャのお尻には、セミコロンが必要で、let
文を完成させます。クロージャ本体の最後の行から返る値(num
)が、
呼び出された時にクロージャから返る値になります。その行がセミコロンで終わっていないからです;
ちょうど関数の本体みたいですね。
このlet
文は、expensive_closure
が、匿名関数を呼び出した結果の値ではなく、
匿名関数の定義を含むことを意味することに注意してください。コードを定義して、
1箇所で呼び出し、そのコードを保存し、後々、それを呼び出したいがためにクロージャを使用していることを思い出してください;
呼び出したいコードは、現在、expensive_closure
に保存されています。
クロージャが定義されたので、if
ブロックのコードを変更して、そのコードを実行するクロージャを呼び出し、結果値を得ることができます。
クロージャは、関数のように呼び出せます: クロージャ定義を含む変数名を指定し、使用したい引数値を含むかっこを続けます。
リスト13-6に示したようにですね。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; fn generate_workout(intensity: u32, random_number: u32) { let expensive_closure = |num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; if intensity < 25 { println!( "Today, do {} pushups!", expensive_closure(intensity) ); println!( "Next, do {} situps!", expensive_closure(intensity) ); } else { if random_number == 3 { println!("Take a break today! Remember to stay hydrated!"); } else { println!( "Today, run for {} minutes!", expensive_closure(intensity) ); } } } }
今では、重い計算はたった1箇所でのみ呼び出され、その結果が必要なコードを実行するだけになりました。
ところが、リスト13-3の問題の一つを再浮上させてしまいました: それでも、最初のif
ブロックでクロージャを2回呼んでいて、
そうすると、重いコードを2回呼び出し、必要な分の2倍ユーザを待たせてしまいます。そのif
ブロックのみに属する変数を生成して、
クロージャの呼び出し結果を保持するそのif
ブロックに固有の変数を生成することでこの問題を解消することもできますが、
クロージャは他の解決法も用意してくれます。その解決策については、もう少し先で語りましょう。でもまずは、
クロージャ定義に型注釈がない理由とクロージャに関わるトレイトについて話しましょう。
クロージャの型推論と注釈
クロージャでは、fn
関数のように引数の型や戻り値の型を注釈する必要はありません。関数では、
型注釈は必要です。ユーザに露出する明示的なインターフェイスの一部だからです。このインターフェイスを堅実に定義することは、
関数が使用したり、返したりする値の型についてみんなが合意していることを保証するために重要なのです。
しかし、クロージャはこのような露出するインターフェイスには使用されません: 変数に保存され、
名前付けしたり、ライブラリの使用者に晒されることなく、使用されます。
クロージャは通常短く、あらゆる任意の筋書きではなく、狭い文脈でのみ関係します。 このような限定された文脈内では、コンパイラは、多くの変数の型を推論できるのと似たように、 引数や戻り値の型を頼もしく推論することができます。
このような小さく匿名の関数で型をプログラマに注釈させることは煩わしいし、コンパイラがすでに利用可能な情報と大きく被っています。
本当に必要な以上に冗長になることと引き換えに、明示性と明瞭性を向上させたいなら、変数に型注釈を加えることもできます; リスト13-5で定義したクロージャに型を注釈するなら、リスト13-7に示した定義のようになるでしょう。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; let expensive_closure = |num: u32| -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; }
型注釈を付け加えると、クロージャの記法は、関数の記法により酷似して見えます。以下が、引数に1を加える関数の定義と、 同じ振る舞いをするクロージャの定義の記法を縦に比べたものです。 空白を追加して、関連のある部分を並べています。これにより、縦棒の使用と省略可能な記法の量を除いて、 クロージャ記法が関数記法に似ているところを説明しています。
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
1行目が関数定義を示し、2行目がフルに注釈したクロージャ定義を示しています。 3行目は、クロージャ定義から型注釈を取り除き、4行目は、かっこを取り除いていて、 かっこはクロージャの本体がただ1つの式からなるので、省略可能です。これらは全て、 呼び出された時に同じ振る舞いになる合法な定義です。
クロージャ定義には、引数それぞれと戻り値に対して推論される具体的な型が一つあります。例えば、
リスト13-8に引数として受け取った値を返すだけの短いクロージャの定義を示しました。
このクロージャは、この例での目的以外には有用ではありません。この定義には、
何も型注釈を加えていないことに注意してください: それから1回目にString
を引数に、
2回目にu32
を引数に使用してこのクロージャを2回呼び出そうとしたら、エラーになります。
ファイル名: src/main.rs
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
コンパイラは、次のエラーを返します:
error[E0308]: mismatched types
--> src/main.rs
|
| let n = example_closure(5);
| ^ expected struct `std::string::String`, found
integral variable
|
= note: expected type `std::string::String`
found type `{integer}`
String
値でexample_closure
を呼び出した最初の時点で、コンパイラはx
とクロージャの戻り値の型をString
と推論します。
そして、その型がexample_closure
のクロージャに閉じ込められ、同じクロージャを異なる型で使用しようとすると、
型エラーが出るのです。
ジェネリック引数とFn
トレイトを使用してクロージャを保存する
トレーニング生成アプリに戻りましょう。リスト13-6において、まだコードは必要以上の回数、重い計算のクロージャを呼んでいました。 この問題を解決する一つの選択肢は、重いクロージャの結果を再利用できるように変数に保存し、クロージャを再度呼ぶ代わりに、 結果が必要になる箇所それぞれでその変数を使用することです。しかしながら、この方法は同じコードを大量に繰り返す可能性があります。
運のいいことに、別の解決策もあります。クロージャやクロージャの呼び出し結果の値を保持する構造体を作れるのです。 結果の値が必要な場合のみにその構造体はクロージャを実行し、その結果の値をキャッシュするので、残りのコードは、 結果を保存し、再利用する責任を負わなくて済むのです。このパターンは、メモ化(memoization)または、 遅延評価(lazy evaluation)として知っているかもしれません。
クロージャを保持する構造体を作成するために、クロージャの型を指定する必要があります。 構造体定義は、各フィールドの型を把握しておく必要がありますからね。各クロージャインスタンスには、 独自の匿名の型があります: つまり、たとえ2つのクロージャが全く同じシグニチャでも、その型はそれでも違うものと考えられるということです。 クロージャを使用する構造体、enum、関数引数を定義するには、第10章で議論したように、 ジェネリクスとトレイト境界を使用します。
Fn
トレイトは、標準ライブラリで用意されています。全てのクロージャは、以下のいずれかのトレイトを実装しています:
Fn
、FnMut
または、FnOnce
です。「クロージャで環境をキャプチャする」節で、これらのトレイト間の差異を議論します;
この例では、Fn
トレイトを使えます。
Fn
トレイト境界にいくつかの型を追加することで、このトレイト境界に合致するクロージャが持つべき引数と戻り値の型を示します。
今回のクロージャはu32
型の引数を一つ取り、u32
を返すので、指定するトレイト境界はFn(u32) -> u32
になります。
リスト13-9は、クロージャとオプションの結果値を保持するCacher
構造体の定義を示しています。
ファイル名: src/main.rs
#![allow(unused)] fn main() { struct Cacher<T> where T: Fn(u32) -> u32 { calculation: T, value: Option<u32>, } }
Cacher
構造体は、ジェネリックな型T
のcalculation
フィールドを持ちます。T
のトレイト境界は、
Fn
トレイトを使うことでクロージャであると指定しています。calculation
フィールドに保存したいクロージャは全て、
1つのu32
引数(Fn
の後の括弧内で指定されている)を取り、u32
(->
の後に指定されている)を返さなければなりません。
注釈: 関数も3つの
Fn
トレイト全部を実装します。もし環境から値をキャプチャする必要がなければ、Fn
トレイトを実装する何かが必要になるクロージャではなく、関数を使用できます。
value
フィールドの型は、Option<u32>
です。クロージャを実行する前に、value
はNone
になるでしょう。
Cacher
を使用するコードがクロージャの結果を求めてきたら、その時点でCacher
はクロージャを実行し、
その結果をvalue
フィールドのSome
列挙子に保存します。それから、コードが再度クロージャの結果を求めたら、
クロージャを再実行するのではなく、Cacher
はSome
列挙子に保持された結果を返すでしょう。
たった今解説したvalue
フィールド周りのロジックは、リスト13-10で定義されています。
ファイル名: src/main.rs
#![allow(unused)] fn main() { struct Cacher<T> where T: Fn(u32) -> u32 { calculation: T, value: Option<u32>, } impl<T> Cacher<T> where T: Fn(u32) -> u32 { fn new(calculation: T) -> Cacher<T> { Cacher { calculation, value: None, } } fn value(&mut self, arg: u32) -> u32 { match self.value { Some(v) => v, None => { let v = (self.calculation)(arg); self.value = Some(v); v }, } } } }
呼び出し元のコードにこれらのフィールドの値を直接変えてもらうのではなく、Cacher
に構造体のフィールドの値を管理してほしいので、
これらのフィールドは非公開になっています。
Cacher::new
関数はジェネリックな引数のT
を取り、Cacher
構造体と同じトレイト境界を持つよう定義しました。
それからcalculation
フィールドに指定されたクロージャと、
value
フィールドにNone
値を保持するCacher
インスタンスをCacher::new
は返します。
まだクロージャを実行していないからですね。
呼び出し元のコードがクロージャの評価結果を必要としたら、クロージャを直接呼ぶ代わりに、value
メソッドを呼びます。
このメソッドは、結果の値がself.value
のSome
に既にあるかどうか確認します; そうなら、
クロージャを再度実行することなくSome
内の値を返します。
self.value
がNone
なら、コードはself.calculation
に保存されたクロージャを呼び出し、
結果を将来使えるようにself.value
に保存し、その値を返しもします。
リスト13-11は、リスト13-6の関数generate_workout
でこのCacher
構造体を使用する方法を示しています。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; struct Cacher<T> where T: Fn(u32) -> u32 { calculation: T, value: Option<u32>, } impl<T> Cacher<T> where T: Fn(u32) -> u32 { fn new(calculation: T) -> Cacher<T> { Cacher { calculation, value: None, } } fn value(&mut self, arg: u32) -> u32 { match self.value { Some(v) => v, None => { let v = (self.calculation)(arg); self.value = Some(v); v }, } } } fn generate_workout(intensity: u32, random_number: u32) { let mut expensive_result = Cacher::new(|num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }); if intensity < 25 { println!( "Today, do {} pushups!", expensive_result.value(intensity) ); println!( "Next, do {} situps!", expensive_result.value(intensity) ); } else { if random_number == 3 { println!("Take a break today! Remember to stay hydrated!"); } else { println!( "Today, run for {} minutes!", expensive_result.value(intensity) ); } } } }
クロージャを変数に直接保存する代わりに、クロージャを保持するCacher
の新規インスタンスを保存しています。
そして、結果が必要な場所それぞれで、そのCacher
インスタンスに対してvalue
メソッドを呼び出しています。
必要なだけvalue
メソッドを呼び出したり、全く呼び出さないこともでき、重い計算は最大でも1回しか走りません。
リスト13-2のmain
関数とともにこのプログラムを走らせてみてください。
simulated_user_specified_value
とsimulated_random_number
変数の値を変えて、
いろんなif
やelse
ブロックの場合全てで、calculating slowly
は1回だけ、必要な時にのみ出現することを実証してください。
必要以上に重い計算を呼び出さないことを保証するのに必要なロジックの面倒をCacher
は見るので、
generate_workout
はビジネスロジックに集中できるのです。
Cacher
実装の限界
値をキャッシュすることは、コードの他の部分でも異なるクロージャで行いたくなる可能性のある一般的に有用な振る舞いです。
しかし、現在のCacher
の実装には、他の文脈で再利用することを困難にしてしまう問題が2つあります。
1番目の問題は、Cacher
インスタンスが、常にvalue
メソッドの引数arg
に対して同じ値になると想定していることです。
言い換えると、Cacher
のこのテストは、失敗するでしょう:
#[test]
fn call_with_different_values() {
let mut c = Cacher::new(|a| a);
let v1 = c.value(1);
let v2 = c.value(2);
assert_eq!(v2, 2);
}
このテストは、渡された値を返すクロージャを伴うCacher
インスタンスを新しく生成しています。
このCacher
インスタンスに対して1というarg
値で呼び出し、それから2というarg
値で呼び出し、
2というarg
値のvalue
呼び出しは2を返すべきと期待しています。
このテストをリスト13-9とリスト13-10のCacher
実装で動かすと、assert_eq
からこんなメッセージが出て、
テストは失敗します:
thread 'call_with_different_values' panicked at 'assertion failed: `(left == right)`
left: `1`,
right: `2`', src/main.rs
問題は、初めてc.value
を1で呼び出した時に、Cacher
インスタンスはself.value
にSome(1)
を保存したことです。
その後value
メソッドに何を渡しても、常に1を返すわけです。
単独の値ではなく、ハッシュマップを保持するようにCacher
を改変してみてください。ハッシュマップのキーは、
渡されるarg
値になり、ハッシュマップの値は、そのキーでクロージャを呼び出した結果になるでしょう。
self.value
が直接Some
かNone
値であることを調べる代わりに、value
関数はハッシュマップのarg
を調べ、
存在するならその値を返します。存在しないなら、Cacher
はクロージャを呼び出し、
arg
値に紐づけてハッシュマップに結果の値を保存します。
現在のCacher
実装の2番目の問題は、引数の型にu32
を一つ取り、u32
を返すクロージャしか受け付けないことです。
例えば、文字列スライスを取り、usize
を返すクロージャの結果をキャッシュしたくなるかもしれません。
この問題を修正するには、Cacher
機能の柔軟性を向上させるためによりジェネリックな引数を導入してみてください。
クロージャで環境をキャプチャする
トレーニング生成の例においては、クロージャをインラインの匿名関数として使っただけでした。しかし、 クロージャには、関数にはない追加の能力があります: 環境をキャプチャし、 自分が定義されたスコープの変数にアクセスできるのです。
リスト13-12は、equal_to_x
変数に保持されたクロージャを囲む環境からx
変数を使用するクロージャの例です。
ファイル名: src/main.rs
fn main() { let x = 4; let equal_to_x = |z| z == x; let y = 4; assert!(equal_to_x(y)); }
ここで、x
はequal_to_x
の引数でもないのに、
equal_to_x
が定義されているのと同じスコープで定義されているx
変数をequal_to_x
クロージャは使用できています。
同じことを関数では行うことができません; 以下の例で試したら、コードはコンパイルできません:
ファイル名: src/main.rs
fn main() {
let x = 4;
fn equal_to_x(z: i32) -> bool { z == x }
let y = 4;
assert!(equal_to_x(y));
}
エラーが出ます:
error[E0434]: can't capture dynamic environment in a fn item; use the || { ...
} closure form instead
(エラー: fn要素では動的な環境をキャプチャできません; 代わりに|| { ... }のクロージャ形式を
使用してください)
--> src/main.rs
|
4 | fn equal_to_x(z: i32) -> bool { z == x }
| ^
コンパイラは、この形式はクロージャでのみ動作することさえも思い出させてくれています!
クロージャが環境から値をキャプチャすると、メモリを使用してクロージャ本体で使用できるようにその値を保存します。 このメモリ使用は、環境をキャプチャしないコードを実行するようなもっと一般的な場合には払いたくないオーバーヘッドです。 関数は、絶対に環境をキャプチャすることが許可されていないので、関数を定義して使えば、このオーバーヘッドを招くことは絶対にありません。
クロージャは、3つの方法で環境から値をキャプチャでき、この方法は関数が引数を取れる3つの方法に直に対応します:
所有権を奪う、可変で借用する、不変で借用するです。これらは、以下のように3つのFn
トレイトでコード化されています:
FnOnce
は、クロージャの環境として知られている内包されたスコープからキャプチャした変数を消費します。 キャプチャした変数を消費するために、定義された際にクロージャはこれらの変数の所有権を奪い、 自身にムーブするのです。名前のうち、Once
の部分は、 このクロージャは同じ変数の所有権を2回以上奪うことができないという事実を表しているので、1回しか呼ぶことができないのです。FnMut
は、可変で値を借用するので、環境を変更することができます。Fn
は、環境から値を不変で借用します。
クロージャを生成する時、クロージャが環境を使用する方法に基づいて、コンパイラはどのトレイトを使用するか推論します。
少なくとも1回は呼び出されるので、全てのクロージャはFnOnce
を実装しています。キャプチャした変数をムーブしないクロージャは、
FnMut
も実装し、キャプチャした変数に可変でアクセスする必要のないクロージャは、Fn
も実装しています。
リスト13-12では、equal_to_x
クロージャはx
を不変で借用しています(ゆえにequal_to_x
はFn
トレイトです)。
クロージャの本体は、x
を読む必要しかないからです。
環境でクロージャが使用している値の所有権を奪うことをクロージャに強制したいなら、引数リストの前にmove
キーワードを使用できます。
このテクニックは、新しいスレッドにデータが所有されるように、クロージャを新しいスレッドに渡して、
データをムーブする際に大概は有用です。
並行性について語る第16章で、move
クロージャの例はもっと多く出てきます。とりあえず、
こちらがmove
キーワードがクロージャ定義に追加され、整数の代わりにベクタを使用するリスト13-12からのコードです。
整数はムーブではなく、コピーされてしまいますからね; このコードはまだコンパイルできないことに注意してください。
ファイル名: src/main.rs
fn main() {
let x = vec![1, 2, 3];
let equal_to_x = move |z| z == x;
// ここでは、xを使用できません: {:?}
println!("can't use x here: {:?}", x);
let y = vec![1, 2, 3];
assert!(equal_to_x(y));
}
以下のようなエラーを受けます:
error[E0382]: use of moved value: `x`
(エラー: ムーブされた値の使用: `x`)
--> src/main.rs:6:40
|
4 | let equal_to_x = move |z| z == x;
| -------- value moved (into closure) here
(値はここで(クロージャに)ムーブされた)
5 |
6 | println!("can't use x here: {:?}", x);
| ^ value used here after move
(ムーブ後、値はここで使用された)
|
= note: move occurs because `x` has type `std::vec::Vec<i32>`, which does not
implement the `Copy` trait
(注釈: `x`が`std::vec::Vec<i32>`という`Copy`トレイトを実装しない型のため、ムーブが起きました)
クロージャが定義された際に、クロージャにx
の値はムーブされています。move
キーワードを追加したからです。
そして、クロージャはx
の所有権を持ち、main
がprintln!
でx
を使うことはもう叶わないのです。
println!
を取り除けば、この例は修正されます。
Fn
トレイトのどれかを指定するほとんどの場合、Fn
から始めると、コンパイラがクロージャ本体内で起こっていることにより、
FnMut
やFnOnce
が必要な場合、教えてくれるでしょう。
環境をキャプチャできるクロージャが関数の引数として有用な場面を説明するために、次のトピックに移りましょう: イテレータです。