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

โคลเชอร์ (Closures)

โคลเชอร์ (Closures) ใน Rust คือฟังก์ชันนิรนาม (anonymous functions) ที่คุณสามารถบันทึกไว้ในตัวแปรหรือส่งเป็นอาร์กิวเมนต์ให้กับฟังก์ชันอื่นได้ คุณสามารถสร้างโคลเชอร์ไว้ที่จุดหนึ่ง แล้วเรียกใช้โคลเชอร์นั้นที่จุดอื่นเพื่อประมวลผล (evaluate) ในบริบทที่แตกต่างออกไปได้ ต่างจากฟังก์ชันตรงที่โคลเชอร์สามารถดึงค่า (capture values) จากขอบเขต (scope) ที่พวกมันถูกนิยามไว้ได้ เราจะแสดงให้เห็นว่าฟีเจอร์ของโคลเชอร์เหล่านี้ช่วยให้เกิดการใช้โค้ดซ้ำ (code reuse) และการปรับแต่งพฤติกรรมได้อย่างไร

การดึงค่าจากสภาพแวดล้อม (Capturing the Environment)

อันดับแรกเราจะพิจารณาว่าเราสามารถใช้โคลเชอร์ในการดึงค่าจากสภาพแวดล้อมที่พวกมันถูกนิยามไว้เพื่อนำมาใช้ในภายหลังได้อย่างไร นี่คือสถานการณ์ตัวอย่าง: บริษัทเสื้อยืดของเราจะแจกเสื้อยืดรุ่นลิมิเต็ดเอดิชันสุดพิเศษให้กับผู้ที่อยู่ในรายชื่ออีเมลของเราเป็นโปรโมชันอยู่เรื่อย ๆ ผู้ที่อยู่ในรายชื่ออีเมลสามารถเลือกเพิ่มสีที่ชอบลงในโปรไฟล์ของตนเองได้ หากผู้ที่ได้รับเลือกให้ได้เสื้อฟรีมีการตั้งค่าสีที่ชอบไว้ พวกเขาก็จะได้เสื้อสีนั้น แต่ถ้าผู้ได้รับเลือกไม่ได้ระบุสีที่ชอบ พวกเขาก็จะได้เสื้อสีที่บริษัทมีจำนวนมากที่สุด ณ ขณะนั้น

มีหลายวิธีในการเขียนโค้ดสำหรับระบบนี้ สำหรับตัวอย่างนี้ เราจะใช้ enum ชื่อ ShirtColor ที่มีตัวแปรย่อย (variants) คือ Red และ Blue (จำกัดจำนวนสีไว้เพื่อความง่าย) เราแทนคลังสินค้าของบริษัทด้วย struct Inventory ที่มีฟิลด์ชื่อ shirts ซึ่งบรรจุ Vec<ShirtColor> เพื่อแทนสีเสื้อที่มีอยู่ในสต็อก ณ ตอนนั้น เมธอด giveaway ที่นิยามไว้บน Inventory จะรับตัวเลือกสีเสื้อที่ชอบของผู้ชนะเลิศ และจะคืนค่าสีเสื้อที่คน ๆ นั้นจะได้รับ โครงสร้างนี้แสดงอยู่ในโค้ดตัวอย่างที่ 13-1

#[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
    );
}

ในฟังก์ชัน main ตัวแปร store มีเสื้อสีน้ำเงินเหลือ 2 ตัวและเสื้อสีแดงเหลือ 1 ตัวสำหรับโปรโมชันลิมิเต็ดเอดิชันนี้ เราเรียกใช้เมธอด giveaway สำหรับผู้ใช้ที่ชอบเสื้อสีแดง และผู้ใช้ที่ไม่ระบุสีที่ชอบ

