โคลเชอร์ (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) เมธอดของตัวซ้ำจำนวนมากรับอาร์กิวเมนต์เป็นโคลเชอร์ ดังนั้นโปรดคำนึงถึงรายละเอียดโคลเชอร์เหล่านี้เมื่อเราเรียนรู้ต่อไป!