Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Closure

Closure ของ Rust คือฟังก์ชัน anonymous ที่คุณบันทึกในตัวแปรหรือส่ง เป็นอาร์กิวเมนต์ให้ฟังก์ชันอื่นได้ คุณสร้าง closure ในที่หนึ่งและ เรียก closure ที่อื่นเพื่อประเมินมันใน context ที่ต่างได้ ต่างจาก ฟังก์ชัน closure จับค่าจาก scope ที่พวกมันถูกนิยามได้ เราจะสาธิตว่า ฟีเจอร์ closure เหล่านี้อนุญาตให้ใช้โค้ดซ้ำและกำหนดพฤติกรรมเองได้ อย่างไร

จับ Environment

เราจะตรวจสอบก่อนว่าเราใช้ closure จับค่าจาก environment ที่พวกมันถูก นิยามเพื่อใช้ภายหลังได้อย่างไร นี่คือ scenario — เป็นบางครั้ง บริษัท T-shirt ของเราแจกเสื้อ limited-edition พิเศษให้ใครบางคนใน mailing list ของเราเป็นการ promotion คนใน mailing list ใส่สีโปรดของพวกเขาใน profile ได้ ถ้าคนที่ถูกเลือกสำหรับเสื้อฟรีมีสีโปรดที่ตั้งไว้ พวกเขา จะได้เสื้อสีนั้น ถ้าคนนั้นไม่ได้ระบุสีโปรด พวกเขาจะได้สีอะไรก็ตามที่ บริษัทมีมากที่สุดในปัจจุบัน

มีวิธี implement สิ่งนี้หลายอย่าง สำหรับตัวอย่างนี้ เราจะใช้ enum ชื่อ ShirtColor ที่มี variant Red และ Blue (จำกัดจำนวนสีที่มี ให้ใช้เพื่อความง่าย) เราแทน inventory ของบริษัทด้วย struct Inventory ที่มี field ชื่อ shirts ที่บรรจุ Vec<ShirtColor> ที่ แทนสีเสื้อที่มีใน stock ปัจจุบัน เมธอด giveaway ที่นิยามบน Inventory รับค่าความชอบสีเสื้อที่เป็น optional ของผู้ชนะเสื้อฟรี และมัน return สีเสื้อที่คนจะได้ การ setup นี้แสดงใน Listing 13-1

Filename: 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
    );
}
Listing 13-1: สถานการณ์แจกเสื้อของบริษัท

store ที่นิยามใน main มีเสื้อสีน้ำเงินสองตัวและเสื้อสีแดงหนึ่ง ตัวที่เหลือสำหรับแจกใน promotion limited-edition นี้ เราเรียกเมธอด giveaway สำหรับ user ที่มีความชอบเสื้อสีแดงและ user ที่ไม่มีความ ชอบ

อีกครั้ง โค้ดนี้ implement ได้หลายวิธี และที่นี่เพื่อโฟกัสที่ closure เราติดกับแนวคิดที่คุณได้เรียนมาแล้ว ยกเว้น body ของเมธอด giveaway ที่ใช้ closure ในเมธอด giveaway เรารับความชอบของ user เป็น parameter type Option<ShirtColor> และเรียกเมธอด unwrap_or_else บน user_preference เมธอด unwrap_or_else บน Option<T> นิยามโดย standard library มันรับหนึ่งอาร์กิวเมนต์ — closure ที่ไม่มีอาร์กิวเมนต์ที่ return ค่า T (type เดียวกับที่เก็บใน variant Some ของ Option<T> ในกรณีนี้ คือ ShirtColor) ถ้า Option<T> เป็น variant Some unwrap_or_else return ค่าจากภายใน Some ถ้า Option<T> เป็น variant None unwrap_or_else เรียก closure และ return ค่าที่ return โดย closure

เราระบุ closure expression || self.most_stocked() เป็นอาร์กิวเมนต์ ให้ unwrap_or_else นี่คือ closure ที่ไม่รับ parameter เอง (ถ้า closure มี parameter พวกมันจะปรากฏระหว่าง vertical pipe สองตัว) body ของ closure เรียก self.most_stocked() เรากำลังนิยาม closure ที่นี่ และ implementation ของ unwrap_or_else จะประเมิน closure ภายหลังเมื่อจำเป็น

