สรุปแนวทางการใช้งาน Ownership และ Borrowing ในภาษา Rust กันหน่อย

Ownership

โดยทั่วไปแล้วแนวคิด Ownership ใน Rust จะมองว่าทุกค่า (value) ใน Rust มีตัวแปรหนึ่งตัวที่เป็นเจ้าของ (owner) ของมัน ในเวลาใดเวลาหนึ่ง ค่า (value) จะมีเจ้าของเพียงตัวเดียวเท่านั้น เมื่อออกนอก scope ค่านั้นจะถูกทำลาย (drop) โดยอัตโนมัติ

กฎหลักของ Ownership

  • ค่าจะมีเจ้าของเพียงตัวเดียวในเวลาใดเวลาหนึ่ง
  • เมื่อเจ้าของ (ตัวแปร) ออกจาก scope ข้อมูลในหน่วยความจำจะถูกคืนอัตโนมัติ
  • ไม่สามารถมีการ copy ของค่าที่เป็น heap โดยอัตโนมัติ (ต้องใช้ Clone หรือ Copy trait)

ตัวอย่างเช่น

fn main() {
    let s1 = String::from("Hello"); // s1 เป็นเจ้าของ String นี้
    let s2 = s1; // Ownership ถูกย้าย (move) ไปที่ s2
    
    // println!("{}", s1); // ❌ Error: s1 ไม่เป็นเจ้าของอีกต่อไป
    println!("{}", s2); // ✅ ใช้ s2 ได้
}

String เก็บข้อมูลใน Heap ดังนั้น เมื่อเรา let s2 = s1; Rust จะ move ownership แทนการ copy เพื่อป้องกันการ free ซ้ำ

ถ้าอยากใช้ทั้ง s1 และ s2 ต้องใช้ clone()

fn main() {
    let s1 = String::from("Hello");
    let s2 = s1.clone(); // copy ข้อมูลจริงใน Heap
    
    println!("s1 = {}, s2 = {}", s1, s2); // ใช้ได้ทั้งคู่
}

Borrowing

Borrowing ใน Rust คือ การ “ยืมค่า” จากเจ้าของ (owner) โดยไม่ย้าย ownership ไปยังตัวแปรอื่น จุดประสงค์ คือ ให้ฟังก์ชันหรือโค้ดส่วนอื่นเข้าถึงข้อมูลชั่วคราว โดยไม่ต้องโอนความเป็นเจ้าของ

กฎหลักของ Borrowing

  1. Immutable references (&T):
    • ยืมค่า แบบอ่านอย่างเดียว
    • ยืมได้หลายตัวพร้อมกัน (read-only, ไม่มีปัญหา data race)
  1. Mutable references (&mut T):
    • ยืมค่า แบบแก้ไขได้
    • ยืมได้ แค่หนึ่งตัวในเวลาเดียวกัน (เพื่อป้องกันการแก้ไขพร้อมกัน)
  1. ห้ามมี mutable borrow พร้อมกับ immutable borrow ในเวลาเดียวกัน
    • เพื่อป้องกันการอ่านค่าที่กำลังถูกแก้ไข

ตัวอย่าง Borrow แบบอ่านอย่างเดียว (Immutable Borrow)

fn main() {
    let s = String::from("hello");

    print_length(&s);  // ยืม s
    println!("{}", s); // ✅ s ยังใช้ได้เพราะ ownership ไม่ถูก move
}

fn print_length(s: &String) {
    println!("Length: {}", s.len());
}

สิ่งสำคัญ: s ยังคงเป็นเจ้าของหลังจากส่ง &s ให้ฟังก์ชัน

ตัวอย่าง Borrow แบบแก้ไข (Mutable Borrow)

fn main() {
    let mut s = String::from("hello");

    modify(&mut s); // ส่ง mutable reference
    println!("{}", s); // ✅ s ยังเป็นเจ้าของ
}

fn modify(s: &mut String) {
    s.push_str(" world");
}

สิ่งสำคัญ: ต้องประกาศ mut ที่ตัวแปรด้วย (let mut s) เพื่อเป็นการบอกว่าตัวแปรนี้ อนุญาตให้แก้ไขได้

