クロージャ: 環境をキャプチャする匿名関数

Rustのクロージャは、変数に保存したり、引数として他の関数に渡すことのできる匿名関数です。 ある場所でクロージャを生成した後、他の場所でクロージャを呼び出して、別の文脈で評価することができます。 クロージャは関数と異なり、定義されたスコープから値をキャプチャすることができます。 これらのクロージャの特性を使用して、どのようにコードの再利用や振る舞いのカスタマイズができるか、実際に試してみましょう。

クロージャで環境をキャプチャする

まず、クロージャから定義された環境から後で使用するために値をキャプチャするために、 どのようにクロージャを使用することができるのか、確かめてみましょう。 このようなシナリオを考えます: 私たちのTシャツ会社は時折プロモーションとして、 メーリングリスト上の誰かに、その人専用の限定版のシャツを配ります。 メーリングリスト上の人たちは、自身のプロファイルにお気に入りの色を任意で追加することができます。 シャツ無料対象に選ばれた人がお気に入りの色を設定している場合は、その色のシャツをもらえます。 その人がお気に入りの色を指定していない場合は、会社が現在最も多く持っている色をもらえます。

これを実装する方法はたくさんあります。この例では、RedBlueの列挙子を持つ、 ShirtColorというenumを使用します(簡潔さのために入手可能な色の数は制限しています)。 会社の棚卸資産は、Vec<ShirtColor>を含み現在在庫にあるシャツの色を表現するshirtsというフィールドを持つ、 Inventory構造体で表現します。Inventory上に定義されたgiveawayメソッドは、 シャツ無料対象者の任意選択のシャツの色の好みを取得し、その人がもらえるシャツの色を返します。 この構築をリスト13-1に示します:

ファイル名: src/main.rs

#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        user_preference.unwrap_or_else(|| self.most_stocked())
    }

    fn most_stocked(&self) -> ShirtColor {
        let mut num_red = 0;
        let mut num_blue = 0;

        for color in &self.shirts {
            match color {
                ShirtColor::Red => num_red += 1,
                ShirtColor::Blue => num_blue += 1,
            }
        }
        if num_red > num_blue {
            ShirtColor::Red
        } else {
            ShirtColor::Blue
        }
    }
}

fn main() {
    let store = Inventory {
        shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
    };

    let user_pref1 = Some(ShirtColor::Red);
    let giveaway1 = store.giveaway(user_pref1);
    println!(
        // "好み{:?}を持つユーザは{:?}を得ます"
        "The user with preference {:?} gets {:?}",
        user_pref1, giveaway1
    );

    let user_pref2 = None;
    let giveaway2 = store.giveaway(user_pref2);
    println!(
        // "好み{:?}を持つユーザは{:?}を得ます"
        "The user with preference {:?} gets {:?}",
        user_pref2, giveaway2
    );
}

リスト13-1: シャツ会社の無料プレゼントのシチュエーション

mainで定義されているstoreには、この限定版プロモーションのために残された、 青いシャツが2着と赤いシャツが1着あります。赤いシャツが好みのユーザと、好みがないユーザを対象として、 giveawayメソッドを呼び出しています。

繰り返しになりますが、このコードは多くの方法で実装することができます。 ここではクロージャに着目するために、すでに学習した概念に固執しています。 ただし、giveawayメソッドの本体はクロージャを使用していて、例外です。 giveawayメソッドでは、ユーザの好みをOption<ShirtColor>型の引数として取り、 user_preferenceに対してunwrap_or_elseメソッドを呼び出します。 Option<T>に対するunwrap_or_elseメソッドは標準ライブラリで定義されています。 このメソッドは引数を1つ取ります: 値T(Option<T>Some列挙子内に保存されているのと同じ型、 この場合はShirtColor)を返す、引数のないクロージャです。 もしOption<T>Some列挙子の場合、unwrap_or_elseSomeの内側の値を返します。 もしOption<T>None列挙子の場合、unwrap_or_elseはクロージャを呼び出し、 そのクロージャによって返された値を返します。

unwrap_or_elseへの引数としてクロージャ式|| self.most_stocked()を指定しています。 これは引数を取らないクロージャです(クロージャが引数を持つ場合、それらは2本の縦線の間に現れます)。 クロージャの本体はself.most_stocked()を呼び出します。 クロージャはここで定義され、unwrap_or_elseの実装詳細がクロージャの結果を必要とするなら、 このクロージャを呼び出すでしょう。

