Skip to content

Latest commit

 

History

History
369 lines (270 loc) · 16.2 KB

File metadata and controls

369 lines (270 loc) · 16.2 KB

02 — Ownership & Borrowing (Python → Rust Core)

TOC · Prev · Next

Keywords: ownership, borrowing, lifetimes, concurrency, playbook

อ่านแบบคน Python:

  • ถ้าอยากเอา “ภาพรวม” ก่อน: โฟกัสว่าโค้ดตรงไหน “ถือของเอง” vs “ยืมมาใช้”
  • ถ้าอยาก “ลงมือทำ”: คัดลอก snippet ทีละอัน แล้วสังเกตว่าจุดไหน compiler กันบั๊กให้
  • ถ้าติดข้อความ error: เปิด 12-learning-playbook.md

บทนี้คือ “คีย์หลัก” ที่ทำให้คุณเขียน Rust ได้จริง โดยเฉพาะเวลาคุณเริ่มมี:

  • ส่งข้อมูลไปมาหลายฟังก์ชัน
  • state มากขึ้น (และเริ่มกระจาย)
  • โค้ดโตจนต้อง refactor
  • หรือเริ่มแตะ concurrency

สิ่งที่อยากให้คุณได้จากบทนี้:

  • แยกให้ขาดว่า “ตอนนี้เราถือข้อมูล (owned) อยู่” หรือ “แค่ยืมมาใช้ (borrowed) อยู่”
  • เข้าใจ move/clone/Copy แบบที่เอาไปแก้ปัญหาได้จริง
  • อ่าน error ของ borrow checker แล้วรู้ว่า compiler กำลังป้องกันบั๊กอะไร

อ้างอิงหลัก:


1) Mental model: Python vs Rust

1.1 Python (ที่คุณคุ้น)

แนวคิดที่ควรรู้เวลานึกภาพ:

  • ตัวแปรใน Python คือ “ชื่อ” ที่ชี้ไป object
  • การ assign ส่วนใหญ่คือ “ผูกชื่อใหม่” กับ object เดิม (alias)
  • ส่ง dict/list ไปไหนก็แก้ได้ง่าย → โค้ดไหลเร็ว แต่ state อาจถูกแก้จากที่ไม่คาดคิด

ตัวอย่าง (Python):

s1 = ["hi"]
s2 = s1
s2.append("!")
print(s1)

Output (example):

['hi', '!']

อ่านให้ลึกขึ้นอีกนิด:

  • s1 กับ s2 ไม่ได้ “คัดลอก list” แต่เป็นชื่อสองชื่อที่ชี้ object เดียวกัน
  • บั๊กที่ชอบเกิดคือ: ฟังก์ชัน A ส่ง list ให้ฟังก์ชัน B แล้ว B แก้ list ทำให้ A แปลกใจตอนใช้ต่อ

Python bug → Rust blocks: hidden mutation ผ่าน alias (แก้จากที่ไม่คิด)

Python ที่พบบ่อยในงานจริง:

  • ฟังก์ชันดูเหมือน “แค่อ่าน” แต่เผลอแก้ list/dict ที่รับเข้ามา

ตัวอย่าง (Python):

def add_bang(xs: list[str]) -> None:
    xs.append("!")  # แก้ของ caller โดยตรง

items = ["hi"]
add_bang(items)
print(items)  # ['hi', '!'] (caller อาจไม่คาดหวัง)

Rust บังคับให้คุณ “ประกาศเจตนา” ว่าจะอ่านหรือแก้:

  • ถ้าแค่อ่าน → รับ &Vec<T> / &[T]
  • ถ้าจะแก้ → ต้องรับ &mut Vec<T> (caller ต้องส่ง &mut แบบตั้งใจ)

ตัวอย่าง (Rust):

fn add_bang(xs: &mut Vec<String>) {
    xs.push("!".to_string());
}

fn main() {
    let mut items = vec!["hi".to_string()];
    add_bang(&mut items);
    println!("{:?}", items);
}

1.2 Rust

Rust เลือกทำให้ “ความรับผิดชอบต่ออายุของข้อมูล” ชัดเจน:

  • ค่าหนึ่งมี owner เดียวในเชิง “ใครรับผิดชอบการ drop”
  • การยืม (&T, &mut T) คือการ “ขอสิทธิ์” อ่าน/แก้แบบชั่วคราวตามกฎที่ปลอดภัย

ผลลัพธ์ที่ได้:

  • ลด use-after-free / double-free / data race (ใน safe code) ตั้งแต่ compile time

ภาพสรุปเร็ว:

  • Python: “แชร์ได้ง่าย” → ต้องระวัง side effects
  • Rust: “บังคับให้ชัด” → ช้าตอนเริ่ม แต่คุมระบบใหญ่ได้ดี

2) Owned vs Borrowed: String และ &str

จุดที่คนมาจาก Python มักงงคือ Rust มีทั้ง String และ &str

