Mutable Borrows
Mutate ผ่าน Reference
หัวข้อที่มีชื่อว่า “Mutate ผ่าน Reference”shared reference &T เป็นแบบอ่านอย่างเดียว เมื่อคุณต้องการแก้ไขค่าที่ borrow มา ให้ใช้ mutable reference: &mut T
ต้องมีเงื่อนไขสองข้อพร้อมกัน:
- ตัวแปรเดิมต้องประกาศด้วย
mut - ตัว reference เองต้องเขียนเป็น
&mut
fn append_world(s: &mut String) { s.push_str(", world");}
fn main() { let mut greeting = String::from("hello"); append_world(&mut greeting); println!("{}", greeting); // hello, world}กฎ Exclusive-Access
หัวข้อที่มีชื่อว่า “กฎ Exclusive-Access”borrow checker บังคับใช้ invariant เดียว:
ณ จุดใด ๆ ในโค้ด คุณมีได้ อย่างใดอย่างหนึ่ง ระหว่าง shared reference กี่ตัวก็ได้ หรือ mutable reference เพียง หนึ่งตัว ไปยังค่าเดียวกัน — ไม่เคยทั้งสองอย่างพร้อมกัน
กฎนี้ป้องกัน data race ตอน compile time data race เกิดขึ้นเมื่อสอง thread ขึ้นไปเข้าถึงหน่วยความจำเดียวกันพร้อมกัน และอย่างน้อยหนึ่งการเข้าถึงคือการเขียน การทำให้การเข้าถึงแบบ mutable พร้อมกันเป็น compile error ทำให้ Rust ตัดบั๊กทั้งหมวด data-race ออกไป — แม้แต่ในโค้ด single-thread กฎเดียวกันนี้ก็ยังช่วยให้ logic ถูกต้อง
// @expect-error E0502fn main() { let mut s = String::from("hello"); let r1 = &s; // shared borrow let r2 = &mut s; // ERROR: s is already borrowed as immutable println!("{} {}", r1, r2);}error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable --> src/main.rs:5:14 |4 | let r1 = &s; // shared borrow | -- immutable borrow occurs here5 | let r2 = &mut s; // ERROR: s is already borrowed as immutable | ^^^^^^ mutable borrow occurs here6 | println!("{} {}", r1, r2); | -- immutable borrow later used hereflowchart LR Free[No active borrow] Shared[N shared borrows, all read-only] Exclusive[One exclusive borrow, read-write] Free -->|&x| Shared Shared -->|another &x| Shared Shared -->|last shared use ends, NLL| Free Free -->|&mut x| Exclusive Exclusive -->|borrow's last use ends, NLL| Free Shared -. blocked while any read is still live .-> Exclusive Exclusive -. blocked, only one writer at a time .-> Exclusive
Mutable Borrow สองตัวพร้อมกัน
หัวข้อที่มีชื่อว่า “Mutable Borrow สองตัวพร้อมกัน”กฎเดียวกันนี้ปิดกั้น mutable borrow ตัวที่สองแม้จะไม่มี shared reference เข้ามาเกี่ยวข้องเลย — &mut T เป็น exclusive กับ ทุกอย่าง รวมถึง &mut T อีกตัวด้วย:
// @expect-error E0499fn main() { let mut v = vec![1, 2, 3]; let r1 = &mut v; let r2 = &mut v; r1.push(4); r2.push(5);}error[E0499]: cannot borrow `v` as mutable more than once at a time --> src/main.rs:4:14 |3 | let r1 = &mut v; | ------ first mutable borrow occurs here4 | let r2 = &mut v; | ^^^^^^ second mutable borrow occurs here5 | r1.push(4); | -- first borrow later used here
error: aborting due to 1 previous errorทางแก้คือ pattern เดียวกับที่หัวข้อถัดไปจะทำให้เป็นทางการ: ใช้ r1 จนถึงจุดใช้งานสุดท้าย (last use) ก่อนสร้าง r2 เพื่อไม่ให้ borrow ทั้งสองซ้อนกันในเวลาเดียวกัน
Non-Lexical Lifetimes (NLL)
หัวข้อที่มีชื่อว่า “Non-Lexical Lifetimes (NLL)”ตั้งแต่ Rust 2018 คอมไพเลอร์ใช้ Non-Lexical Lifetimes (NLL): borrow หนึ่งจะจบลงที่จุดใช้งานจริงครั้งสุดท้าย ไม่ใช่ที่ท้าย block ที่ครอบมันอยู่ นั่นแปลว่าคุณเริ่ม borrow ใหม่ได้อย่างปลอดภัยหลังจาก last use ของ borrow ก่อนหน้า แม้จะอยู่ใน scope เดียวกัน
fn main() { let mut s = String::from("hello");
let r1 = &mut s; r1.push_str(", world"); // last use of r1 // r1's borrow ends here (NLL)
let r2 = &s; // perfectly fine — r1 is gone println!("{}", r2);}Two-Phase Borrows
หัวข้อที่มีชื่อว่า “Two-Phase Borrows”มีข้อยกเว้นตั้งใจหนึ่งข้อของ exclusivity แบบเข้มงวด และมันมีอยู่เพื่อไม่ให้โค้ดที่ดูปกติต้องใช้ตัวแปรชั่วคราวเพิ่ม ลองดู v.push(v.len()): การ evaluate argument v.len() ต้องการการอ่านแบบ shared ของ v แต่ .push(...) ต้องการ &mut v — และทั้งสองเกิดขึ้นใน expression เดียวกัน
fn main() { let mut v = vec![1, 2, 3]; // two-phase borrow: v.push(...) needs &mut v, but v.len() (a shared read) // is allowed to run first while the &mut is only "reserved" v.push(v.len()); println!("{:?}", v);}[1, 2, 3, 3]โค้ดนี้ compile ผ่านเพราะ two-phase borrows: &mut v ที่ .push ต้องการจะถูก จองไว้ ก่อนเท่านั้น (ทำตัวเหมือน shared borrow ระหว่างที่ argument ยัง evaluate อยู่) และจะกลายเป็น exclusive borrow เต็มตัวก็ตอนที่ .push ถูกเรียกจริง ๆ เท่านั้น ซึ่งตอนนั้น v.len() ถูกอ่านไปแล้ว ถ้าไม่มีข้อยกเว้นนี้ v.push(v.len()) จะผิดกฎเหมือนตัวอย่าง r1/r2 ข้างบนเป๊ะ และผู้เรียน Rust ทุกคนจะต้องเขียน let len = v.len(); v.push(len); แทน
Reborrowing
หัวข้อที่มีชื่อว่า “Reborrowing”คุณ copy &mut T ไม่ได้ (มีได้แค่ตัวเดียวเสมอ) แต่คุณสร้าง &mut T ที่มีอายุสั้นกว่า จาก ตัวเดิมได้ ใช้มัน แล้วกลับมาใช้ตัวเดิมต่อ — สิ่งนี้เรียกว่า reborrowing และมันเกิดขึ้นได้ทั้งแบบชัดเจน (&mut *r) และแบบ implicit ทุกครั้งที่คุณส่ง &mut T เข้าไปในฟังก์ชัน
fn add_one(n: &mut i32) { *n += 1;}
fn main() { let mut x = 41; let r = &mut x; add_one(&mut *r); // reborrow: a shorter-lived &mut carved out of r add_one(r); // r itself is usable again afterwards println!("{}", x);}43add_one(&mut *r) dereference r แล้ว re-borrow มันทันที — อายุของ reborrow นั้นจบลงเมื่อ add_one return ทำให้ r เองกลับมาใช้ได้อีกในบรรทัดถัดไป จริง ๆ แล้ว add_one(r) เดี่ยว ๆ ก็ทำ reborrow แบบ implicit อยู่แล้ว (การส่ง &mut T ไปยัง parameter ที่ต้องการ &mut T จะ reborrow อัตโนมัติ); รูปแบบ &mut *r ที่ชัดเจนมีไว้เพื่อความชัดเจนเป็นหลัก หรือตอนที่คุณต้อง reborrow ผ่าน indirection อีกชั้น เช่น &mut &mut T
Mutable References กับ Function Parameters
หัวข้อที่มีชื่อว่า “Mutable References กับ Function Parameters”การส่ง &mut T ให้ฟังก์ชันบ่งบอกว่าฟังก์ชัน อาจ แก้ไขค่านั้น ทำให้การ mutate ชัดเจนและมองเห็นได้ที่จุดเรียก — คุณเห็น &mut ในทุกจุดที่ค่าอาจเปลี่ยน
fn double(n: &mut i32) { *n *= 2; // dereference to modify the value behind the reference}
fn main() { let mut value = 7; double(&mut value); println!("{}", value); // 14}สังเกต dereference operator *n: การแก้ไขค่าที่ mutable reference ชี้ไป คุณต้อง dereference มันก่อน สำหรับ method ของ String อย่าง .push_str() Rust จะ dereference ให้อัตโนมัติผ่านฟีเจอร์ที่เรียกว่า auto-deref
Iterator Invalidation
หัวข้อที่มีชื่อว่า “Iterator Invalidation”for x in &v ถือ shared borrow ของ v ไว้ตลอดทั้ง loop การพยายาม mutate v จากข้างใน loop นั้นคือการละเมิด exclusivity แบบเดียวกับก่อนหน้า เพียงแต่สังเกตยากกว่าเพราะ borrow เป็น implicit อยู่ใน syntax for แทนที่จะเป็น &v ชัดเจนใน let:
// @expect-error E0502fn main() { let mut v = vec![1, 2, 3]; for x in &v { v.push(*x); // ERROR: mutating v while iterating over &v } println!("{:?}", v);}error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable --> src/main.rs:4:9 |3 | for x in &v { | -- | | | immutable borrow occurs here | immutable borrow later used here4 | v.push(*x); // ERROR: mutating v while iterating over &v | ^^^^^^^^^^ mutable borrow occurs here
error: aborting due to 1 previous errorนี่ไม่ใช่ borrow checker ที่ระวังเกินเหตุ: Vec reallocate ใหม่ได้ เมื่อมันขยายขนาด ย้าย element ทั้งหมดไปยัง heap buffer ใหม่ — pointer ภายในของ iterator จะเหลือแค่ชี้ไปยังหน่วยความจำที่คืนไปแล้ว compile error นี้แทนที่บั๊ก use-after-free จริงที่ภาษาอื่นปล่อยให้คุณเขียนได้ ทางแก้แทบทุกครั้งคือ collect สิ่งที่ต้องการ push ใส่ Vec แยกต่างหากก่อน แล้วค่อย extend หลัง loop: let mut to_add = Vec::new(); for x in &v { to_add.push(*x); } v.extend(to_add);
Move, Assign, และ Extract ผ่าน Borrow ที่ยังมีชีวิตอยู่
หัวข้อที่มีชื่อว่า “Move, Assign, และ Extract ผ่าน Borrow ที่ยังมีชีวิตอยู่”error code สามตัวที่เกี่ยวข้องกันครอบคลุมวิธีที่โค้ดพยายามทำให้ค่าที่ reference ยังพึ่งพาอยู่ใช้งานไม่ได้ ทุกตัวมีรูปแบบเดียวกัน: มี borrow อยู่ แล้วมีอะไรบางอย่างพยายามเปลี่ยนสิ่งที่มันชี้ไป
E0505 — move ค่าออกมาทั้งที่ยังมี borrow ที่มีชีวิตอยู่:
// @expect-error E0505fn consume(v: Vec<i32>) { println!("consumed len {}", v.len());}
fn main() { let v = vec![1, 2, 3]; let r = &v; consume(v); // moving v while r is still borrowing it println!("{:?}", r);}error[E0505]: cannot move out of `v` because it is borrowed --> src/main.rs:9:13 |7 | let v = vec![1, 2, 3]; | - binding `v` declared here8 | let r = &v; | -- borrow of `v` occurs here9 | consume(v); // moving v while r is still borrowing it | ^ move out of `v` occurs here10| println!("{:?}", r); | - borrow later used hereแก้โดยจัดโครงสร้างใหม่ให้ last use ของ borrow เกิดก่อนการ move (NLL จะปล่อยให้ borrow จบทันเวลา):
fn consume(v: Vec<i32>) { println!("consumed len {}", v.len());}
fn main() { let v = vec![1, 2, 3]; let r = &v; println!("{:?}", r); // last use of r ends its borrow here (NLL) consume(v); // fine now — no live borrow of v}ถ้า last use ของ borrow ไม่สามารถย้ายมาก่อนได้จริง ๆ ในฟังก์ชันที่ยาวกว่านี้ std::mem::take (จากบทก่อนหน้า) ให้ผลลัพธ์เดียวกันโดยไม่ต้องจัดลำดับใหม่: มัน move ค่าออกมาแล้วทิ้งค่า default ที่ valid ไว้แทน ทำให้ไม่มีจังหวะไหนที่ค่าถูก borrow และ move พร้อมกันเลย
E0506 — assign ให้ตัวแปรทั้งที่มันยังถูก borrow อยู่:
// @expect-error E0506fn main() { let mut s = String::from("hello"); let r = &s; s = String::from("replaced"); // assigning to s while r borrows it println!("{} {}", r, s);}error[E0506]: cannot assign to `s` because it is borrowed --> src/main.rs:4:5 |3 | let r = &s; | -- `s` is borrowed here4 | s = String::from("replaced"); // assigning to s while r borrows it | ^ `s` is assigned to here but it was already borrowed5 | println!("{} {}", r, s); | - borrow later used hereแก้โดยจัดโครงสร้างใหม่ให้ last use ของ r มาก่อน assignment อีกครั้ง:
fn main() { let mut s = String::from("hello"); let r = &s; println!("{}", r); // last use of r ends its borrow here (NLL) s = String::from("replaced"); // fine now — no live borrow of s println!("{}", s);}E0507 — move ค่าออกจาก field ของ struct (หรือ index) ที่เข้าถึงผ่าน shared reference:
// @expect-error E0507struct Wrapper { text: String,}
fn main() { let w = Wrapper { text: String::from("inside") }; let r = &w; let taken = r.text; // moving out of *r, a borrowed value println!("{}", taken);}error[E0507]: cannot move out of `r.text` which is behind a shared reference --> src/main.rs:9:17 |9 | let taken = r.text; // moving out of *r, a borrowed value | ^^^^^^ move occurs because `r.text` has type `String`, which does not implement the `Copy` traitตรงนี้ clone คือทางแก้แบบ idiomatic — คุณ move field ออกจากข้างหลัง reference ใด ๆ ไม่ได้เลย ไม่ว่าจะ borrow อยู่หรือไม่ ดังนั้นการ copy อย่างชัดเจนคือเครื่องมือที่ถูกต้อง ไม่ใช่ทางลัด:
struct Wrapper { text: String,}
fn main() { let w = Wrapper { text: String::from("inside") }; let r = &w; let taken = r.text.clone(); // clone instead of moving out from behind &w println!("{} {}", taken, w.text); // both usable — w still owns its field}fn append_world(s: &mut String) { s.push_str(", world");}
fn main() { let mut greeting = String::from("hello"); println!("before: {}", greeting); append_world(&mut greeting); println!("after: {}", greeting);
// One mutable borrow at a time let r1 = &mut greeting; r1.push_str("!"); println!("mutated: {}", r1); // r1 is no longer used here (NLL ends the borrow)
let r2 = &greeting; // shared borrow is fine now println!("shared: {}", r2);}Compiling…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ให้คิดว่าทุกค่ามี lock แบบ single-writer ที่คอมไพเลอร์ (ไม่ใช่ OS) บังคับใช้ทั้งหมดตอน compile time แบบไม่มีต้นทุน: ผู้อ่านมีกี่คนก็ได้พร้อมกัน แต่ทันทีที่ผู้เขียนหนึ่งคนถือ lock ไว้ ทุกคนอื่น — ทั้งผู้อ่านและผู้เขียน — ต้องรอจนกว่า last use ของผู้เขียนคนนั้นจะจบ ทั้ง NLL และ two-phase borrow มีไว้เพื่อทำให้ “รอจนกว่า last use” รบกวนน้อยที่สุด กฎจึงรู้สึกเหมือนหายไปสำหรับโค้ดที่เรียงลำดับกันจริง ๆ และจะกัดก็ต่อเมื่อ borrow สองตัวซ้อนกันในเวลาจริง ๆ เท่านั้น
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”กฎ exclusivity คือสิ่งที่ทำให้ Rust แบบไม่มี unsafe ปลอดภัยจาก data race และ iterator invalidation และ NLL/two-phase borrow ตัด false positive เกือบทั้งหมดที่ borrow checker เวอร์ชันเก่าแบบ lexical เคยสร้าง สิ่งที่เหลืออยู่คือต้นทุนจริง ที่บางครั้งน่าหงุดหงิด: pattern ที่มนุษย์มองว่าปลอดภัยชัดเจน — mutate field หนึ่งของ struct พร้อมกับอ่านอีก field หนึ่ง หรือจัดโครงสร้าง loop ที่ทั้งอ่านและ mutate collection แบบมีเงื่อนไข — บางครั้งยังต้องเขียนใหม่ (split borrow บน field, collect-แล้ว-extend บน collection) แค่เพื่อให้ checker ที่ให้เหตุผลกับค่าทั้งก้อน ไม่ใช่ byte เฉพาะที่มนุษย์มองเห็นว่าไม่เกี่ยวข้องกัน พอใจ
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “mutable reference สองตัวไม่เป็นไร ตราบใดที่ฉันไม่ได้ใช้ทั้งคู่จริง ๆ”
E0499เกิดขึ้นทันทีที่ borrow ทั้งสองมีชีวิตอยู่พร้อมกัน ไม่ว่าคุณจะเคยเรียก method ผ่านตัวที่สองหรือไม่ก็ตาม — คอมไพเลอร์ track scope ของ borrow ไม่ใช่ว่าคุณเคยอ่านจาก reference ตัวไหนจริง ๆ - “
v.push(v.len())ต้องเป็นการเจาะจงกรณีพิเศษแบบ hack ไม่ใช่กฎจริง” มันเป็นฟีเจอร์ที่ตั้งใจและมีชื่อเรียก — two-phase borrows — ไม่ใช่ความไม่สอดคล้องกัน&mutที่.push()ต้องการจะถูก จองไว้ เท่านั้นระหว่างที่ argument ของมันยัง evaluate อยู่ และจะ exclusive เต็มตัวก็ต่อเมื่อการเรียกเกิดขึ้นจริงเท่านั้น - “การ iterate ด้วย
for x in &vแล้ว mutatevข้างใน loop ไม่เป็นไร ตราบใดที่ไม่แตะ element ที่กำลังดูอยู่” คอมไพเลอร์ให้เหตุผลในระดับนั้นไม่ได้ — และมีเหตุผลที่ดี:v.push(...)อาจกระตุ้นให้ reallocate ทั้งหมด ทำให้ address ของทุก element ใช้ไม่ได้ ไม่ใช่แค่ตัวที่คุณคิดว่าจะไม่แตะ
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”AI assistant มักถือ reference เข้าไปใน Vec ไว้ (บ่อยครั้งผ่านการ index, &v[0]) ข้ามการเรียกที่ทำให้ reallocate ได้ อย่าง .push() — โค้ดดูปกติดีเพราะค่าที่ index นั้น “ไม่น่าจะเปลี่ยน” แต่คอมไพเลอร์กำลังให้เหตุผลกับ address ของ buffer ไม่ใช่ค่า
🐛 หา bug ในโค้ดนี้
fn main() { let mut v = vec![1, 2, 3]; let first = &v[0]; v.push(4); println!("first is: {}", first);}เฉลย
โค้ดนี้ compile ไม่ผ่าน — error[E0502]: cannot borrow \v` as mutable because it is also borrowed as immutable— และนั่นคือประเด็นสำคัญ: มันคือ pattern ที่ AI assistant เขียนพอดีเมื่อสมมติว่า&v[0]เป็นค่าคงที่ ไม่ใช่ borrow ที่มีชีวิตอยู่ของ buffer ที่vเป็นเจ้าของอยู่ตอนนี้.push(4)อาจทำให้vต้อง allocate buffer ใหม่ที่ใหญ่กว่าแล้วย้าย element ทุกตัวเข้าไป ซึ่งจะทำให้firstชี้ไปยังหน่วยความจำที่คืนไปแล้วถ้าคอมไพเลอร์อนุญาต ทางแก้คืออ่านv[0] เป็นค่าที่เป็นเจ้าของก่อนการ push (let first = v[0];สำหรับ element ที่เป็นCopyหรือ.clone()สำหรับตัวที่ไม่ใช่) หรือจัดลำดับใหม่ให้ last use ของfirstเกิดก่อนv.push(4)`