このコードを実行すると、以下が出力されます:

$ cargo run
   Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
    Finished dev [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue

ここで興味深い側面は、現在のInventoryインスタンスに対してself.most_stocked()を呼び出すクロージャを渡していることです。 標準ライブラリは、プログラマが定義したInventoryShirtColor型や、 このシナリオで使用したいロジックについては何も知りません。 クロージャはself Inventoryインスタンスへの不変参照をキャプチャし、 この不変参照をプログラマが指定したコードとともにunwrap_or_elseメソッドに渡します。 一方で関数は、このように環境をキャプチャすることができません。

クロージャの型推論と注釈

関数とクロージャの違いは他にもあります。 クロージャでは通常、fn関数のように引数の型や戻り値の型を注釈する必要はありません。 関数では、型注釈は必要です。型はユーザに露出する明示的なインターフェイスの一部だからです。 このインターフェイスを堅実に定義することは、関数が使用したり、 返したりする値の型についてみんなが合意していることを保証するために重要なのです。 一方で、クロージャはこのような露出するインターフェイスには使用されません: 変数に保存され、 名前付けしたり、ライブラリの使用者に晒されることなく、使用されます。

クロージャは典型的には短く、あらゆる任意の筋書きではなく、狭い文脈でのみ関係します。 このような限定された文脈内では、コンパイラは、多くの変数の型を推論できるのと似たように、 引数や戻り値の型を推論することができます (コンパイラがクロージャ型注釈を要求するレアケースもあります)。

本当に必要な以上に冗長になることと引き換えに、明示性と明瞭性を向上させたいなら、 変数に型注釈を加えることもできます; クロージャに対する型注釈は、リスト13-2に示した定義のようになるでしょう。 この例では、リスト13-1でやったようにクロージャを定義したその場で引数として渡すのではなく、 クロージャを定義して変数に保存しています。

ファイル名: src/main.rs

use std::thread;
use std::time::Duration;

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num: u32| -> u32 {
        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)
            );
        }
    }
}

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(simulated_user_specified_value, simulated_random_number);
}

リスト13-2: クロージャの引数と戻り値の省略可能な型注釈を追加する

型注釈を付け加えると、クロージャの記法は関数の記法とより似た見た目になっていきます。 以下は比較のために、引数に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つの式からなるので、このかっこは省略可能です。 これらは全て、呼び出された時に同じ振る舞いになる合法な定義です。 型はその使い方から推論されるので、add_one_v3add_one_v4行がコンパイルできるようになるためには、 それを評価する必要があります。 これは、コンパイラが型を推論できるようにするために、型注釈かVecの中に挿入する何らかの型の値を必要とする、 let v = Vec::new();と似ています。

クロージャ定義では、コンパイラは引数それぞれと戻り値に対して、一つの具体的な型を推論します。 例えば、リスト13-3に引数として受け取った値を返すだけの短いクロージャの定義を示しました。 このクロージャは、この例での目的以外には有用ではありません。 この定義には、何も型注釈を加えていないことに注意してください。 型注釈が無いので、任意の型に対してこのクロージャを呼び出すことができ、 ここでは最初はStringに対して呼び出しています。 その後整数に対してexample_closureを呼び出そうとすると、エラーになります。

ファイル名: src/main.rs

fn main() {
    let example_closure = |x| x;

    let s = example_closure(String::from("hello"));
    let n = example_closure(5);
}

リスト13-3: 2つの異なる型で型が推論されるクロージャの呼び出しを試みる

コンパイラは、次のエラーを返します:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
(エラー: 型が合いません)
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |             --------------- ^- help: try using a conversion method: `.to_string()`
  |             |               | (ヘルプ: 変換メソッド`.to_string()`を使用してみてください)
  |             |               |
  |             |               expected `String`, found integer
  |             |              (`String`を予期していましたが、整数が見つかりました)
  |             arguments to this function are incorrect
  |            (この関数への引数が正しくありません)
  |
note: expected because the closure was earlier called with an argument of type `String`
(注釈: クロージャは以前に型`String`を持つ引数とともに呼ばれているため、これが予期されています)
 --> src/main.rs:4:29
  |
4 |     let s = example_closure(String::from("hello"));
  |             --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
  |             |                                    (この引数が型`String`を持つことから予期されています)
  |             |
  |             in this closure call
  |            (このクロージャ呼び出しで)
note: closure parameter defined here
 --> src/main.rs:2:28
  |