การรันโค้ดนี้ print ต่อไปนี้:

$ cargo run
   Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
    Finished `dev` profile [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

แง่มุมที่น่าสนใจที่นี่คือเราได้ส่ง closure ที่เรียก self.most_stocked() บน instance Inventory ปัจจุบัน standard library ไม่ต้องรู้อะไรเกี่ยวกับ type Inventory หรือ ShirtColor ที่เรานิยาม หรือ logic ที่เราต้องการใช้ใน scenario นี้ closure จับ immutable reference ของ self instance Inventory และส่งมันพร้อม โค้ดที่เราระบุให้เมธอด unwrap_or_else ฟังก์ชัน ในทางกลับกัน จับ environment ของพวกมันในแบบนี้ไม่ได้

Infer และ Annotate Type ของ Closure

มีความแตกต่างมากกว่าระหว่างฟังก์ชันและ closure โดยปกติ closure ไม่ ต้องให้คุณ annotate type ของ parameter หรือค่า return เหมือนที่ ฟังก์ชัน fn ต้อง type annotation จำเป็นบนฟังก์ชันเพราะ type เป็น ส่วนหนึ่งของ interface ชัดเจนที่ expose ให้ user ของคุณ การนิยาม interface นี้อย่างเข้มงวดสำคัญในการรับประกันว่าทุกคนเห็นด้วยกับ type ของค่าที่ฟังก์ชันใช้และ return closure ในทางกลับกัน ไม่ถูกใช้ใน interface ที่ expose แบบนี้ — พวกมันถูกเก็บในตัวแปร และถูกใช้โดยไม่ ต้องตั้งชื่อและ expose พวกมันให้ user ของ library เรา

Closure มักสั้นและเกี่ยวข้องเฉพาะใน context แคบ ๆ ไม่ใช่ใน scenario ตามอำเภอใจใด ๆ ภายใน context จำกัดเหล่านี้ compiler infer type ของ parameter และ return type ได้ คล้ายกับวิธีที่มัน infer type ของตัวแปร ส่วนใหญ่ได้ (มีกรณีที่หายากที่ compiler ต้องการ closure type annotation ด้วย)

เช่นเดียวกับตัวแปร เราเพิ่ม type annotation ได้ถ้าเราต้องการเพิ่ม ความชัดเจนแต่ค่าคือ verbose มากกว่าที่จำเป็นจริง ๆ การ annotate type สำหรับ closure จะดูเหมือนนิยามที่แสดงใน Listing 13-2 ในตัวอย่าง นี้ เรากำลังนิยาม closure และเก็บมันในตัวแปร ไม่ใช่นิยาม closure ในจุดที่เราส่งมันเป็นอาร์กิวเมนต์ ดังที่เราทำใน Listing 13-1

Filename: 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);
}
Listing 13-2: เพิ่ม type annotation ที่เป็น optional ของ parameter และ type ค่า return ใน closure

ด้วย type annotation ที่เพิ่ม syntax ของ closure ดูคล้ายกับ syntax ของฟังก์ชันมากขึ้น ที่นี่ เรานิยามฟังก์ชันที่เพิ่ม 1 ให้ parameter ของมัน และ closure ที่มีพฤติกรรมเดียวกัน เพื่อเปรียบเทียบ เราเพิ่ม ช่องว่างเพื่อจัดส่วนที่เกี่ยวข้องให้ตรงกัน นี่แสดงว่า syntax closure คล้ายกับ syntax ฟังก์ชันอย่างไร ยกเว้นการใช้ pipe และจำนวน syntax ที่เป็น optional:

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  ;

บรรทัดแรกแสดงนิยามฟังก์ชัน และบรรทัดที่สองแสดงนิยาม closure ที่ annotate เต็ม ในบรรทัดที่สาม เราลบ type annotation จากนิยาม closure ในบรรทัดที่สี่ เราลบวงเล็บ ซึ่งเป็น optional เพราะ body ของ closure มีเพียง expression เดียว เหล่านี้เป็นนิยามที่ valid ทั้งหมดที่จะ สร้างพฤติกรรมเดียวกันเมื่อถูกเรียก บรรทัด add_one_v3 และ add_one_v4 ต้องการให้ closure ถูกประเมินเพื่อให้สามารถคอมไพล์ได้ เพราะ type จะถูก infer จากการใช้พวกมัน นี่คล้ายกับ let v = Vec::new(); ที่ต้องการ type annotation หรือค่าของบาง type ที่ถูก insert เข้า Vec เพื่อให้ Rust สามารถ infer type ได้

