Ownership และ Borrowing
Ownership คืออะไร?
หัวข้อที่มีชื่อว่า “Ownership คืออะไร?”ฟีเจอร์ที่โดดเด่นที่สุดของ Rust คือ ระบบ ownership — ชุดของกฎที่คอมไพเลอร์บังคับใช้ตอน compile time เพื่อรับประกันความปลอดภัยของหน่วยความจำโดยไม่ต้องใช้ garbage collector
หน่วยความจำทุกส่วนในโปรแกรม Rust จะถูกเป็นเจ้าของ (owned) โดยตัวแปรเพียงตัวเดียวในแต่ละช่วงเวลา เมื่อตัวแปรนั้นออกจาก scope ไป Rust จะคืนหน่วยความจำให้อัตโนมัติ ไม่มีการจดบันทึกตอน runtime ไม่มี GC pause และไม่มีโอกาสเกิดบั๊กแบบ use-after-free หรือ double-free เลย — คอมไพเลอร์ตัดปัญหาเหล่านี้ออกไปทั้งหมด
กฎสามข้อของ Ownership
หัวข้อที่มีชื่อว่า “กฎสามข้อของ Ownership”- ทุกค่าใน Rust มี เจ้าของเพียงหนึ่งเดียว (one owner)
- เมื่อเจ้าของ ออกจาก scope ค่านั้นจะถูก dropped (หน่วยความจำถูกคืน)
- Ownership สามารถถูก ถ่ายโอน (move) ไปยังเจ้าของใหม่ได้ — เมื่อถึงจุดนั้นเจ้าของเดิมจะใช้งานไม่ได้อีกต่อไป
กฎสามข้อนี้คือรากฐานทั้งหมด ทุกอย่างที่เหลือในโมดูลนี้ — moves, references, mutable borrows และ slices — ล้วนต่อยอดมาจากกฎเหล่านี้
fn main() { // Rule 1: one owner let s = String::from("hello");
// Rule 2: out of scope → dropped { let inner = String::from("inner"); println!("{}", inner); } // inner is freed here
// Rule 3: ownership transferred to t let t = s; println!("{}", t); // s is no longer valid here}โมดูลนี้ครอบคลุมอะไรบ้าง
หัวข้อที่มีชื่อว่า “โมดูลนี้ครอบคลุมอะไรบ้าง”| บทเรียน | แนวคิด |
|---|---|
| Ownership และ Moves | แต่ละค่ามีเจ้าของหนึ่งเดียว; การ assign จะ move ค่าที่ไม่ใช่ type Copy |
| References และ Borrowing | borrow ค่าด้วย &T โดยไม่ต้องเอา ownership ไป |
| Mutable Borrows | แก้ไขค่าผ่าน &mut T; กฎการเข้าถึงแบบ exclusive |
| Slices | borrow ส่วนที่ต่อเนื่องกันของ collection ด้วย &[T] หรือ &str |
| Smart Pointers | Box, Rc/Weak, RefCell, Cell, Arc — การจอง heap, shared ownership, interior mutability |
ทำไมถึงไม่มี Garbage Collector?
หัวข้อที่มีชื่อว่า “ทำไมถึงไม่มี Garbage Collector?”GC จะสแกนหน่วยความจำตอน runtime เพื่อหาค่าที่เข้าถึงไม่ได้แล้วคืนหน่วยความจำ วิธีนี้เพิ่ม overhead ตอน runtime และทำให้เกิด pause ที่คาดเดาไม่ได้ Rust ย้ายงานทั้งหมดนี้ไปทำตอน compile time แทน: คอมไพเลอร์จะติดตามทุกการถ่ายโอน ownership และทุกการออกจาก scope แล้วแทรกการเรียก drop ให้คุณเอง ผลลัพธ์คือประสิทธิภาพระดับ C พร้อมความปลอดภัยของหน่วยความจำอย่างสมบูรณ์
fn main() { // Rule 1: every value has exactly one owner let owner = String::from("owned by owner"); println!("Rule 1: {}", owner);
// Rule 2: when the owner goes out of scope, the value is dropped { let scoped = String::from("scoped value"); println!("Rule 2 inside scope: {}", scoped); } // scoped is dropped here — memory is freed println!("Rule 2: scoped is gone");
// Rule 3: ownership can be transferred (moved) let new_owner = owner; println!("Rule 3: now {}", new_owner); // 'owner' is no longer valid here}Compiling…
ต้องอ่านบทไหน?
หัวข้อที่มีชื่อว่า “ต้องอ่านบทไหน?”ทุกบรรทัดของโค้ด Rust ที่แตะค่าใดค่าหนึ่ง จริง ๆ แล้วกำลังตอบคำถามเดียว: ตอนนี้ฉันต้องทำอะไรกับค่านี้? คำตอบจะพาไปยังหนึ่งในสี่บทของโมดูลนี้
flowchart TD
Q{What do you need from the value?}
Q -->|Give it away for good| MOVE[Move ownership - this lesson, Ownership and Moves]
Q -->|Just read it| REF[Shared reference, read-only - References and Borrowing]
Q -->|Change it in place| MUT[Exclusive reference, mutable - Mutable Borrows]
Q -->|Only need part of a collection| SLICE[Borrowed view - Slices] ถ้ายังไม่แน่ใจ ค่าเริ่มต้นของ Rust แบบ idiomatic เกือบทุกครั้งคือ borrow ก่อนเสมอ (&T หรือ &mut T ถ้าจำเป็นต้อง mutate) แล้วค่อยเลือก move เมื่อฟังก์ชันที่เรียกต้องเป็นเจ้าของค่านั้นจริง ๆ เท่านั้น — เช่น เก็บมันไว้ใน struct หรือส่งข้าม thread
Compiler พูดว่าอะไร: การใช้ค่าที่ถูก Move ไปแล้ว
หัวข้อที่มีชื่อว่า “Compiler พูดว่าอะไร: การใช้ค่าที่ถูก Move ไปแล้ว”error แรกที่ผู้เรียน Rust แทบทุกคนเจอคือการใช้ค่าหลังจากมันถูก move ไปแล้ว ตัวอย่างนี้ใช้ Vec<i32> แทน String เพื่อแสดงว่ากฎนี้อยู่ที่ความเป็น Copy ของ type ไม่ใช่เรื่องเฉพาะของ string:
// @expect-error E0382fn main() { let v1 = vec![1, 2, 3]; let v2 = v1; // v1 moves into v2 — Vec<i32> is not Copy println!("{:?}", v1); // ERROR: v1 was moved println!("{:?}", v2);}error[E0382]: borrow of moved value: `v1` --> src/main.rs:4:22 |2 | let v1 = vec![1, 2, 3]; | -- move occurs because `v1` has type `Vec<i32>`, which does not implement the `Copy` trait3 | let v2 = v1; // v1 moves into v2 — Vec<i32> is not Copy | -- value moved here4 | println!("{:?}", v1); // ERROR: v1 was moved | ^^ value borrowed here after move |help: consider cloning the value if the performance cost is acceptable |3 | let v2 = v1.clone(); // v1 moves into v2 — Vec<i32> is not Copy | ++++++++สังเกตว่าคอมไพเลอร์เอ่ยชื่อกฎตรง ๆ: Vec<i32> “does not implement the Copy trait” บทถัดไป Ownership และ Moves จะพูดถึงเรื่องนี้ทั้งบท ว่า type ไหนเป็น Copy และทำไม
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ให้คิดว่า ownership เป็นต้นไม้ที่มีเจ้าของเพียงคนเดียว ไม่ใช่ตาข่ายของ pointer ในทุกขณะ แต่ละค่าจะมีตัวแปรเพียงหนึ่งเดียวที่รับผิดชอบการคืนหน่วยความจำของมัน — การ move ค่าคือการย้ายความรับผิดชอบนั้นไปยังตัวแปรใหม่ ไม่เคยทำสำเนาซ้ำ ส่วนการ borrow (&T และ &mut T) ไม่แตะต้นไม้นี้เลย มันแค่เปิดหน้าต่างชั่วคราวที่คอมไพเลอร์ตรวจสอบแล้วไปยังค่าที่คนอื่นยังเป็นเจ้าของอยู่ และหน้าต่างนั้นรับประกันว่าจะปิดก่อนที่เจ้าของจะปิดมันเอง
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”การเช็ก ownership ตอน compile time มีต้นทุนจริง: มันต้องใช้ความคิดล่วงหน้ามากกว่าภาษาที่มี garbage collector และคอมไพเลอร์จะปฏิเสธบาง pattern ที่จริง ๆ ปลอดภัยแต่มันพิสูจน์ไม่ได้ (กราฟที่แชร์ mutable state คือตัวอย่างคลาสสิก — Rust ผลักเรื่องนี้ไปให้ Rc/RefCell ซึ่งจะพูดถึงทีหลังในคอร์ส) สิ่งที่ได้กลับมาคือ overhead ตอน runtime สำหรับความปลอดภัยของหน่วยความจำเป็นศูนย์ — ไม่มี GC pause ไม่มีภาษีของการนับ reference โดย default — และบั๊กทั้งหมวด (use-after-free, double-free, data race บนข้อมูลที่แชร์กัน) จะคอมไพล์ไม่ผ่านเลย
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “การ move ค่าคือการ copy มัน เหมือนกับการ assign ในภาษาอื่น ๆ ส่วนใหญ่” ตรงกันข้ามคือความจริงโดย default: การ assign จะ move ค่าที่ไม่ใช่
Copyและทำให้ตัวเดิมใช้งานไม่ได้ มีแค่ type ที่ implementCopyอย่างชัดเจนเท่านั้นที่จะได้สำเนาแบบ bitwise แทน — บทถัดไปจะบอกว่า type ไหนเข้าเงื่อนไขนี้และทำไม - “กฎสามข้อนี้แปลว่า Rust แอบ copy ข้อมูลอยู่เบื้องหลังเพื่อความปลอดภัย” ไม่มี copy แบบซ่อนเกิดขึ้นเลย คอมไพเลอร์จะถ่ายโอน ownership เท่านั้น (ถูก: มัน copy แค่ header ขนาดคงที่เล็ก ๆ ไม่เคย copy ข้อมูลบน heap) หรือไม่ก็ให้คุณ borrow — การเรียก
.clone()อย่างชัดเจนเท่านั้นที่จะได้สำเนาจริงของข้อมูล - “พอเข้าใจ syntax
&แล้ว ก็เข้าใจ ownership ทั้งหมดแล้ว” syntax คือส่วนที่ง่ายที่สุด กฎที่ใช้เวลาซึมซับนานที่สุดคือ invariant เรื่อง exclusivity ที่บังคับใช้ตลอดทั้งฟังก์ชัน — ว่าค่าหนึ่งมีผู้อ่านได้หลายคนหรือผู้เขียนได้คนเดียว ไม่เคยทั้งสองอย่างพร้อมกัน — ซึ่งบท Mutable Borrows จะลงลึกเรื่องนี้
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”AI assistant มักเขียน function signature ที่รับ ownership ไปเลย (s: String) ทั้งที่ body ของฟังก์ชันแค่อ่านค่านั้นเท่านั้น โค้ดแบบนี้ compile ผ่านสบาย ๆ จึงไม่มีอะไรเตือนอัตโนมัติ — แต่มันบังคับให้ caller ทุกคนต้อง move ค่าของตัวเองทิ้งไป หรือไม่ก็ต้องเติม .clone() ที่ไม่จำเป็น
🐛 หา bug ในโค้ดนี้
fn print_summary(title: String) { println!("Summary: {}", title);}
fn main() { let title = String::from("Quarterly Report"); print_summary(title); // println!("{}", title); // would not compile — title was moved for no reason}เฉลย
print_summary ไม่เคย mutate หรือเก็บ title ไว้เลย — มันแค่อ่านครั้งเดียวเพื่อ print การรับ String แบบ by value บังคับให้ caller ต้องสละ ownership (หรือ clone) แค่เพื่อเรียกฟังก์ชันแบบอ่านอย่างเดียว ทางแก้ที่บทถัดไปจะพูดถึงคือรับ reference แทน: fn print_summary(title: &str) เรียกด้วย print_summary(&title) — caller จะยังใช้ title ต่อได้ และไม่มีการ copy ข้อมูลบน heap เลย