2.1 String = owned heap string

  • เป็น “เจ้าของ” ข้อมูล string บน heap
  • เก็บใน struct/state ได้ดี (ไม่ผูกอายุไว้กับ input)
  • สามารถถูก move ได้ (ย้าย ownership)

2.2 &str = borrowed string slice

  • เป็น “มุมมอง (view)” ของข้อมูล string ที่มี owner อยู่แล้ว
  • มักใช้เป็น input ของฟังก์ชัน เพราะไม่บังคับ caller ให้ต้อง allocate

Python (สื่อแนวคิด):

def greet(name: str) -> None:
    print(f"hi {name}")


def main() -> None:
    s = "world"
    greet(s)
    greet("rustacean")


main()

Output (example):

hi world
hi rustacean

Rust:

fn greet(name: &str) {
    println!("hi {name}");
}

fn main() {
    let s = String::from("world");
    greet(&s);       // &String coerces to &str
    greet("rustacean");
}

Output (example):

hi world
hi rustacean

แนวทาง:

  • รับ input จากภายนอก/ฟังก์ชัน: ชอบใช้ &str
  • เก็บใน struct/state: ชอบใช้ String

เหตุผลลึก ๆ:

  • &str ทำให้ API “รับได้กว้าง” (caller ส่ง String หรือ string literal ก็ได้)
  • String ทำให้ state “เป็นเจ้าของ” ข้อมูล ไม่ผูกอายุไว้กับ input ภายนอก

3) Move semantics (สิ่งที่ Python ไม่มีแบบเดียวกัน)

3.1 Python: assignment คือชื่อใหม่

ใน Python s2 = s1 แปลว่า “ชื่อใหม่ชี้ object เดิม”

  • ถ้า object เป็น mutable แล้วถูกแก้ → ทุกชื่อที่ชี้ object นั้นจะเห็นการเปลี่ยนแปลง

3.2 Rust: owned heap data มักถูก move เมื่อ assign

Rust ต้องการกันบั๊กคลาส “ใช้ค่าที่ถูก drop แล้ว” และ “double-free” ดังนั้น owned type บางตัว (เช่น String) จะถูก move เมื่อ assign

Rust:

fn main() {
    let s1 = String::from("hi");
    let s2 = s1; // move

    // println!("{s1}"); // error: use of moved value
    println!("{s2}");
}

Output (example):

hi

ทำไมต้อง move?

  • String มี pointer ไป heap
  • ถ้าอนุญาตให้สองตัวแปร “เป็นเจ้าของ” heap เดียวกันแบบ implicit → เสี่ยง double-free
  • Rust เลือก “ย้ายเจ้าของให้ชัด” แทน “แชร์แบบเผลอ ๆ”

4) Copy vs Clone vs Move (สรุปที่ใช้แก้งานจริง)

คนมาจาก Python มักคิดว่า “ก็ copy สิ” แต่ใน Rust ต้องแยก 3 คำนี้ให้ชัด เพราะแต่ละอย่างหมายถึงต้นทุน/พฤติกรรมต่างกัน

  • Move: ย้าย ownership (ค่าเดิมใช้ต่อไม่ได้)
  • Copy: คัดลอกแบบ bitwise ที่ถูกและปลอดภัย (เช่น i32, bool) → ใช้ต่อได้ทั้งสองฝั่ง
  • Clone: คัดลอกแบบ explicit (มักเป็น deep copy หรือ copy ที่มีต้นทุน) → ต้องเรียกเอง

ตัวอย่าง:

fn main() {
    let a: i32 = 10;
    let b = a; // Copy
    println!("{a} {b}");

    let s1 = String::from("hi");
    let s2 = s1.clone(); // Clone (explicit)
    println!("{s2}");
}

Output (example):

10 10
hi

ทิป practical (แก้ปัญหาได้จริง):

  • ถ้า compiler บอก “move occurs because ... does not implement Copy” ให้ถามตัวเองว่า
    • ต้องใช้ค่าต้นฉบับต่อจริงไหม? ถ้าไม่ → move ได้
    • ถ้าต้องใช้ต่อ → ยืม (&T) หรือ clone() แบบมีเหตุผล

แนวคิดสำคัญสำหรับคนมาจาก Python:

  • ใน Rust “clone ที่ถูกที่” มักเกิดที่ boundary (รับ input → เก็บลง state) มากกว่ากระจาย clone ทั่วโปรเจกต์

5) Borrowing rules (กฎเหล็ก 2 ข้อ)

Rust มี “กฎความปลอดภัย” ที่คุณต้องคุ้น เพราะมันคือหัวใจของ borrow checker:

  1. มี &T ได้หลายอันพร้อมกัน (อ่านพร้อมกันได้)
  2. มี &mut T ได้แค่อันเดียวในช่วงนั้น และห้ามมี &T ซ้อนช่วงเดียวกัน

