Closures
Closure คืออะไร?
หัวข้อที่มีชื่อว่า “Closure คืออะไร?”Closure คือฟังก์ชันไม่มีชื่อที่ประกาศแบบ inline ด้วย syntax |params| body ต่างจาก fn ปกติตรงที่ closure สามารถ capture ตัวแปรจาก scope รอบๆ มันได้
let multiplier = 3;let triple = |x| x * multiplier; // captures multiplier from the environmentprintln!("{}", triple(7)); // 21การ Capture Environment
หัวข้อที่มีชื่อว่า “การ Capture Environment”Rust มี closure trait สามตัวที่บอกว่า closure ใช้ค่าที่ capture มายังไง:
| Trait | วิธี capture | เรียกได้กี่ครั้ง |
|---|---|---|
Fn | borrow แบบ immutable (&T) | กี่ครั้งก็ได้ |
FnMut | borrow แบบ mutable (&mut T) | กี่ครั้งก็ได้ |
FnOnce | เอา ownership ไป (T) | ครั้งเดียวเท่านั้น |
closure ทุกตัว implement FnOnce เป็นอย่างน้อย ถ้ามันแค่ borrow มันก็ implement Fn ด้วย คอมไพเลอร์ infer ว่า closure implement trait ไหนโดยดูว่า body ของมันทำอะไรกับแต่ละตัวแปรที่ capture มาจริงๆ — ไม่เคยดูว่าคุณตั้งใจจะใช้มันยังไง การ infer นั้นไม่ใช่รายละเอียดที่คุณละเลยได้ เพราะทันทีที่คุณใช้ closure ในที่ที่ต้องการการรับประกันที่แข็งแรงกว่าที่ body ได้มา คอมไพเลอร์จะบอกคุณ
flowchart TD
A[What does the closure body do with a captured variable?] --> B{Only reads it?}
B -->|Yes| C[Fn: borrows &T]
B -->|No, mutates it| D{Only mutates, never consumes?}
D -->|Yes| E[FnMut: borrows &mut T]
D -->|No, moves or drops it| F[FnOnce: takes ownership T] Fn — Borrow แบบ Immutable
หัวข้อที่มีชื่อว่า “Fn — Borrow แบบ Immutable”fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }
let base = 10;let add_base = |x| x + base; // borrows base immutablyprintln!("{}", apply(add_base, 5)); // 15FnMut — Borrow แบบ Mutable
หัวข้อที่มีชื่อว่า “FnMut — Borrow แบบ Mutable”let mut count = 0;let mut increment = || { count += 1; count };println!("{}", increment()); // 1println!("{}", increment()); // 2drop(increment); // release the mutable borrowprintln!("count is {}", count); // 2drop(increment) ต้องถูกเรียกก่อนอ่าน count โดยตรง เพราะ increment ถือ &mut count ที่จะขัดแย้งกับ immutable borrow ใน println!
FnOnce — เอา Ownership ไป
หัวข้อที่มีชื่อว่า “FnOnce — เอา Ownership ไป”let name = String::from("Rust");let greet = || println!("Hello, {}!", name); // moves name into closuregreet(); // name is consumed here// greet(); // would compile-error — FnOnce can only be called onceคอมไพเลอร์พูดว่าอะไร: Closure ที่ Consume ค่าเรียกซ้ำสองครั้งไม่ได้
หัวข้อที่มีชื่อว่า “คอมไพเลอร์พูดว่าอะไร: Closure ที่ Consume ค่าเรียกซ้ำสองครั้งไม่ได้”Fn เป็นข้อกำหนดแบบ supertrait: อะไรก็ตามที่ต้องการ Fn ต้องเรียก closure ได้กี่ครั้งก็ได้ตามที่ต้องการ ดังนั้น closure ที่ body ของมัน consume ค่าที่ capture มาจริงๆ — แม้แค่เรียก drop() บนมัน — จะเป็นได้แค่ FnOnce เท่านั้น และการใช้มันในที่ที่ต้องการ Fn คือ type error ไม่ใช่ความประหลาดใจตอน runtime:
// @expect-error E0525fn call_twice<F: Fn()>(f: F) { f(); f();}
fn main() { let name = String::from("Rust"); // this closure takes ownership of `name` and consumes it inside — FnOnce only let greet = move || { println!("bye, {}", name); drop(name); }; call_twice(greet); // ERROR: call_twice requires Fn, greet is only FnOnce}error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce` --> src/main.rs:9:17 | 9 | let greet = move || { | ^^^^^^^ this closure implements `FnOnce`, not `Fn`10 | println!("bye, {}", name);11 | drop(name); | ---- closure is `FnOnce` because it moves the variable `name` out of its environment12 | };13 | call_twice(greet); // ERROR: call_twice requires Fn, greet is only FnOnce | ---------- ----- the requirement to implement `Fn` derives from here | | | required by a bound introduced by this call |note: required by a bound in `call_twice` --> src/main.rs:1:18 | 1 | fn call_twice<F: Fn()>(f: F) { | ^^^^ required by this bound in `call_twice`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0525`.สังเกตว่า diagnostic ชี้ไปที่บรรทัดที่แน่นอน (drop(name)) ที่ทำให้ closure ตกไปเป็น FnOnce — คอมไพเลอร์ไม่ได้เดาเอา มันสรุปมาจาก body ของ closure จริงๆ การเอา drop(name) ออก (แค่ print name โดยไม่ consume มัน) จะทำให้ closure เป็น Fn และคอมไพล์ผ่าน
Keyword move
หัวข้อที่มีชื่อว่า “Keyword move”move บังคับให้ closure เอา ownership ของทุกตัวแปรที่ capture มา ไม่ว่ามันจะแค่ borrow เฉยๆ ถ้าไม่มี move ก็ตาม:
let message = String::from("hi");let say = move || println!("{}", message); // message moved into say// println!("{}", message); // error — message was movedsay();move จำเป็นเมื่อ closure ต้องมีชีวิตอยู่นานกว่า scope ที่มันถูกสร้างขึ้น (เช่น ส่งให้ thread) ให้สังเกตว่า move เพียงอย่างเดียวไม่ได้บังคับให้เป็น FnOnce — closure greet ข้างบนต้องมีทั้ง move และ drop(name) ภายในเพื่อให้กลายเป็น FnOnce; closure แบบ move ที่แค่อ่าน String ที่ capture มาก็ยังเป็น Fn อยู่ เพราะ Fn/FnMut/FnOnce อธิบายว่า closure ใช้ สิ่งที่ capture มายังไง ไม่ใช่ว่ามัน capture มายังไง
คอมไพเลอร์พูดว่าอะไร: Closure ที่ยังมีชีวิตอยู่ยังถือ Borrow ของมันไว้
หัวข้อที่มีชื่อว่า “คอมไพเลอร์พูดว่าอะไร: Closure ที่ยังมีชีวิตอยู่ยังถือ Borrow ของมันไว้”closure แบบ FnMut ถือ &mut captured_var ไว้ตลอดที่ closure นั้นยังอาจถูกเรียกอีก — ไม่ใช่แค่ระหว่างการเรียกครั้งเดียว การอ่านตัวแปรโดยตรงระหว่างการเรียกสองครั้งคือ borrow conflict แบบเดียวกับ E0502 ที่เห็นในบท iterators เพียงแต่ครั้งนี้มาจากการ capture ของ closure แทนที่จะเป็น &mut ที่เขียนตรงๆ:
// @expect-error E0502fn main() { let mut count = 0; let mut incrementer = || { count += 1; count }; println!("first: {}", incrementer()); println!("count directly: {}", count); // ERROR: incrementer is used again below, so its &mut count borrow is still live println!("second: {}", incrementer());}error[E0502]: cannot borrow `count` as immutable because it is also borrowed as mutable --> src/main.rs:5:36 |3 | let mut incrementer = || { count += 1; count }; | -- ----- first borrow occurs due to use of `count` in closure | | | mutable borrow occurs here4 | println!("first: {}", incrementer());5 | println!("count directly: {}", count); // ERROR: incrementer is used again below, so its &mut count borrow is still live | ^^^^^ immutable borrow occurs here6 | println!("second: {}", incrementer()); | ----------- mutable borrow later used hereรูปแบบเดียวกันนี้ก็เกิดขึ้นระหว่าง closure สองตัวที่ capture ตัวแปรเดียวกันแบบ mutable ด้วย: การมี &mut count ที่ยังมีชีวิตอยู่สองตัวพร้อมกันคือ E0499 ไม่ใช่ E0502 แต่กฎเบื้องหลัง (การเข้าถึงแบบเอกสิทธิ์) เหมือนกันเป๊ะ:
// @expect-error E0499fn main() { let mut count = 0; let mut add_one = || count += 1; let mut add_two = || count += 2; // ERROR: two live mutable borrows of count add_one(); add_two();}error[E0499]: cannot borrow `count` as mutable more than once at a time --> src/main.rs:4:23 |3 | let mut add_one = || count += 1; | -- ----- first borrow occurs due to use of `count` in closure | | | first mutable borrow occurs here4 | let mut add_two = || count += 2; // ERROR: two live mutable borrows of count | ^^ ----- second borrow occurs due to use of `count` in closure | | | second mutable borrow occurs here5 | add_one(); | ------- first borrow later used hereการคืนค่า Closure
หัวข้อที่มีชื่อว่า “การคืนค่า Closure”ฟังก์ชันที่คืนค่า closure ไม่สามารถเอ่ยชื่อ concrete type ของ closure ได้ (closure ทุกตัวได้ type ที่ไม่ซ้ำกันซึ่งคอมไพเลอร์สร้างให้) — มันต้องคืนค่าเป็น impl Fn(...) -> ... (type ที่เป็นรูปธรรมแต่ไม่มีชื่อหนึ่งตัว ตัดสิน ณ compile time) หรือ Box<dyn Fn(...) -> ...> (trait object เมื่อ concrete type ต่างกันจริงๆ เช่น แต่ละ branch คืนค่า closure ต่างชนิดกัน):
fn make_adder(base: i32) -> impl Fn(i32) -> i32 { move |x| x + base}
fn make_multiplier(factor: i32) -> Box<dyn Fn(i32) -> i32> { Box::new(move |x| x * factor)}
fn main() { let add5 = make_adder(5); println!("add5(10) = {}", add5(10));
let ops: Vec<Box<dyn Fn(i32) -> i32>> = vec![ make_multiplier(2), make_multiplier(3), Box::new(|x| x - 1), ]; for op in &ops { println!("op(10) = {}", op(10)); }}add5(10) = 15op(10) = 20op(10) = 30op(10) = 9make_adder คืนค่า closure รูปทรงเดียวกันเสมอ (move |x| x + base ตัวเดียว) ดังนั้น impl Fn(i32) -> i32 — ไม่มีต้นทุนเพิ่ม ไม่ต้อง allocate บน heap ไม่มี vtable — ก็เพียงพอแล้ว Vec<Box<dyn Fn(i32) -> i32>> ใน main ต้องการ Box<dyn Fn> เพราะมันเก็บ concrete closure type ที่ต่างกันไว้ในที่เดียวกันใน collection เดียว — impl Trait ทำแบบนั้นไม่ได้ เพราะมันเอ่ยชื่อ concrete type แค่ตัวเดียวต่อ function signature หนึ่งอัน
Closure ในฐานะ Iterator Adaptor
หัวข้อที่มีชื่อว่า “Closure ในฐานะ Iterator Adaptor”iterator adaptor อย่าง map และ filter รับ closure ได้โดยตรง:
let threshold = 3;let big: Vec<i32> = vec![1, 2, 3, 4, 5] .iter() .filter(|&&x| x > threshold) // closure captures threshold by & .map(|&x| x * 10) .collect();// [40, 50]fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x)}
fn apply_mut<F: FnMut() -> i32>(mut f: F) -> i32 { f() + f()}
fn apply_once<F: FnOnce() -> String>(f: F) -> String { f()}
fn main() { // Fn: borrows environment immutably let multiplier = 3; let triple = |x| x * multiplier; println!("apply triple to 5: {}", apply(triple, 5));
// FnMut: captures and mutates let mut count = 0; let mut increment = || { count += 1; count }; let result = apply_mut(&mut increment); drop(increment); // release mutable borrow before reading count println!("FnMut result: {}, count is now: {}", result, count);
// FnOnce + move: takes ownership let name = String::from("Rust"); let greeting = move || format!("Hello, {}!", name); println!("{}", apply_once(greeting));
// closures as iterator adaptors let nums = vec![1, 2, 3, 4, 5]; let threshold = 3; let big: Vec<i32> = nums .iter() .filter(|&&x| x > threshold) .map(|&x| x * 10) .collect(); println!("big: {:?}", big);}Compiling…
แบบจำลองความคิด
หัวข้อที่มีชื่อว่า “แบบจำลองความคิด”closure คือ syntax sugar ของ struct ไม่มีชื่อบวก trait impl: คอมไพเลอร์สร้าง struct ที่เก็บตัวแปรที่ capture มาพอดี (ด้วย &, &mut, หรือ value — แล้วแต่ body ต้องการ) และ implement Fn/FnMut/FnOnce ให้มันตามสิ่งที่การ call ทำกับ field เหล่านั้น Fn/FnMut/FnOnce สร้างเป็นลำดับชั้น ไม่ใช่ตัวเลือกสามอันที่ไม่เกี่ยวข้องกัน: closure ที่เป็น Fn ทุกตัวก็เป็น FnMut และ FnOnce ด้วย (คุณเรียกอะไรน้อยครั้งกว่าหรือผ่าน borrow ที่อ่อนกว่าที่มันต้องการได้เสมอ) แต่กลับกันไม่จริง เมื่อคุณเห็น E0525 ให้อ่านมันว่า “struct ที่ closure นี้สร้างมา implement แค่ trait ที่อ่อนกว่า แต่จุดเรียกต้องการตัวที่แข็งแรงกว่า”
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”impl Fn(...) -> ... เป็น return type ไม่มีต้นทุนตอน runtime (คอมไพเลอร์ monomorphize concrete closure type เข้าไปใน caller) แต่ต้องการให้ทุก code path ในฟังก์ชันคืนค่า concrete closure type เดียวกัน Box<dyn Fn(...) -> ...> มีต้นทุนคือ heap allocation หนึ่งครั้งและ vtable indirection หนึ่งครั้งต่อการเรียก แต่ให้คุณคืนค่า (หรือเก็บใน Vec เดียว) closure ที่ต่างกันจริงๆ ได้ การ borrow เป็นค่าเริ่มต้น (Fn/FnMut) ทำให้ข้อมูลที่ capture มาใช้งานนอก closure ได้ต่อ; move สละความยืดหยุ่นนั้นแลกกับ closure ที่มีชีวิตอยู่นานกว่า scope ที่สร้างมันได้
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
moveทำให้ closure เป็นFnOnce”moveเปลี่ยนแค่วิธีที่ตัวแปรถูก capture (เป็น value แทนที่จะเป็น reference); มันไม่ได้บอกอะไรเกี่ยวกับวิธีที่ body ของ closureใช้มันต่อ closure แบบmoveที่แค่อ่านข้อมูลที่ capture มาก็ยังเป็นFnอยู่ — ตัวอย่างE0525ข้างบนต้องมีdrop(name)ที่ชัดเจนอยู่ใน body เพื่อให้กลายเป็นFnOnceไม่ใช่แค่moveเฉยๆ - “
Fn,FnMut, และFnOnceเป็นสามหมวดที่แยกจากกันและไม่ทับซ้อนกัน ที่ closure หนึ่งตัวสังกัดอยู่” มันสร้างเป็นลำดับชั้น: closure ทุกตัว implementFnOnce; closure ที่ไม่ consume สิ่งที่ capture มาก็ implementFnMutด้วย; ตัวที่ไม่ได้แม้แต่จะ mutate มันก็ implementFnด้วย closure ที่ “เป็นFn” implement ทั้งสามตัวพร้อมกัน - “เพราะ
increment()ถูกเรียกไปแล้วครั้งหนึ่ง borrow ของมันต่อcountน่าจะจบไปแล้วตอนที่ฉันอ่านcountโดยตรง” Non-lexical lifetimes จบ borrow ที่จุดใช้จริงครั้งสุดท้าย ไม่ใช่ครั้งแรก — ถ้า closure ถูกเรียกอีกครั้งในภายหลังในฟังก์ชันเดียวกัน borrow นั้นครอบคลุมทั้งช่วง รวมถึงความพยายามใดๆ ที่จะอ่านcountโดยตรงระหว่างนั้น (นั่นคือตัวอย่างE0502ข้างบนพอดี)
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โค้ดที่ AI สร้างบางครั้งเลือกใช้ .clone() กับข้อมูลที่ capture มาโดย closure ที่ส่งให้ map/filter เพียงเพื่อหลบข้อร้องเรียนของ borrow checker ที่มันวินิจฉัยไม่ครบ แม้ว่า closure จะแค่ต้องอ่านข้อมูลผ่าน reference เท่านั้น
🐛 หา bug ในโค้ดนี้
fn tag_all(prefix: String, items: &[String]) -> Vec<String> { items .iter() .map(|item| format!("{}{}", prefix.clone(), item)) .collect()}เฉลย
prefix.clone() รันหนึ่งครั้งต่อ element ของ items โดย clone String เดิมซ้ำแล้วซ้ำเล่าภายใน closure แม้ว่า closure จะแค่อ่าน prefix เท่านั้น — มันไม่เคยต้องการเป็นเจ้าของสำเนาใหม่เลย format!("{}{}", prefix, item) borrow prefix แบบ immutable แทน (closure capture ด้วย shared reference เป็นค่าเริ่มต้นเมื่อ body แค่อ่านเท่านั้น) และ closure นี้เรียกได้กี่ครั้งก็ได้โดยไม่ต้อง clone เลย .clone() ตรงนี้ไม่ได้แก้ borrow-checker error จริงๆ — มันเป็นปฏิกิริยาสะท้อนที่ใช้ไปโดยไม่ตรวจสอบว่าจำเป็นต้องมี ownership จริงหรือเปล่า