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

Ownership และ Borrowing

ฟีเจอร์ที่โดดเด่นที่สุดของ Rust คือ ระบบ ownership — ชุดของกฎที่คอมไพเลอร์บังคับใช้ตอน compile time เพื่อรับประกันความปลอดภัยของหน่วยความจำโดยไม่ต้องใช้ garbage collector

หน่วยความจำทุกส่วนในโปรแกรม Rust จะถูกเป็นเจ้าของ (owned) โดยตัวแปรเพียงตัวเดียวในแต่ละช่วงเวลา เมื่อตัวแปรนั้นออกจาก scope ไป Rust จะคืนหน่วยความจำให้อัตโนมัติ ไม่มีการจดบันทึกตอน runtime ไม่มี GC pause และไม่มีโอกาสเกิดบั๊กแบบ use-after-free หรือ double-free เลย — คอมไพเลอร์ตัดปัญหาเหล่านี้ออกไปทั้งหมด

  1. ทุกค่าใน Rust มี เจ้าของเพียงหนึ่งเดียว (one owner)
  2. เมื่อเจ้าของ ออกจาก scope ค่านั้นจะถูก dropped (หน่วยความจำถูกคืน)
  3. 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 และ Borrowingborrow ค่าด้วย &T โดยไม่ต้องเอา ownership ไป
Mutable Borrowsแก้ไขค่าผ่าน &mut T; กฎการเข้าถึงแบบ exclusive
Slicesborrow ส่วนที่ต่อเนื่องกันของ collection ด้วย &[T] หรือ &str
Smart PointersBox, Rc/Weak, RefCell, Cell, Arc — การจอง heap, shared ownership, interior mutability

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
}

ทุกบรรทัดของโค้ด 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]
แผนภาพการตัดสินใจเรื่อง ownership

ถ้ายังไม่แน่ใจ ค่าเริ่มต้นของ Rust แบบ idiomatic เกือบทุกครั้งคือ borrow ก่อนเสมอ (&T หรือ &mut T ถ้าจำเป็นต้อง mutate) แล้วค่อยเลือก move เมื่อฟังก์ชันที่เรียกต้องเป็นเจ้าของค่านั้นจริง ๆ เท่านั้น — เช่น เก็บมันไว้ใน struct หรือส่งข้าม thread

error แรกที่ผู้เรียน Rust แทบทุกคนเจอคือการใช้ค่าหลังจากมันถูก move ไปแล้ว ตัวอย่างนี้ใช้ Vec<i32> แทน String เพื่อแสดงว่ากฎนี้อยู่ที่ความเป็น Copy ของ type ไม่ใช่เรื่องเฉพาะของ string:

// @expect-error E0382
fn 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` trait
3 | let v2 = v1; // v1 moves into v2 — Vec<i32> is not Copy
| -- value moved here
4 | 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 และทำไม

ให้คิดว่า ownership เป็นต้นไม้ที่มีเจ้าของเพียงคนเดียว ไม่ใช่ตาข่ายของ pointer ในทุกขณะ แต่ละค่าจะมีตัวแปรเพียงหนึ่งเดียวที่รับผิดชอบการคืนหน่วยความจำของมัน — การ move ค่าคือการย้ายความรับผิดชอบนั้นไปยังตัวแปรใหม่ ไม่เคยทำสำเนาซ้ำ ส่วนการ borrow (&T และ &mut T) ไม่แตะต้นไม้นี้เลย มันแค่เปิดหน้าต่างชั่วคราวที่คอมไพเลอร์ตรวจสอบแล้วไปยังค่าที่คนอื่นยังเป็นเจ้าของอยู่ และหน้าต่างนั้นรับประกันว่าจะปิดก่อนที่เจ้าของจะปิดมันเอง

การเช็ก ownership ตอน compile time มีต้นทุนจริง: มันต้องใช้ความคิดล่วงหน้ามากกว่าภาษาที่มี garbage collector และคอมไพเลอร์จะปฏิเสธบาง pattern ที่จริง ๆ ปลอดภัยแต่มันพิสูจน์ไม่ได้ (กราฟที่แชร์ mutable state คือตัวอย่างคลาสสิก — Rust ผลักเรื่องนี้ไปให้ Rc/RefCell ซึ่งจะพูดถึงทีหลังในคอร์ส) สิ่งที่ได้กลับมาคือ overhead ตอน runtime สำหรับความปลอดภัยของหน่วยความจำเป็นศูนย์ — ไม่มี GC pause ไม่มีภาษีของการนับ reference โดย default — และบั๊กทั้งหมวด (use-after-free, double-free, data race บนข้อมูลที่แชร์กัน) จะคอมไพล์ไม่ผ่านเลย

  • “การ move ค่าคือการ copy มัน เหมือนกับการ assign ในภาษาอื่น ๆ ส่วนใหญ่” ตรงกันข้ามคือความจริงโดย default: การ assign จะ move ค่าที่ไม่ใช่ Copy และทำให้ตัวเดิมใช้งานไม่ได้ มีแค่ type ที่ implement Copy อย่างชัดเจนเท่านั้นที่จะได้สำเนาแบบ bitwise แทน — บทถัดไปจะบอกว่า type ไหนเข้าเงื่อนไขนี้และทำไม
  • “กฎสามข้อนี้แปลว่า Rust แอบ copy ข้อมูลอยู่เบื้องหลังเพื่อความปลอดภัย” ไม่มี copy แบบซ่อนเกิดขึ้นเลย คอมไพเลอร์จะถ่ายโอน ownership เท่านั้น (ถูก: มัน copy แค่ header ขนาดคงที่เล็ก ๆ ไม่เคย copy ข้อมูลบน heap) หรือไม่ก็ให้คุณ borrow — การเรียก .clone() อย่างชัดเจนเท่านั้นที่จะได้สำเนาจริงของข้อมูล
  • “พอเข้าใจ syntax & แล้ว ก็เข้าใจ ownership ทั้งหมดแล้ว” syntax คือส่วนที่ง่ายที่สุด กฎที่ใช้เวลาซึมซับนานที่สุดคือ invariant เรื่อง exclusivity ที่บังคับใช้ตลอดทั้งฟังก์ชัน — ว่าค่าหนึ่งมีผู้อ่านได้หลายคนหรือผู้เขียนได้คนเดียว ไม่เคยทั้งสองอย่างพร้อมกัน — ซึ่งบท Mutable Borrows จะลงลึกเรื่องนี้

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 เลย

ใน Rust ค่าหนึ่งค่าสามารถมีเจ้าของได้กี่คนในเวลาเดียวกัน?
เกิดอะไรขึ้นกับค่าเมื่อเจ้าของของตัวเองออกจาก scope?
Rust ใช้กลไกใดเพื่อรับประกันความปลอดภัยของหน่วยความจำ?
โค้ดนี้ compile ไหม? ถ้าลองรันจะเกิดอะไรขึ้น?
fn main() {
    let a = vec![1, 2, 3];
    let b = a;
    println!("{:?}", a);
}
โปรแกรมนี้ print อะไร?
fn main() {
    let x = 5;
    let y = x;
    println!("{} {}", x, y);
}