ข้ามไปยังเนื้อหา

Closures

Closure คือฟังก์ชันไม่มีชื่อที่ประกาศแบบ inline ด้วย syntax |params| body ต่างจาก fn ปกติตรงที่ closure สามารถ capture ตัวแปรจาก scope รอบๆ มันได้

let multiplier = 3;
let triple = |x| x * multiplier; // captures multiplier from the environment
println!("{}", triple(7)); // 21

Rust มี closure trait สามตัวที่บอกว่า closure ใช้ค่าที่ capture มายังไง:

Traitวิธี captureเรียกได้กี่ครั้ง
Fnborrow แบบ immutable (&T)กี่ครั้งก็ได้
FnMutborrow แบบ 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]
คอมไพเลอร์ infer Fn / FnMut / FnOnce อย่างไร
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }
let base = 10;
let add_base = |x| x + base; // borrows base immutably
println!("{}", apply(add_base, 5)); // 15
let mut count = 0;
let mut increment = || { count += 1; count };
println!("{}", increment()); // 1
println!("{}", increment()); // 2
drop(increment); // release the mutable borrow
println!("count is {}", count); // 2

drop(increment) ต้องถูกเรียกก่อนอ่าน count โดยตรง เพราะ increment ถือ &mut count ที่จะขัดแย้งกับ immutable borrow ใน println!

let name = String::from("Rust");
let greet = || println!("Hello, {}!", name); // moves name into closure
greet(); // 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 E0525
fn 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 environment
12 | };
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 และคอมไพล์ผ่าน

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 moved
say();

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 E0502
fn 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 here
4 | 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 here
6 | println!("second: {}", incrementer());
| ----------- mutable borrow later used here

รูปแบบเดียวกันนี้ก็เกิดขึ้นระหว่าง closure สองตัวที่ capture ตัวแปรเดียวกันแบบ mutable ด้วย: การมี &mut count ที่ยังมีชีวิตอยู่สองตัวพร้อมกันคือ E0499 ไม่ใช่ E0502 แต่กฎเบื้องหลัง (การเข้าถึงแบบเอกสิทธิ์) เหมือนกันเป๊ะ:

// @expect-error E0499
fn 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 here
4 | 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 here
5 | add_one();
| ------- first borrow later used here

ฟังก์ชันที่คืนค่า 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) = 15
op(10) = 20
op(10) = 30
op(10) = 9

make_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 หนึ่งอัน

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

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 ทุกตัว implement FnOnce; closure ที่ไม่ consume สิ่งที่ capture มาก็ implement FnMut ด้วย; ตัวที่ไม่ได้แม้แต่จะ mutate มันก็ implement Fn ด้วย closure ที่ “เป็น Fn” implement ทั้งสามตัวพร้อมกัน
  • “เพราะ increment() ถูกเรียกไปแล้วครั้งหนึ่ง borrow ของมันต่อ count น่าจะจบไปแล้วตอนที่ฉันอ่าน count โดยตรง” Non-lexical lifetimes จบ borrow ที่จุดใช้จริงครั้งสุดท้าย ไม่ใช่ครั้งแรก — ถ้า closure ถูกเรียกอีกครั้งในภายหลังในฟังก์ชันเดียวกัน borrow นั้นครอบคลุมทั้งช่วง รวมถึงความพยายามใดๆ ที่จะอ่าน count โดยตรงระหว่างนั้น (นั่นคือตัวอย่าง E0502 ข้างบนพอดี)

โค้ดที่ 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 จริงหรือเปล่า

closure trait ไหนอธิบาย closure ที่ borrow ตัวแปรที่ capture มาแบบ mutable?
ทำไม drop(increment) ต้องถูกเรียกก่อน println!("{}", count) ในตัวอย่าง FnMut?
keyword move ทำอะไรกับ closure?
closure ที่แค่อ่านตัวแปรที่ capture มาโดยไม่แก้ไข implement trait ไหน?
โค้ดนี้คอมไพล์ไม่ผ่านด้วยอะไร?
fn call_twice<F: Fn()>(f: F) {
    f();
    f();
}
fn main() {
    let s = String::from("hi");
    let c = move || { println!("{}", s); drop(s); };
    call_twice(c);
}
ทำไมฟังก์ชันที่คืนค่า concrete closure ต่างกันในแต่ละ branch จึงต้องการ Box<dyn Fn(...)> แทน impl Fn(...)?
ใน items.iter().map(|item| format!("{}{}", prefix.clone(), item)) ทำไม prefix.clone() มักจะไม่จำเป็น?