ขอย้ำอีกครั้งว่าโค้ดนี้สามารถเขียนได้หลายวิธี และในที่นี้ เพื่อเน้นไปที่เรื่องโคลเชอร์ เราจึงเลือกใช้แนวคิดที่คุณได้เรียนรู้มาแล้ว ยกเว้นในส่วนของเนื้อหาในเมธอด giveaway ที่มีการใช้โคลเชอร์ ในเมธอด giveaway เรารับค่าความชอบของผู้ใช้เป็นพารามิเตอร์ชนิด Option<ShirtColor> และเรียกใช้เมธอด unwrap_or_else บน user_preference เมธอด unwrap_or_else บน Option<T> ถูกนิยามไว้ในไลบรารีมาตรฐาน (standard library) โดยรับอาร์กิวเมนต์หนึ่งตัว คือโคลเชอร์ที่ไม่รับพารามิเตอร์ใด ๆ และคืนค่าประเภท T (ชนิดข้อมูลเดียวกับที่เก็บบรรจุอยู่ในตัวแปรย่อย Some ของ Option<T> ซึ่งในที่นี้คือ ShirtColor) หาก Option<T> เป็นตัวแปรย่อย Some เมธอด unwrap_or_else จะคืนค่าที่อยู่ภายใน Some นั้น แต่ถ้า Option<T> เป็นตัวแปรย่อย None เมธอด unwrap_or_else จะเรียกใช้โคลเชอร์และคืนค่าที่ได้จากโคลเชอร์นั้น

เราระบุพจน์โคลเชอร์ (closure expression) || self.most_stocked() เป็นอาร์กิวเมนต์ให้กับ unwrap_or_else นี่คือโคลเชอร์ที่ไม่รับพารามิเตอร์ใด ๆ (หากโคลเชอร์มีพารามิเตอร์ พารามิเตอร์เหล่านั้นจะปรากฏอยู่ระหว่างแนวตั้งสองเส้น ||) เนื้อหาภายในโคลเชอร์จะเรียกใช้ self.most_stocked() เรากำลังนิยามโคลเชอร์ไว้ตรงนี้ และการทำงานของ unwrap_or_else จะประมวลผลโคลเชอร์ในภายหลังหากจำเป็นต้องใช้ผลลัพธ์

เมื่อรันโค้ดนี้จะพิมพ์ผลลัพธ์ดังนี้:

$ 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

ประเด็นที่น่าสนใจประการหนึ่งตรงนี้คือ เราได้ส่งโคลเชอร์ที่เรียกใช้ self.most_stocked() บนอินสแตนซ์ Inventory ปัจจุบัน โดยที่ไลบรารีมาตรฐานไม่จำเป็นต้องรู้จักชนิดข้อมูล Inventory หรือ ShirtColor ที่เรานิยามขึ้น หรือตรรกะที่เราต้องการใช้ในสถานการณ์นี้เลย โคลเชอร์จะดึงการอ้างอิงแบบแก้ไขไม่ได้ (immutable reference) ไปยังอินสแตนซ์ self ของ Inventory และส่งต่อมันพร้อมกับโค้ดที่เรากำหนดไปยังเมธอด unwrap_or_else ในขณะที่ฟังก์ชันทั่วไปไม่สามารถดึงค่าจากสภาพแวดล้อมในลักษณะนี้ได้

การอนุมานและการระบุชนิดข้อมูลของโคลเชอร์ (Inferring and Annotating Closure Types)

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

โดยทั่วไปโคลเชอร์มักจะสั้นและมีความเกี่ยวข้องเฉพาะในบริบทที่แคบ แทนที่จะใช้ในสถานการณ์ทั่วไป ภายในบริบทที่จำกัดเหล่านี้ คอมไพเลอร์สามารถอนุมาน (infer) ชนิดข้อมูลของพารามิเตอร์และชนิดข้อมูลของค่าคืนกลับได้ คล้ายกับความสามารถในการอนุมานชนิดข้อมูลของตัวแปรส่วนใหญ่ (มีกรณีที่หายากซึ่งคอมไพเลอร์ต้องการให้ระบุชนิดข้อมูลของโคลเชอร์ด้วยเช่นกัน)

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

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);
}