2 |     let example_closure = |x| x;
  |                            ^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error

String値でexample_closureを呼び出した最初の時点で、 コンパイラはxとクロージャの戻り値の型をStringと推論します。 そして、その型がexample_closureのクロージャに閉じ込められ、 次に同じクロージャを異なる型で使用しようとすると、型エラーが出るのです。

参照をキャプチャするか、所有権を移動するか

クロージャは、3つの方法で環境から値をキャプチャでき、この方法は関数が引数を取れる3つの方法に直に対応します: 不変で借用する、可変で借用する、所有権を奪うです。クロージャは、 関数本体がキャプチャされた値を使って何をするかに基づいて、これらのうちどれを使用するかを決定します。

リスト13-4では、listというベクタへの不変参照をキャプチャするクロージャが定義されます。 値を出力するためには不変参照しか必要としないからです:

ファイル名: src/main.rs

fn main() {
    let list = vec![1, 2, 3];
    //       "クロージャの定義前: {:?}"
    println!("Before defining closure: {:?}", list);

    //                             "クロージャから: {:?}"
    let only_borrows = || println!("From closure: {:?}", list);

    //       "クロージャの呼び出し前: {:?}"
    println!("Before calling closure: {:?}", list);
    only_borrows();
    //       "クロージャの呼び出し後: {:?}"
    println!("After calling closure: {:?}", list);
}

リスト13-4: 不変参照をキャプチャするクロージャを定義し、呼び出す

この例は、変数をクロージャ定義に束縛することができ、 その変数名が関数名であるかのように変数名と丸かっこを使用することで、 後でそのクロージャを呼び出すことができることも示しています。

listへの不変参照は複数同時に存在することができるので、listには、 クロージャ定義より前のコードからでも、クロージャ定義からクロージャ呼び出しまでの間のコードからでも、 クロージャ呼び出し後のコードからでも、アクセスすることができます。 このコードはコンパイルでき、実行でき、以下を出力します:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]

次にリスト13-5では、listベクタへ要素を追加するようにクロージャ本体を変更します。 クロージャは可変参照をキャプチャするようになります:

ファイル名: src/main.rs

fn main() {
    let mut list = vec![1, 2, 3];
    //       "クロージャの定義前: {:?}"
    println!("Before defining closure: {:?}", list);

    let mut borrows_mutably = || list.push(7);

    borrows_mutably();
    //       "クロージャの呼び出し後: {:?}"
    println!("After calling closure: {:?}", list);
}

リスト13-5: 可変参照をキャプチャするクロージャを定義し、呼び出す

このコードはコンパイルでき、実行でき、以下を出力します:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]

borrows_mutablyクロージャの定義と呼び出しの間のprintln!を消したことに注意してください: borrows_mutablyが定義されるときに、listへの可変参照がキャプチャされます。 クロージャを呼び出した後は、クロージャを再度使用していないので、可変借用は終了します。 可変借用が存在するときに他の借用は許可されないので、 クロージャ定義とクロージャ呼び出しの間に、出力するための不変借用は許可されません。 println!を追加してみて、どんなエラーメッセージが得られるか確認してみてください!

クロージャの本体が厳密には所有権を必要としない場合であっても、 クロージャにそれが使用する値の所有権を環境から奪うように強制したい場合は、 引数リストの前でmoveキーワードを使用することができます。

この手法は主に、新しいスレッドにクロージャを渡すときに、 データが新しいスレッドに所有されるようにムーブするために有用です。 スレッドについて、そしてなぜスレッドを使用するのかについては、 第16章で並行性について語るときに詳しく議論しますが、今のところは、 moveキーワードが必要なクロージャを使って新しいスレッドを生成することについて簡単に探検してみましょう。 リスト13-6に、メインスレッド内ではなく新しいスレッド内でベクタを出力するようにリスト13-4を変更したものを示します:

ファイル名: src/main.rs

use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    //       "クロージャの定義前: {:?}"
    println!("Before defining closure: {:?}", list);

    //                             "スレッドから: {:?}"
    thread::spawn(move || println!("From thread: {:?}", list))
        .join()
        .unwrap();
}

リスト13-6: moveを使用して、スレッドのためのクロージャにlistの所有権を奪わせる

