クロージャ: 環境をキャプチャできる匿名関数
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 } }
リスト13-1: 実行に約2秒かかる架空の計算の代役を務める関数
次は、この例で重要なトレーニングアプリの部分を含む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) {}
リスト13-2: ユーザ入力や乱数生成をシミュレートするハードコードされた値があるmain関数
簡潔性のために、変数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: 入力に基づいてトレーニングプランを出力するビジネスロジックと、
simulated_expensive_calculation関数の呼び出し
リスト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 ); } } } }
リスト13-4: 複数のsimulated_expensive_calculationの呼び出しを1箇所に抽出し、
結果を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); }
リスト13-5: クロージャを定義し、expensive_closure変数に保存する
クロージャ定義が=に続き、変数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) ); } } } }
リスト13-6: 定義したexpensive_closureを呼び出す
今では、重い計算はたった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 }; }
リスト13-7: クロージャの引数と戻り値の省略可能な型注釈を追加する
型注釈を付け加えると、クロージャの記法は、関数の記法により酷似して見えます。以下が、引数に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);
リスト13-8: 2つの異なる型で型が推論されるクロージャの呼び出しを試みる
コンパイラは、次のエラーを返します:
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>, } }
リスト13-9: クロージャをcalculationに、オプションの結果値をvalueに保持するCacher構造体を定義する
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 }, } } } }
リスト13-10: Cacherのキャッシュ機構
呼び出し元のコードにこれらのフィールドの値を直接変えてもらうのではなく、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) ); } } } }
リスト13-11: generate_workout関数内でCacherを使用し、キャッシュ機構を抽象化する
クロージャを変数に直接保存する代わりに、クロージャを保持する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)); }
リスト13-12: 内包するスコープの変数を参照するクロージャの例
ここで、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が必要な場合、教えてくれるでしょう。
環境をキャプチャできるクロージャが関数の引数として有用な場面を説明するために、次のトピックに移りましょう: イテレータです。