สำหรับนิยาม closure compiler จะ infer type คอนกรีตหนึ่งสำหรับแต่ละ parameter และสำหรับค่า return ของพวกมัน ตัวอย่างเช่น Listing 13-3 แสดงนิยามของ closure สั้นที่แค่ return ค่าที่มันรับเป็น parameter closure นี้ไม่มีประโยชน์มากยกเว้นเพื่อจุดประสงค์ของตัวอย่างนี้ สังเกตว่าเราไม่ได้เพิ่ม type annotation ใด ๆ ในนิยาม เพราะไม่มี type annotation เราเรียก closure ด้วย type ใดก็ได้ ซึ่งเราทำที่นี่ด้วย String ในครั้งแรก ถ้าเราจากนั้นพยายามเรียก example_closure ด้วย integer เราจะได้ error

Filename: src/main.rs
fn main() {
    let example_closure = |x| x;

    let s = example_closure(String::from("hello"));
    let n = example_closure(5);
}
Listing 13-3: พยายามเรียก closure ที่ type ของมันถูก infer ด้วยสอง type ต่างกัน

compiler ให้ error นี้แก่เรา:

$ 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);
  |             --------------- ^ expected `String`, found integer
  |             |
  |             arguments to this function are incorrect
  |
note: expected because the closure was earlier called with an argument of type `String`
 --> src/main.rs:4:29
  |
4 |     let s = example_closure(String::from("hello"));
  |             --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
  |             |
  |             in this closure call
note: closure parameter defined here
 --> src/main.rs:2:28
  |
2 |     let example_closure = |x| x;
  |                            ^
help: try using a conversion method
  |
5 |     let n = example_closure(5.to_string());
  |                              ++++++++++++

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

ครั้งแรกที่เราเรียก example_closure ด้วยค่า String compiler infer type ของ x และ return type ของ closure เป็น String type เหล่านั้นจากนั้นถูก lock เข้า closure ใน example_closure และเรา ได้ error type เมื่อเราพยายามใช้ type ต่างกับ closure เดียวกัน ครั้งถัดไป

จับ Reference หรือย้าย Ownership

Closure จับค่าจาก environment ของพวกมันได้สามวิธี ซึ่ง map ตรงกับ สามวิธีที่ฟังก์ชันรับ parameter — borrow immutably, borrow mutably และรับ ownership closure จะตัดสินว่าใช้วิธีไหนตามสิ่งที่ body ของ ฟังก์ชันทำกับค่าที่จับ

ใน Listing 13-4 เรานิยาม closure ที่จับ immutable reference ของ vector ชื่อ list เพราะมันต้องการเพียง immutable reference เพื่อ print ค่า

Filename: 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:?}");
}
Listing 13-4: นิยามและเรียก closure ที่จับ immutable reference

ตัวอย่างนี้ยังแสดงว่าตัวแปร bind กับนิยาม closure ได้ และเราเรียก closure ภายหลังโดยใช้ชื่อตัวแปรและวงเล็บเหมือนกับชื่อตัวแปรเป็นชื่อ ฟังก์ชันได้

เพราะเรามี immutable reference หลายตัวของ list พร้อมกันได้ list ยังเข้าถึงได้จากโค้ดก่อนนิยาม closure, หลังนิยาม closure แต่ก่อน closure ถูกเรียก และหลัง closure ถูกเรียก โค้ดนี้คอมไพล์ รัน และ print:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished `dev` profile [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]

ถัดไป ใน Listing 13-5 เราเปลี่ยน body ของ closure เพื่อให้มันเพิ่ม element ให้ vector list closure ตอนนี้จับ mutable reference

Filename: 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:?}");
}
Listing 13-5: นิยามและเรียก closure ที่จับ mutable reference