実行するクロージャを引数としてスレッドに与えて、新しいスレッドを生成します。 クロージャ本体はリストを表示します。 リスト13-4では、クロージャは不変参照を使ってlistをキャプチャしているだけでした。 それがlistを出力するために必要な最小のアクセスだからです。 この例では、クロージャ本体はやはり不変参照のみを要求しますが、 クロージャ定義の先頭にmoveキーワードを置くことで、 listをクロージャ内にムーブすべきだと指定する必要があります。 新しいスレッドはメインスレッドの残りの部分が終了するより前に終了するかもしれませんし、 メインスレッドが先に終了するかもしれません。 もしメインスレッドがlistの所有権を持ち続け、新しいスレッドが終了する前に終了してlistをドロップしてしまうと、 スレッド内の不変参照は無効になるでしょう。そのためコンパイラは、参照が有効になるように、 新しいスレッドに渡されるクロージャにlistがムーブされることを要求します。 moveキーワードを削除したり、メインスレッド内でクロージャが定義された後にlistを使用したりしてみて、 どんなコンパイルエラーが得られるか確認してみてください!

キャプチャされた値のクロージャからのムーブと、Fn系トレイト

クロージャが参照をキャプチャしたり、クロージャが定義されている環境から値の所有権をキャプチャする (つまりクロージャの中に何をムーブするかに影響を与えます)と、クロージャの本体内のコードは、 後でクロージャが評価されたときにその参照または値に何が起こるかを定義します (つまりクロージャの外に何をムーブするかに影響を与えます)。 クロージャ本体は以下のいずれかを行うことができます: キャプチャされた値をクロージャの外にムーブしたり、 キャプチャされた値を変更したり、値をムーブも変更もしないでいたり、そもそも環境から何もキャプチャしないかです。

クロージャがどのように環境から値をキャプチャして使用するかは、クロージャがどのトレイトを実装するかに影響を及ぼします。 トレイトは、関数や構造体がどんな種類のクロージャを使用できるかを指定するための方法として使用されます。 クロージャは、その本体はどのように値を使用するかに応じて、以下のFn系トレイトのうち1つ、2つ、または3つすべてを、付加的に自動的に実装します:

  1. FnOnceは、一度呼び出すことができるクロージャに適用されます。 すべてのクロージャは、呼び出すことができるので、最低でもこのトレイトを実装します。 キャプチャされた値を本体の外にムーブするクロージャは、一度しか呼び出すことができないので、 FnOnceのみを実装し、他のFn系トレイト群を実装しないことになるでしょう。
  2. FnMutは、キャプチャされた値を本体の外にムーブしないが、キャプチャされた値を変更するかもしれないクロージャに適用されます。 これらのクロージャは複数回呼び出すことができます。
  3. Fnは、キャプチャされた値を本体の外にムーブせず、キャプチャされた値を変更しないクロージャと、 環境から何もキャプチャしないクロージャに適用されます。これらのクロージャは環境を変更することなく複数回呼び出すことができ、 クロージャを並行して複数回呼び出す場合などに重要です。

リスト13-1で使用した、Option<T>に対するunwrap_or_elseメソッドの定義を見てみましょう:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

TOptionSome列挙子内の値の型を表現するジェネリック型であることを思い出してください。 型Tunwrap_or_else関数の戻り値型でもあります: 例えばOption<String>に対してunwrap_or_elseを呼び出すコードは、Stringを得るでしょう。

次に、unwrap_or_else関数は追加のジェネリック型引数Fを持つことに注目してください。 型Fは引数fの型で、funwrap_or_elseを呼び出すときにこちらから提供するクロージャです。

FnOnce() -> Tはジェネリック型Fに対して指定されているトレイト境界で、これは、 Fは一度呼び出すことができ、引数を取らず、Tを返すことを意味します。 トレイト境界でFnOnceを使用することは、unwrap_or_elsefを多くとも1回しか呼び出さないという制約を表現します。 unwrap_or_elseの本体の中では、OptionSomeの場合はfは呼ばれないことがわかるでしょう。 OptionNoneの場合は、fは一度呼び出されるでしょう。すべてのクロージャはFnOnceを実装するので、 unwrap_or_elseは最も多くの種類のクロージャを受け入れ、最も柔軟です。

注釈: 関数も3つのFn系トレイト全部を実装することができます。もし環境から値をキャプチャする必要がなければ、 Fn系トレイトのいずれかを実装する何かが必要になるクロージャではなく、関数の名前を使用できます。 例えば、Option<Vec<T>>値に対して、その値がNoneである場合には新しい空のベクタを得るためには、 unwrap_or_else(Vec::new)と呼び出すことができます。

