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 กำลังป้องกันบั๊กอะไร
อ้างอิงหลัก:
แนวคิดที่ควรรู้เวลานึกภาพ:
- ตัวแปรใน 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);
}Rust เลือกทำให้ “ความรับผิดชอบต่ออายุของข้อมูล” ชัดเจน:
- ค่าหนึ่งมี owner เดียวในเชิง “ใครรับผิดชอบการ drop”
- การยืม (
&T,&mut T) คือการ “ขอสิทธิ์” อ่าน/แก้แบบชั่วคราวตามกฎที่ปลอดภัย
ผลลัพธ์ที่ได้:
- ลด use-after-free / double-free / data race (ใน safe code) ตั้งแต่ compile time
ภาพสรุปเร็ว:
- Python: “แชร์ได้ง่าย” → ต้องระวัง side effects
- Rust: “บังคับให้ชัด” → ช้าตอนเริ่ม แต่คุมระบบใหญ่ได้ดี
จุดที่คนมาจาก Python มักงงคือ Rust มีทั้ง String และ &str
- เป็น “เจ้าของ” ข้อมูล string บน heap
- เก็บใน struct/state ได้ดี (ไม่ผูกอายุไว้กับ input)
- สามารถถูก move ได้ (ย้าย ownership)
- เป็น “มุมมอง (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 ภายนอก
ใน Python s2 = s1 แปลว่า “ชื่อใหม่ชี้ object เดิม”
- ถ้า object เป็น mutable แล้วถูกแก้ → ทุกชื่อที่ชี้ object นั้นจะเห็นการเปลี่ยนแปลง
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 เลือก “ย้ายเจ้าของให้ชัด” แทน “แชร์แบบเผลอ ๆ”
คนมาจาก 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 ทั่วโปรเจกต์
Rust มี “กฎความปลอดภัย” ที่คุณต้องคุ้น เพราะมันคือหัวใจของ borrow checker:
- มี
&Tได้หลายอันพร้อมกัน (อ่านพร้อมกันได้) - มี
&mut Tได้แค่อันเดียวในช่วงนั้น และห้ามมี&Tซ้อนช่วงเดียวกัน
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!
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 ยากมาก
ใน 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จริง ๆ
T ใน Arc<T>, Mutex<T>, RwLock<T> คือ generic type parameter
- เป็นเรื่องของ type ตอน compile-time ไม่ใช่ตัวแปรตอน runtime
ตัวอย่าง:
Arc<u64>Mutex<Vec<String>>RwLock<AppState>
ใช้เมื่อคุณต้องมี “state ก้อนเดียว” ที่หลาย threads ต้องอ่าน/แก้ร่วมกัน (shared mutable state)
ภาพรวมแบบจับต้องได้:
Arc<T>= shared ownership แบบ thread-safe (refcount แบบ atomic)Mutex<T>= กันการ “แก้พร้อมกัน” โดยบังคับให้เข้าแก้ได้ทีละ threadRwLock<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 จะขยายเรื่องนี้แบบเป็นระบบและปลอดภัย)
- เขียนฟังก์ชัน
fn add_suffix(name: &str) -> Stringที่คืนค่า ownedString - สร้าง
AppState { messages: Vec<String> }และฟังก์ชันpush_message(&mut AppState, &str) - ลองทำให้เกิด borrow error 1 ครั้ง แล้วอ่าน error message ว่ามันกันอะไร
ทิป: ถ้า error มีรหัสเช่น E0502 ให้ลองรัน rustc --explain E0502 จะช่วยให้เห็นรูปแบบปัญหาเร็วขึ้น (ดู playbook ใน 12-learning-playbook.md ด้วย)