โค้ดนี้คอมไพล์ รัน และ print:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished `dev` profile [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]

สังเกตว่าไม่มี println! ระหว่างนิยามและการเรียกของ closure borrows_mutably อีกแล้ว — เมื่อ borrows_mutably ถูกนิยาม มันจับ mutable reference ของ list เราไม่ใช้ closure อีกหลัง closure ถูก เรียก ดังนั้น mutable borrow จบ ระหว่างนิยาม closure และการเรียก closure immutable borrow เพื่อ print ไม่ได้รับอนุญาต เพราะไม่มี borrow อื่นที่อนุญาตเมื่อมี mutable borrow ลองเพิ่ม println! ที่นั่น เพื่อดูว่าคุณจะได้ข้อความ error อะไร!

ถ้าคุณต้องการบังคับให้ closure รับ ownership ของค่าที่มันใช้ใน environment แม้ว่า body ของ closure ไม่ได้ต้องการ ownership อย่าง เคร่งครัด คุณใช้ keyword move ก่อน list parameter ได้

เทคนิคนี้มีประโยชน์มากที่สุดเมื่อส่ง closure ให้เธรดใหม่เพื่อย้าย ข้อมูลให้มันถูก own โดยเธรดใหม่ เราจะพูดถึงเธรดและทำไมคุณจะต้องการ ใช้พวกมันในรายละเอียดในบทที่ 16 เมื่อเราพูดถึง concurrency แต่ตอนนี้ มาสำรวจสั้น ๆ เกี่ยวกับ spawn เธรดใหม่โดยใช้ closure ที่ต้องการ keyword move Listing 13-6 แสดง Listing 13-4 ที่แก้ให้ print vector ในเธรดใหม่ไม่ใช่ในเธรดหลัก

Filename: 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();
}
Listing 13-6: ใช้ move เพื่อบังคับให้ closure สำหรับเธรดรับ ownership ของ list

เรา spawn เธรดใหม่ ให้เธรด closure เพื่อรันเป็นอาร์กิวเมนต์ body ของ closure print list ออก ใน Listing 13-4 closure จับ list โดย ใช้ immutable reference เท่านั้นเพราะนั่นเป็นจำนวนสิทธิ์เข้าถึงน้อย ที่สุดของ list ที่ต้องการเพื่อ print มัน ในตัวอย่างนี้ แม้ว่า body ของ closure ยังต้องการเพียง immutable reference เราต้องระบุว่า list ควรถูกย้ายเข้า closure โดยใส่ keyword move ที่จุดเริ่มต้น ของนิยาม closure ถ้าเธรดหลักทำ operation มากขึ้นก่อนเรียก join บนเธรดใหม่ เธรดใหม่อาจเสร็จก่อนส่วนที่เหลือของเธรดหลักเสร็จ หรือเธรด หลักอาจเสร็จก่อน ถ้าเธรดหลักรักษา ownership ของ list แต่จบก่อน เธรดใหม่และ drop list immutable reference ในเธรดจะไม่ valid ดังนั้น compiler ต้องการให้ list ถูกย้ายเข้า closure ที่ให้เธรด ใหม่เพื่อให้ reference จะ valid ลองลบ keyword move หรือใช้ list ในเธรดหลักหลัง closure ถูกนิยามเพื่อดูว่าคุณจะได้ error ของ compiler อะไร!

ย้ายค่าที่จับออกจาก Closure

เมื่อ closure ได้จับ reference หรือจับ ownership ของค่าจาก environment ที่ closure ถูกนิยาม (ดังนั้นกระทบสิ่งที่ถ้ามี ถูกย้าย_เข้า_ closure) โค้ดใน body ของ closure นิยามสิ่งที่เกิดขึ้นกับ reference หรือค่า เมื่อ closure ถูกประเมินภายหลัง (ดังนั้นกระทบสิ่งที่ถ้ามี ถูกย้าย ออกจาก closure)

body ของ closure ทำสิ่งใดต่อไปนี้ได้ — ย้ายค่าที่จับออกจาก closure, mutate ค่าที่จับ, ทั้งไม่ย้ายและไม่ mutate ค่า หรือไม่จับอะไรจาก environment ตั้งแต่ต้น