เมื่อเพิ่มการระบุชนิดข้อมูลแล้ว ไวยากรณ์ของโคลเชอร์จะดูคล้ายกับไวยากรณ์ของฟังก์ชันมากขึ้น ในที่นี้ นิยามฟังก์ชันที่บวก 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  ;

บรรทัดแรกแสดงการนิยามฟังก์ชัน และบรรทัดที่สองแสดงการนิยามโคลเชอร์แบบระบุชนิดข้อมูลอย่างสมบูรณ์ ในบรรทัดที่สาม เราถอดการระบุชนิดข้อมูลออกจากนิยามโคลเชอร์ ในบรรทัดที่สี่ เราถอดเครื่องหมายปีกกาออก ซึ่งสามารถละได้เพราะเนื้อหาของโคลเชอร์มีเพียงพจน์ (expression) เดียว ทั้งหมดนี้เป็นการนิยามที่ถูกต้องและจะให้พฤติกรรมเดียวกันเมื่อถูกเรียกใช้ บรรทัด add_one_v3 และ add_one_v4 จำเป็นต้องให้โคลเชอร์ถูกประมวลผลจึงจะสามารถคอมไพล์ได้ เพราะชนิดข้อมูลจะถูกอนุมานจากการใช้งาน ซึ่งคล้ายกับ let v = Vec::new(); ที่ต้องมีทั้งการระบุชนิดข้อมูลหรือการใส่ค่าบางชนิดลงใน Vec เพื่อให้ Rust สามารถอนุมานชนิดข้อมูลได้

สำหรับการนิยามโคลเชอร์ คอมไพเลอร์จะอนุมานชนิดข้อมูลที่เป็นรูปธรรม (concrete type) เพียงชนิดเดียวสำหรับพารามิเตอร์แต่ละตัวและสำหรับค่าคืนกลับ ตัวอย่างเช่น โค้ดตัวอย่างที่ 13-3 แสดงการนิยามโคลเชอร์สั้น ๆ ที่ทำเพียงคืนค่าที่ได้รับมาเป็นพารามิเตอร์ โคลเชอร์นี้ไม่ได้มีประโยชน์มากนักเว้นแต่เพื่อวัตถุประสงค์ของตัวอย่างนี้ โปรดสังเกตว่าเราไม่ได้เพิ่มการระบุชนิดข้อมูลใด ๆ ลงในการนิยาม เนื่องจากไม่มีการระบุชนิดข้อมูล เราจึงสามารถเรียกใช้โคลเชอร์ด้วยชนิดข้อมูลใดก็ได้ ซึ่งในครั้งแรกเราทำด้วย String หากเราพยายามเรียกใช้ example_closure ด้วยจำนวนเต็มในบรรทัดถัดมา เราจะได้ข้อผิดพลาด (error)

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

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

คอมไพเลอร์แจ้งข้อผิดพลาดนี้แก่เรา:

$ 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 คอมไพเลอร์จะอนุมานชนิดข้อมูลของ x และชนิดข้อมูลของค่าคืนกลับของโคลเชอร์ว่าเป็น String ชนิดข้อมูลเหล่านั้นจะถูกล็อกไว้กับโคลเชอร์ใน example_closure และเราจะได้ข้อผิดพลาดเรื่องชนิดข้อมูล (type error) เมื่อเราพยายามใช้ชนิดข้อมูลอื่นกับโคลเชอร์เดิมในครั้งถัดไป

การยืมอ้างอิงและการย้ายความเป็นเจ้าของ (Capturing References or Moving Ownership)