それでは、スライスに対して定義されている標準ライブラリメソッドsort_by_keyを見てみて、 unwrap_or_elseとどう違うのか、sort_by_keyはなぜトレイト境界にFnOnceではなくFnMutを使用するのか、 確認してみましょう。このクロージャは、処理対象のスライス内の現在の要素への参照という形で1つの引数を取り、 順序付けることができるK型の値を返します。この関数は、 スライスを各要素の特定の属性によってソートしたいときに便利です。 リスト13-7では、Rectangleインスタンスからなるリストがあり、 width属性によってその各要素を昇順に並び替えるために、sort_by_keyを使用しています:

ファイル名: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    list.sort_by_key(|r| r.width);
    println!("{:#?}", list);
}

リスト13-7: sort_by_keyを使用して、幅で長方形を順序付ける

このコードは以下を出力します:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/rectangles`
[
    Rectangle {
        width: 3,
        height: 5,
    },
    Rectangle {
        width: 7,
        height: 12,
    },
    Rectangle {
        width: 10,
        height: 1,
    },
]

sort_by_keyFnMutクロージャを取るように定義されている理由は、クロージャを複数回呼び出すからです: スライス内の各要素ごとに一度呼び出されます。クロージャ|r| r.width は、環境から何もキャプチャ、 変更、ムーブしないので、トレイト境界の要求を満たしています。

対照的にリスト13-8では、環境から値をムーブすることから、 FnOnceトレイトだけを実装するクロージャの例を示しています。 コンパイラは、sort_by_keyでこのクロージャを使わせてくれないでしょう:

ファイル名: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut sort_operations = vec![];
    //                       "by keyが呼ばれました"
    let value = String::from("by key called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{:#?}", list);
}

リスト13-8: sort_by_keyFnOnceクロージャを使用しようとする

これは、listをソートするときにsort_by_keyが呼ばれる回数を数えようとする、 人為的で複雑な(しかも動かない)方法です。このコードは、 sort_operationsベクタにvalue—クロージャの環境からのString—をプッシュすることで、 このカウントを行おうとします。このクロージャはvalueをキャプチャし、 valueの所有権をsort_operationsベクタに移転することで、valueをクロージャの外にムーブします。 このクロージャは一度呼び出すことができます; 2回目の呼び出しをしようとしても、 sort_operationsにもう一度プッシュするためのvalueはもう環境にないので、うまくいかないでしょう! そのため、このクロージャはFnOnceのみを実装します。 このコードをコンパイルしようとすると、クロージャがFnMutを実装していなくてはならないので、 valueはクロージャの外にムーブすることができない、というエラーが得られます:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
(エラー: `FnMut`クロージャ内にキャプチャされた変数`value`からムーブすることはできません)
  --> src/main.rs:18:30
   |
15 |     let value = String::from("by key called");
   |         ----- captured outer variable
   |              (キャプチャされる外側の変数)
16 |
17 |     list.sort_by_key(|r| {
   |                      --- captured by this `FnMut` closure
   |                         (この`FnMut`クロージャによってキャプチャされています)
18 |         sort_operations.push(value);
   |                              ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait
   |                                   (`value`は型`String`を持ち、`Copy`トレイトを実装しないので、ムーブが発生します)

For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error

エラーは、クロージャ本体内で、valueを環境からムーブする行を指しています。 これを修正するためには、値を環境からムーブしないようにクロージャ本体を変更する必要があります。 sort_by_keyが呼ばれる回数を数えるためには、環境内でカウンタを保持し、 クロージャ本体の中でその値をインクリメントするのが、より自然な方法です。 リスト13-9のクロージャは、num_sort_operationsカウンタへの可変参照をキャプチャするだけで、 そのため複数回呼び出すことができるので、sort_by_keyで使うことができます:

ファイル名: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut num_sort_operations = 0;
    list.sort_by_key(|r| {
        num_sort_operations += 1;
        r.width
    });
    //       "{:#?}、{num_sort_operations}回の操作でソートされました"
    println!("{:#?}, sorted in {num_sort_operations} operations", list);
}

リスト13-9: sort_by_keyFnMutクロージャを使用することは許可される

クロージャを使用する関数や型を定義または使用するときに、Fn系トレイトは重要です。 次の節では、イテレータについて議論します。多くのイテレータメソッドはクロージャ引数を取るので、 続けるにあたって、これらのクロージャの詳細のことを覚えておいてください!