วิธีที่ closure จับและจัดการค่าจาก environment กระทบ trait ที่ closure implement และ trait คือวิธีที่ฟังก์ชันและ struct ระบุว่า ประเภทของ closure ไหนที่พวกมันใช้ได้ closure จะ implement หนึ่ง, สอง หรือทั้งสาม trait Fn เหล่านี้อัตโนมัติในแบบ additive ขึ้นกับวิธีที่ body ของ closure จัดการค่า:

  • FnOnce ใช้กับ closure ที่ถูกเรียกได้ครั้งเดียว closure ทั้งหมด implement อย่างน้อย trait นี้เพราะ closure ทั้งหมดถูกเรียกได้ closure ที่ย้ายค่าที่จับออกจาก body ของมันจะ implement เฉพาะ FnOnce และไม่ implement trait Fn อื่นเพราะมันถูกเรียกได้เพียง ครั้งเดียว
  • FnMut ใช้กับ closure ที่ไม่ย้ายค่าที่จับออกจาก body แต่อาจ mutate ค่าที่จับ closure เหล่านี้ถูกเรียกได้มากกว่าหนึ่งครั้ง
  • Fn ใช้กับ closure ที่ไม่ย้ายค่าที่จับออกจาก body และไม่ mutate ค่าที่จับ รวมถึง closure ที่ไม่จับอะไรจาก environment ของพวกมัน closure เหล่านี้ถูกเรียกได้มากกว่าหนึ่งครั้งโดยไม่ mutate environment ของพวกมัน ซึ่งสำคัญในกรณีเช่นการเรียก closure หลาย ครั้งพร้อมกัน

มาดูนิยามของเมธอด unwrap_or_else บน Option<T> ที่เราใช้ใน Listing 13-1:

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(),
        }
    }
}

จำได้ว่า T เป็น generic type ที่แทน type ของค่าใน variant Some ของ Option type T นั้นยังเป็น return type ของฟังก์ชัน unwrap_or_else — โค้ดที่เรียก unwrap_or_else บน Option<String> ตัวอย่างเช่น จะได้ String

ถัดไป สังเกตว่าฟังก์ชัน unwrap_or_else มี parameter generic type เพิ่ม F type F เป็น type ของ parameter ชื่อ f ซึ่งเป็น closure ที่เราให้เมื่อเรียก unwrap_or_else

trait bound ที่ระบุบน generic type F คือ FnOnce() -> T ซึ่งหมาย ความว่า F ต้องสามารถถูกเรียกครั้งเดียว ไม่รับอาร์กิวเมนต์ และ return T การใช้ FnOnce ใน trait bound แสดงข้อจำกัดว่า unwrap_or_else จะไม่เรียก f มากกว่าหนึ่งครั้ง ใน body ของ unwrap_or_else เราเห็นว่าถ้า Option เป็น Some f จะไม่ถูก เรียก ถ้า Option เป็น None f จะถูกเรียกครั้งเดียว เพราะ closure ทั้งหมด implement FnOnce unwrap_or_else รับ closure ทั้ง สามประเภทและยืดหยุ่นมากที่สุดเท่าที่ทำได้

สังเกต — ถ้าสิ่งที่เราต้องการทำไม่ต้องการการจับค่าจาก environment เราใช้ชื่อของฟังก์ชันแทน closure ที่เราต้องการอะไรบางอย่างที่ implement หนึ่งของ trait Fn ได้ ตัวอย่างเช่น บนค่า Option<Vec<T>> เราเรียก unwrap_or_else(Vec::new) เพื่อได้ vector ว่างใหม่ถ้าค่าเป็น None compiler implement trait Fn ใด ที่ใช้ได้สำหรับนิยามฟังก์ชันอัตโนมัติ