โคลเชอร์สามารถดึงค่าจากสภาพแวดล้อมของมันได้ 3 วิธี ซึ่งสอดคล้องโดยตรงกับ 3 วิธีที่ฟังก์ชันรับพารามิเตอร์ ได้แก่ การยืมแบบแก้ไขไม่ได้ (borrowing immutably) การยืมแบบแก้ไขได้ (borrowing mutably) และการรับความเป็นเจ้าของ (taking ownership) โคลเชอร์จะตัดสินใจว่าจะใช้วิธีใดโดยพิจารณาจากสิ่งที่เนื้อหาของฟังก์ชันทำกับค่าที่ดึงมา

ในโค้ดตัวอย่างที่ 13-4 เรานิยามโคลเชอร์ที่ดึงการอ้างอิงแบบแก้ไขไม่ได้ไปยังเวกเตอร์ชื่อ list เนื่องจากมันต้องการเพียงการอ้างอิงแบบแก้ไขไม่ได้เพื่อพิมพ์ค่าเท่านั้น

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:?}");
}

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

เนื่องจากเราสามารถมีการอ้างอิงแบบแก้ไขไม่ได้หลายตำแหน่งไปยัง list พร้อมกันได้ list จึงยังคงเข้าถึงได้จากโค้ดก่อนหน้าการนิยามโคลเชอร์ หลังการนิยามโคลเชอร์แต่ก่อนที่โคลเชอร์จะถูกเรียก และหลังจากที่โคลเชอร์ถูกเรียก โค้ดนี้สามารถคอมไพล์ รัน และพิมพ์ผลลัพธ์ได้:

$ 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]

ถัดมา ในโค้ดตัวอย่างที่ 13-5 เราเปลี่ยนเนื้อหาโคลเชอร์เพื่อให้เพิ่มสมาชิกเข้าไปในเวกเตอร์ list ตอนนี้โคลเชอร์จะดึงการอ้างอิงแบบแก้ไขได้ (mutable reference)

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:?}");
}

โค้ดนี้สามารถคอมไพล์ รัน และพิมพ์ผลลัพธ์ได้:

$ 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! อยู่ระหว่างการนิยามและการเรียกใช้โคลเชอร์ borrows_mutably อีกต่อไป เมื่อ borrows_mutably ถูกนิยาม มันจะดึงการอ้างอิงแบบแก้ไขได้ไปยัง list เราไม่ได้ใช้โคลเชอร์นี้อีกหลังจากเรียกใช้โคลเชอร์แล้ว ดังนั้นการยืมแบบแก้ไขได้จึงสิ้นสุดลง ในช่วงระหว่างการนิยามโคลเชอร์และการเรียกใช้โคลเชอร์ การยืมแบบแก้ไขไม่ได้เพื่อพิมพ์ค่าจะไม่ได้รับอนุญาต เพราะไม่อนุญาตให้มีการยืมอื่น ๆ เมื่อมีการยืมแบบแก้ไขอยู่นั้น ลองเพิ่ม println! ในจุดนั้นเพื่อดูข้อผิดพลาดที่คุณจะได้รับ!

หากคุณต้องการบังคับให้โคลเชอร์ครอบครองความเป็นเจ้าของ (take ownership) ของค่าที่ใช้ในสภาพแวดล้อม แม้ว่าเนื้อหาของโคลเชอร์จะไม่จำเป็นต้องใช้ความเป็นเจ้าของอย่างเคร่งครัดก็ตาม คุณสามารถใช้คีย์เวิร์ด move ไว้หน้า รายการพารามิเตอร์ ได้