ตัวอย่าง ห้ามมี mutable borrow พร้อมกับ immutable borrow

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    // let r2 = &mut s; // ❌ Error: มี mutable borrow ซ้ำ

    println!("{}", r1);
}

หรือ

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    // let r3 = &mut s; // ❌ Error: immutable borrow + mutable borrow พร้อมกัน

    println!("{}, {}", r1, r2);
}

นอกจากกฏหลัก 3 ข้อข้างบนแล้ว ยังมีรายละเอียดอื่นๆ อีกด้วย เช่น

Borrowing + Lifetimes: ตัวที่ Reference ต้อง มีอายุไม่ยาวกว่าเจ้าของ และ Rust จะตรวจสอบให้โดยใช้ lifetimes

Borrowing ป้องกัน Dangling Pointer (Dangling References) และ Data Race

fn main() {
    let r;
    {
        let s = String::from("hello");
        r = &s; // ❌ Error: s หมดอายุแล้ว แต่ r ยัง reference อยู่
    }
    println!("{}", r);
}

Ownership vs Borrowing

จริงๆ คิดว่าถ้าเข้าใจแต่ละตัวแล้ว เราน่าจะแยกออกแล้วว่า แต่ละตัวต่างกันยังไง แต่ขอสรุปไว้ให้สักหน่อย

Ownership (เจ้าของค่า)

  • ใช้เมื่อเราต้องการ โอนสิทธิ์ความเป็นเจ้าของของค่า (move) ไปยังฟังก์ชันหรือโครงสร้างอื่น
  • ทุกค่ามีเจ้าของ (owner) เพียงตัวเดียว
  • เมื่อเจ้าของหมดอายุ (scope จบ) ค่าจะถูก free อัตโนมัติ
  • เหมาะในกรณี:
    • ค่าไม่ต้องใช้ที่ต้นทางอีก (move ไปเลย)
    • ต้องการหลีกเลี่ยงการ copy ข้อมูลขนาดใหญ่ (ใช้ move แทน clone)
    • ต้องการ transfer resource ownership อย่างปลอดภัย (เช่นเปิดไฟล์, network socket)
  • ป้องกัน memory leak และ double free โดย design

Borrowing (การยืมค่า)

  • ใช้เมื่อเราต้องการ ใช้ค่าที่มีอยู่โดยไม่ย้าย ownership
  • ใช้ & (reference) เพื่อยืมค่าแทนการย้าย (move)
  • มี 2 แบบ:Immutable borrow: &T (ยืมแบบอ่านได้เท่านั้น)Mutable borrow: &mut T (ยืมแบบแก้ไขได้ แต่มีได้แค่ 1 ในเวลาเดียวกัน)ไม่สามารถมี mutable และ immutable borrow พร้อมกัน ได้ → ป้องกัน data race
  • Lifetimes ใช้เพื่อระบุอายุของ reference เพื่อให้แน่ใจว่า reference ยัง valid อยู่

เหมาะในกรณี:

    • ต้องการอ่านค่าชั่วคราว โดยไม่เปลี่ยนแปลง (&T)
    • ต้องการแก้ไขค่าชั่วคราว โดยไม่ย้าย ownership (&mut T)
    • ต้องการส่งค่าหลายครั้งให้หลายฟังก์ชัน โดยไม่ copy
สถานการณ์ ใช้ Ownership ใช้ Borrowing
ส่งค่าขนาดใหญ่ไปฟังก์ชัน แล้วไม่ต้องใช้ที่เดิม
ส่งค่าขนาดใหญ่ไปฟังก์ชัน แต่ยังต้องใช้ที่เดิม
สร้าง object ใหม่ แล้วให้ struct เป็นเจ้าของ
อ่านค่าจาก struct หลายครั้ง ✅ (immutable borrow)
แก้ไขค่าผ่านฟังก์ชัน แต่ยังต้องใช้ต่อ ✅ (mutable borrow)

สรุปไว้ประมาณนี้ ในการใช้งาน rust นั้น เราจะต้องคิดเรื่องการ manage ตัวแปร ให้มีประสิทธิภาพที่สุด