5.1 ตัวอย่างที่ถูก (จัด scope ให้ชัด)

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

    let r = &s;      // read borrow
    println!("{r}"); // r ใช้เสร็จในบรรทัดนี้

    let m = &mut s;  // mutable borrow หลังจาก r หมดอายุแล้ว
    m.push_str("!");

    println!("{s}");
}

Output (example):

hello
hello!

5.2 ตัวอย่างที่ผิด (เพื่อให้เข้าใจ error)

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &mut s; // error
    println!("{r1} {r2}");
}

Output (example):

compile error (expected): cannot borrow `s` as mutable because it is also borrowed as immutable

คุณควรอ่าน error แบบนี้:

  • compiler ไม่ได้ “ห้ามเพราะงี่เง่า” แต่มันกำลังกันบั๊กแนว “อ่านข้อมูลขณะที่กำลังถูกแก้ไข”
  • ในภาษาอื่นบั๊กแบบนี้อาจหลุดเป็น race/logic bug ที่ debug ยากมาก

6) Pattern สำหรับคนเขียน Python: “ย้าย logic ออกจาก global”

ใน Python คุณมักเจอโปรเจกต์ที่:

  • state เป็น global แล้วถูกแก้หลายที่

มันเขียนเร็วตอนแรก แต่พอโตจะเริ่ม:

  • ยากต่อการไล่เหตุผลว่า “ทำไมค่าเปลี่ยน”
  • ยากต่อการ test

ใน Rust ถ้าคุณอยากให้โค้ดโตแบบไม่พัง:

  • สร้าง struct AppState
  • ส่ง &AppState ถ้าอ่าน
  • ส่ง &mut AppState ถ้าแก้

Skeleton:

struct AppState {
    counter: u64,
}

fn inc(state: &mut AppState) {
    state.counter += 1;
}

fn main() {
    let mut state = AppState { counter: 0 };
    inc(&mut state);
}

Output (example):

(no output — snippet only)

สิ่งที่ pattern นี้ช่วย:

  • คุณรู้ “ใครเป็นเจ้าของ state” และ “จุดไหนเปลี่ยน state”
  • borrow checker ช่วย enforce ว่าจุดที่แก้ state ต้องได้สิทธิ์ &mut จริง ๆ

7) เมื่อไรต้องใช้ Arc<Mutex<T>>

7.1 T คืออะไร (ไม่ใช่ตัวแปร)

T ใน Arc<T>, Mutex<T>, RwLock<T> คือ generic type parameter

  • เป็นเรื่องของ type ตอน compile-time ไม่ใช่ตัวแปรตอน runtime

ตัวอย่าง:

  • Arc<u64>
  • Mutex<Vec<String>>
  • RwLock<AppState>

7.2 ต้องแก้ปัญหาอะไรถึงต้องใช้ของพวกนี้

ใช้เมื่อคุณต้องมี “state ก้อนเดียว” ที่หลาย threads ต้องอ่าน/แก้ร่วมกัน (shared mutable state)

ภาพรวมแบบจับต้องได้:

  • Arc<T> = shared ownership แบบ thread-safe (refcount แบบ atomic)
  • Mutex<T> = กันการ “แก้พร้อมกัน” โดยบังคับให้เข้าแก้ได้ทีละ thread
  • RwLock<T> = เหมาะเมื่อ “อ่านเยอะ เขียนน้อย”: อ่านพร้อมกันได้หลาย thread แต่เขียนได้ทีละ thread

ทำไมต้องมี 2 ชั้น?

  • ถ้าไม่มี Arc คุณส่ง T เข้า thread ได้ แต่ “แชร์ owner เดียวกันหลาย thread” ไม่ได้
  • ถ้าไม่มี Mutex/RwLock คุณจะเจอ data race (Rust กันไว้ตั้งแต่ compile-time ใน safe code)

เทียบกับ Python (แนวคิดเดียวกัน):

  • Arc<T> ≈ การส่ง reference ของ object เดียวกันให้หลาย thread ใช้ร่วมกัน
  • Mutex<T>threading.Lock() + with lock:

หมายเหตุ: แม้ Python มี GIL แต่ shared mutable state ก็ยังทำให้ผลลัพธ์ผิด/แปลกได้ (interleaving) เลยยังต้องใช้ lock ในงานจริง

(บท 07-shared-state-and-concurrency.md จะขยายเรื่องนี้แบบเป็นระบบและปลอดภัย)


8) แบบฝึกหัด

  1. เขียนฟังก์ชัน fn add_suffix(name: &str) -> String ที่คืนค่า owned String
  2. สร้าง AppState { messages: Vec<String> } และฟังก์ชัน push_message(&mut AppState, &str)
  3. ลองทำให้เกิด borrow error 1 ครั้ง แล้วอ่าน error message ว่ามันกันอะไร

ทิป: ถ้า error มีรหัสเช่น E0502 ให้ลองรัน rustc --explain E0502 จะช่วยให้เห็นรูปแบบปัญหาเร็วขึ้น (ดู playbook ใน 12-learning-playbook.md ด้วย)