เทคนิคนี้มีประโยชน์อย่างมากเมื่อส่งโคลเชอร์ไปยังเธรดใหม่เพื่อย้ายข้อมูลให้เธรดใหม่เป็นเจ้าของ เราจะหารือเกี่ยวกับเธรดและเหตุผลที่คุณต้องการใช้งานเธรดอย่างละเอียดในบทที่ 16 เมื่อเราพูดถึงเรื่องการทำงานพร้อมกัน (concurrency) แต่สำหรับตอนนี้ เรามาลองสำรวจการสร้างเธรดใหม่โดยใช้โคลเชอร์ที่ต้องใช้คีย์เวิร์ด move กันคร่าว ๆ โค้ดตัวอย่างที่ 13-6 แสดงโค้ดตัวอย่างที่ 13-4 ที่ถูกปรับเปลี่ยนเพื่อพิมพ์เวกเตอร์ในเธรดใหม่แทนที่จะเป็นเธรดหลัก (main thread)

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-4 โคลเชอร์เพียงดึง list โดยใช้การอ้างอิงแบบแก้ไขไม่ได้ เพราะนั่นคือการเข้าถึงน้อยที่สุดที่จำเป็นสำหรับการพิมพ์ค่า ในตัวอย่างนี้ แม้ว่าเนื้อหาของโคลเชอร์จะยังคงต้องการเพียงการอ้างอิงแบบแก้ไขไม่ได้ แต่เราจำเป็นต้องระบุว่า list ควรถูกย้ายเข้าไปในโคลเชอร์โดยใส่คีย์เวิร์ด move ไว้ตอนเริ่มต้นของการนิยามโคลเชอร์ หากเธรดหลักทำงานอื่นเพิ่มเติมก่อนจะเรียก join บนเธรดใหม่ เธรดใหม่อาจจะทำงานเสร็จก่อนส่วนที่เหลือของเธรดหลัก หรือเธรดหลักอาจจะทำงานเสร็จก่อนก็ได้ หากเธรดหลักยังคงครอบครองความเป็นเจ้าของ list แต่สิ้นสุดการทำงานก่อนเธรดใหม่และคืนคืนหน่วยกิต/คืนค่า (drops) list การอ้างอิงแบบแก้ไขไม่ได้ในเธรดจะกลายเป็นค่าที่ไม่ถูกต้อง (invalid) ดังนั้น คอมไพเลอร์จึงกำหนดให้ list ถูกย้ายเข้าไปในโคลเชอร์ที่มอบให้แก่เธรดใหม่เพื่อการอ้างอิงนั้นจะถูกต้องเสมอ ลองถอดคีย์เวิร์ด move ออก หรือลองใช้ list ในเธรดหลักหลังจากนิยามโคลเชอร์ เพื่อดูว่าคุณได้รับข้อผิดพลาดใดจากคอมไพเลอร์!

การย้ายค่าที่ดึงมาออกจากโคลเชอร์ (Moving Captured Values Out of Closures)

เมื่อโคลเชอร์ทำการดึงการอ้างอิงหรือดึงความเป็นเจ้าของของค่าจากสภาพแวดล้อมที่โคลเชอร์ถูกนิยาม (ซึ่งส่งผลต่อสิ่งที่ถูกย้าย เข้าไปใน โคลเชอร์) โค้ดในเนื้อหาของโคลเชอร์จะเป็นตัวกำหนดว่าจะเกิดอะไรขึ้นกับการอ้างอิงหรือค่านั้นเมื่อโคลเชอร์ถูกประมวลผลในภายหลัง (ซึ่งส่งผลต่อสิ่งที่ถูกย้าย ออกจาก โคลเชอร์)

เนื้อหาของโคลเชอร์สามารถทำสิ่งใดสิ่งหนึ่งดังต่อไปนี้: ย้ายค่าที่ดึงมาออกจากโคลเชอร์, เปลี่ยนแปลงค่าที่ดึงมา, ไม่ย้ายและไม่เปลี่ยนแปลงค่า, หรือไม่ดึงสิ่งใดจากสภาพแวดล้อมเลยตั้งแต่แรก