ตอนนี้มาดูเมธอด standard library sort_by_key ที่นิยามบน slice เพื่อ ดูว่ามันต่างจาก unwrap_or_else อย่างไรและทำไม sort_by_key ใช้ FnMut แทน FnOnce สำหรับ trait bound closure รับหนึ่งอาร์กิวเมนต์ ในรูปแบบของ reference ของ item ปัจจุบันใน slice ที่กำลังถูกพิจารณา และมัน return ค่า type K ที่ ordered ได้ ฟังก์ชันนี้มีประโยชน์เมื่อ คุณต้องการ sort slice ตาม attribute เฉพาะของแต่ละ item ใน Listing 13-7 เรามี list ของ instance Rectangle และเราใช้ sort_by_key เพื่อ order พวกมันตาม attribute width ของพวกมันจากต่ำไปสูง

Filename: 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:#?}");
}
Listing 13-7: ใช้ sort_by_key เพื่อ order rectangle ตามความกว้าง

โค้ดนี้ print:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [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_key นิยามให้รับ closure FnMut คือมันเรียก closure หลายครั้ง — ครั้งหนึ่งสำหรับแต่ละ item ใน slice closure |r| r.width ไม่จับ, mutate หรือย้ายอะไรจาก environment ของมัน ดังนั้นมันตรงข้อกำหนด trait bound

ในทางตรงข้าม Listing 13-8 แสดงตัวอย่างของ closure ที่ implement เฉพาะ trait FnOnce เพราะมันย้ายค่าออกจาก environment compiler จะไม่ให้เราใช้ closure นี้กับ sort_by_key

Filename: 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![];
    let value = String::from("closure called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{list:#?}");
}
Listing 13-8: พยายามใช้ closure FnOnce กับ sort_by_key

นี่เป็นวิธีที่ประดิษฐ์และสับสน (ที่ไม่ทำงาน) ที่จะพยายามนับจำนวน ครั้งที่ sort_by_key เรียก closure เมื่อ sort list โค้ดนี้ พยายามทำการนับนี้โดย push valueString จาก environment ของ closure — เข้า vector sort_operations closure จับ value และ ย้าย value ออกจาก closure โดยโอน ownership ของ value ให้ vector sort_operations closure นี้ถูกเรียกได้ครั้งเดียว — การ พยายามเรียกมันครั้งที่สองจะไม่ทำงาน เพราะ value จะไม่อยู่ใน environment ที่จะถูก push เข้า sort_operations อีก! ดังนั้น closure นี้ implement เฉพาะ FnOnce เมื่อเราพยายามคอมไพล์โค้ดนี้ เราได้ error ว่า value ย้ายออกจาก closure ไม่ได้เพราะ closure ต้อง implement FnMut:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
  --> src/main.rs:18:30
   |
15 |     let value = String::from("closure called");
   |         -----   ------------------------------ move occurs because `value` has type `String`, which does not implement the `Copy` trait
   |         |
   |         captured outer variable
16 |
17 |     list.sort_by_key(|r| {
   |                      --- captured by this `FnMut` closure
18 |         sort_operations.push(value);
   |                              ^^^^^ `value` is moved here
   |
help: consider cloning the value if the performance cost is acceptable
   |
18 |         sort_operations.push(value.clone());
   |                                   ++++++++

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

error ชี้ไปยังบรรทัดใน body ของ closure ที่ย้าย value ออกจาก environment เพื่อแก้ เราต้องเปลี่ยน body ของ closure ให้มันไม่ย้าย ค่าออกจาก environment การเก็บ counter ใน environment และเพิ่ม ค่าของมันใน body ของ closure เป็นวิธีตรงไปตรงมามากกว่าในการนับ จำนวนครั้งที่ closure ถูกเรียก closure ใน Listing 13-9 ทำงานกับ sort_by_key ได้เพราะมันจับเพียง mutable reference ของ counter num_sort_operations และดังนั้นถูกเรียกได้มากกว่าหนึ่งครั้ง

Filename: 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
    });
    println!("{list:#?}, sorted in {num_sort_operations} operations");
}
Listing 13-9: ใช้ closure FnMut กับ sort_by_key ได้รับอนุญาต

trait Fn สำคัญเมื่อนิยามหรือใช้ฟังก์ชันหรือ type ที่ใช้ closure ในส่วนถัดไป เราจะพูดถึง iterator เมธอด iterator หลายตัวรับ อาร์กิวเมนต์ closure ดังนั้นเก็บรายละเอียด closure เหล่านี้ในใจ ขณะที่เราดำเนินต่อ!