วิธีการที่โคลเชอร์ดึงและจัดการกับค่าจากสภาพแวดล้อมจะส่งผลต่อเทรต (traits) ที่โคลเชอร์นั้นประยุกต์ใช้ (implement) และเทรตคือวิธีที่ฟังก์ชันและ struct สามารถระบุชนิดของโคลเชอร์ที่พวกมันสามารถใช้งานได้ โคลเชอร์จะประยุกต์ใช้เทรต Fn หนึ่ง สอง หรือทั้งสามเทรตโดยอัตโนมัติ ในลักษณะเพิ่มขยาย (additive) ขึ้นอยู่กับวิธีที่เนื้อหาของโคลเชอร์จัดการกับค่าเหล่านั้น:

  • FnOnce นำไปใช้กับโคลเชอร์ที่สามารถเรียกได้ครั้งเดียว โคลเชอร์ทั้งหมดจะประยุกต์ใช้เทรตนี้เป็นอย่างน้อยเนื่องจากโคลเชอร์ทั้งหมดสามารถถูกเรียกได้ โคลเชอร์ที่ย้ายค่าที่ดึงมาออกจากเนื้อหาของมัน จะประยุกต์ใช้เพียง FnOnce เท่านั้น และจะไม่ประยุกต์ใช้เทรต Fn อื่น ๆ เพราะมันสามารถเรียกได้เพียงครั้งเดียว
  • FnMut นำไปใช้กับโคลเชอร์ที่ไม่ย้ายค่าที่ดึงมาออกจากเนื้อหา แต่ไม่อาจเปลี่ยนแปลง (mutate) ค่าที่ดึงมาได้ โคลเชอร์เหล่านี้สามารถเรียกได้มากกว่าหนึ่งครั้ง
  • Fn นำไปใช้กับโคลเชอร์ที่ไม่ย้ายค่าที่ดึงมาออกจากเนื้อหาและไม่เปลี่ยนแปลงค่าที่ดึงมา รวมถึงโคลเชอร์ที่ไม่ดึงสิ่งใดจากสภาพแวดล้อมเลย โคลเชอร์เหล่านี้สามารถเรียกได้มากกว่าหนึ่งครั้งโดยไม่เปลี่ยนแปลงสภาพแวดล้อม ซึ่งมีความสำคัญในกรณีต่าง ๆ เช่น การเรียกใช้โคลเชอร์หลายครั้งพร้อมกัน (concurrently)

มาดูการนิยามเมธอด unwrap_or_else บน Option<T> ที่เราใช้ในโค้ดตัวอย่างที่ 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) ที่แทนชนิดข้อมูลของค่าในตัวแปรย่อย Some ของ Option ชนิดข้อมูล T นั้นยังเป็นชนิดข้อมูลของค่าคืนกลับของฟังก์ชัน unwrap_or_else อีกด้วย: ตัวอย่างเช่น โค้ดที่เรียกใช้ unwrap_or_else บน Option<String> จะได้รับ String

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

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

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

คราวนี้มาดูเมธอดในไลบรารีมาตรฐานชื่อ sort_by_key ซึ่งนิยามไว้บน slice เพื่อดูว่าแตกต่างจาก unwrap_or_else อย่างไร และเหตุใด sort_by_key จึงใช้ FnMut แทนที่จะเป็น FnOnce สำหรับขอบเขตของเทรต โคลเชอร์จะรับอาร์กิวเมนต์หนึ่งตัวในรูปของการอ้างอิงไปยังสมาชิกปัจจุบันใน slice ที่กำลังพิจารณา และคืนค่าชนิดข้อมูล K ที่สามารถจัดเรียงลำดับได้ ฟังก์ชันนี้มีประโยชน์เมื่อคุณต้องการเรียงลำดับ slice ตามคุณลักษณะเฉพาะของแต่ละสมาชิก ในโค้ดตัวอย่างที่ 13-7 เรามีรายการอินสแตนซ์ของ Rectangle และเราใช้ sort_by_key เพื่อเรียงลำดับพวกมันตามคุณลักษณะความกว้าง (width) จากน้อยไปมาก

#[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:#?}");
}

โค้ดนี้พิมพ์ผลลัพธ์:

$ 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 ถูกนิยามมาให้รับโคลเชอร์ FnMut คือมันจะเรียกใช้โคลเชอร์หลายครั้ง: หนึ่งครั้งสำหรับแต่ละสมาชิกใน slice โคลเชอร์ |r| r.width ไม่ได้ดึง เปลี่ยนแปลง หรือย้ายสิ่งใดออกจากสภาพแวดล้อมของมัน ดังนั้นมันจึงเป็นไปตามข้อกำหนดของขอบเขตเทรต

ในทางกลับกัน โค้ดตัวอย่างที่ 13-8 แสดงตัวอย่างของโคลเชอร์ที่ประยุกต์ใช้เพียงเทรต FnOnce เท่านั้น เนื่องจากมันย้ายค่าออกจากสภาพแวดล้อม คอมไพเลอร์จะไม่ยินยอมให้เราใช้โคลเชอร์นี้กับ sort_by_key

#[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:#?}");
}

นี่คือวิธีประดิษฐ์ซับซ้อน (ที่ไม่ทำงาน) ในการพยายามนับจำนวนครั้งที่ sort_by_key เรียกใช้โคลเชอร์ขณะเรียงลำดับ list โค้ดนี้พยายามทำนับจำนวนโดยการดัน (push) ค่า value—ซึ่งเป็น String จากสภาพแวดล้อมของโคลเชอร์—เข้าไปในเวกเตอร์ sort_operations โคลเชอร์จะดึง value แล้วย้าย value ออกจากโคลเชอร์โดยการโอนย้ายความเป็นเจ้าของของ value ไปยังเวกเตอร์ sort_operations โคลเชอร์นี้สามารถเรียกได้ครั้งเดียว การพยายามเรียกใช้เป็นครั้งที่สองจะไม่ทำงาน เพราะ value ไม่ได้อยู่ในสภาพแวดล้อมที่จะถูกดันเข้าไปใน sort_operations อีกต่อไป! ดังนั้น โคลเชอร์นี้จึงประยุกต์ใช้เพียง FnOnce เมื่อเราพยายามคอมไพล์โค้ดนี้ เราจะได้ข้อผิดพลาดว่า value ไม่สามารถถูกย้ายออกจากโคลเชอร์ได้เนื่องจากโคลเชอร์ต้องประยุกต์ใช้ 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: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but `FnOnce` closures may consume them only once
  --> /rustc/ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96/library/alloc/src/slice.rs:249:11
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

ข้อผิดพลาดจะชี้ไปที่บรรทัดในเนื้อหาของโคลเชอร์ที่ย้าย value ออกจากสภาพแวดล้อม ในการแก้ไข ปัญหานี้ เราต้องเปลี่ยนเนื้อหาของโคลเชอร์ไม่ให้ย้ายค่าออกจากสภาพแวดล้อม การเก็บตัวนับไว้ในสภาพแวดล้อมและเพิ่มค่าตัวนับในเนื้อหาของโคลเชอร์เป็นวิธีที่ตรงไปตรงมามากกว่าในการนับจำนวนครั้งที่โคลเชอร์ถูกเรียก โคลเชอร์ในโค้ดตัวอย่างที่ 13-9 ทำงานร่วมกับ sort_by_key ได้เพราะมันเพียงแค่ดึงการอ้างอิงแบบแก้ไขได้ไปยังตัวนับ num_sort_operations จึงสามารถเรียกใช้ได้มากกว่าหนึ่งครั้ง

#[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");
}

เทรตกลุ่ม Fn มีความสำคัญเมื่อนิยามหรือใช้ฟังก์ชันหรือชนิดข้อมูลที่มีการใช้โคลเชอร์ ในส่วนถัดไป เราจะพูดถึงตัวซ้ำ (iterators) เมธอดของตัวซ้ำจำนวนมากรับอาร์กิวเมนต์เป็นโคลเชอร์ ดังนั้นโปรดคำนึงถึงรายละเอียดโคลเชอร์เหล่านี้เมื่อเราเรียนรู้ต่อไป!