Ownership และ Moves
เจ้าของเดียวในเวลาเดียว
หัวข้อที่มีชื่อว่า “เจ้าของเดียวในเวลาเดียว”ค่าทุกตัวใน Rust มีเจ้าของเพียงหนึ่งเดียว เมื่อคุณ assign ค่าให้ตัวแปรอื่น หรือส่งมันเข้าไปในฟังก์ชัน สิ่งที่เกิดขึ้นขึ้นอยู่กับว่า type นั้น implement Copy หรือไม่
Type ที่ไม่ใช่ Copy จะถูก Move
หัวข้อที่มีชื่อว่า “Type ที่ไม่ใช่ Copy จะถูก Move”Type ที่เป็นเจ้าของหน่วยความจำบน heap — เช่น String, Vec<i32>, หรือ Box<i32> — จะ ไม่ implement Copy เมื่อคุณ assign ตัวหนึ่งให้ตัวแปรอื่น ค่านั้นจะถูก move: binding เดิมจะใช้งานไม่ได้อีกต่อไป
// @expect-error E0382fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 is moved into s2
println!("{}", s1); // ERROR: s1 was moved println!("{}", s2); // fine}การใช้ s1 หลังจากถูก move คือ compile-time error:
error[E0382]: borrow of moved value: `s1` --> src/main.rs:6:20 |3 | let s1 = String::from("hello"); | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait4 | let s2 = s1; // s1 is moved into s2 | -- value moved here5 |6 | println!("{}", s1); // ERROR: s1 was moved | ^^ value borrowed here after move |help: consider cloning the value if the performance cost is acceptable |4 | let s2 = s1.clone(); // s1 is moved into s2 | ++++++++ทางแก้ที่คอมไพเลอร์แนะนำเองคือ .clone() — และนั่นคือกับดักพอดี: การหยิบ .clone() มาใช้ทุกครั้งที่ borrow checker บ่นคือ “smell” ที่พบบ่อยที่สุดในโค้ด Rust ที่ AI เขียน มันทำให้ error หายไปก็จริง แต่บ่อยครั้งมันแปลว่าโค้ดควร borrow (&s1) ตั้งแต่แรกแทนที่จะ move ให้มอง .clone() เป็นการทำ deep copy อย่างตั้งใจสำหรับตอนที่คุณต้องการมันจริง ๆ — ไม่ใช่ปฏิกิริยาสะท้อนเพื่อทำให้ error หายไป
กฎเดียวกันนี้ใช้ตอนที่คุณส่งค่าเข้าไปในฟังก์ชันด้วย:
fn consume(s: String) { println!("{}", s);} // s is dropped here
fn main() { let name = String::from("Rust"); consume(name); // name is moved // name is no longer valid here}สิ่งที่ Move เกิดขึ้นจริง ๆ ในหน่วยความจำ
หัวข้อที่มีชื่อว่า “สิ่งที่ Move เกิดขึ้นจริง ๆ ในหน่วยความจำ”error ข้างบนทำให้เกิดคำถามที่ชัดเจน: ถ้า s2 = s1 ทำงานถูกกว่านี้ แล้ว byte ของ "hello" เกิดอะไรขึ้น? String ไม่ใช่ตัว text เอง — มันคือ header ขนาดเล็กคงที่ที่อยู่บน stack: pointer ไปยัง heap buffer, ความยาว, และ capacity วัดจริงบน toolchain นี้:
fn main() { println!("size_of::<String>() = {}", std::mem::size_of::<String>());}size_of::<String>() = 24field ประเภท usize สามตัว (pointer, length, capacity) ตัวละ 8 byte บน target 64-bit — รวม 24 byte ส่วน byte จริงของ UTF-8 (h, e, l, l, o) อยู่แยกต่างหากบน heap
let s2 = s1; copy แค่ header 24 byte นั้นจาก stack slot ของ s1 ไปยัง stack slot ของ s2 มันไม่แตะ byte บน heap เลย — การ copy มันจะสิ้นเปลืองโดยใช่เหตุ และนั่นคือสิ่งที่ .clone() มีไว้สำหรับตอนที่คุณต้องการมันจริง ๆ
flowchart TB
subgraph Stack
H1[s1 header - ptr len 5 cap 5 - marked moved, unusable]
H2[s2 header - ptr len 5 cap 5 - now the owner]
end
subgraph Heap
BUF[heap buffer - 5 bytes - h e l l o]
end
H1 -. compiler forbids reading this .-> BUF
H2 -->|owns it, will free it on drop| BUF นี่คือเหตุผลว่าทำไมคอมไพเลอร์ต้องปฏิเสธการใช้ s1 ต่อจากนั้น แทนที่จะปล่อยให้มันเป็น pointer ซ้ำที่ไม่มีพิษภัย: ทั้ง s1 และ s2 จะชี้ไปยัง heap buffer เดียวกันเป๊ะ ถ้าทั้งคู่ออกจาก scope ได้ ทั้งคู่จะพยายามคืน pointer เดียวกัน — เกิด double free แทนที่จะเป็นแบบนั้น คอมไพเลอร์ track s1 ว่า “moved-from” และห้ามการใช้งานมันทุกจุดตอน compile time ทำให้มีแค่ destructor ของ s2 เท่านั้นที่จะรัน
Copy Types ถูกทำซ้ำ
หัวข้อที่มีชื่อว่า “Copy Types ถูกทำซ้ำ”Type สเกลาร์ง่าย ๆ ที่อยู่บน stack ทั้งหมด — integer, bool, char, floating-point, และ tuple ของสิ่งเหล่านี้ — implement Copy การ assign หรือส่งพวกมันคือการทำ bitwise copy; ตัวเดิมยังใช้งานได้ต่อ
fn main() { let x: i32 = 42; let y = x; // x is COPIED, not moved println!("x={} y={}", x, y); // both valid}Copy type ที่พบบ่อย: i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool, char
Copy กับ Clone: ทำไม Derive ทั้งคู่พร้อมกันไม่ได้เสมอไป
หัวข้อที่มีชื่อว่า “Copy กับ Clone: ทำไม Derive ทั้งคู่พร้อมกันไม่ได้เสมอไป”Copy เป็น marker trait: มันบอกคอมไพเลอร์ว่า “การ assign ทำสำเนาแบบ bit-for-bit ได้แทนการ move” ซึ่งจะถูกต้องก็ต่อเมื่อทุก field ของ type นั้นเป็น Copy เอง — type ที่เป็นเจ้าของ heap allocation อย่าง String หรือ Vec<T> จะเป็น Copy ไม่ได้เลย เพราะสำเนาแบบ bitwise จะสร้างเจ้าของสองตัวให้ heap buffer เดียวกัน (ปัญหา double-free จากหัวข้อก่อนหน้า) Copy ยังต้องมี Clone เป็น supertrait ด้วย: ทุก Copy type ต้อง Clone ได้ด้วย เพราะ bitwise copy ก็ถือเป็น “clone” ที่ถูกต้องอยู่แล้ว
การพยายาม derive Copy บน struct ที่มี field ที่ไม่ใช่ Copy คือ compile error ไม่ใช่ warning:
// @expect-error E0204#[derive(Clone, Copy)]struct UserId { label: String, id: u32,}
fn main() { let u = UserId { label: String::from("alice"), id: 1 }; println!("{} {}", u.label, u.id);}error[E0204]: the trait `Copy` cannot be implemented for this type --> src/main.rs:2:8 |1 | #[derive(Clone, Copy)] | ---- in this derive macro expansion2 | struct UserId { | ^^^^^^3 | label: String, | ------------- this field does not implement `Copy`
error: aborting due to 1 previous errorทางแก้แทบไม่เคยเป็น “ทำให้ String implement Copy” (มันทำไม่ได้ ด้วยเหตุผล double-free เดียวกัน) — แต่คือการเอา #[derive(Copy)] ออกแล้วเก็บแค่ #[derive(Clone)] ไว้ แล้วเรียก .clone() อย่างชัดเจนตรงจุดที่ต้องการ copy อิสระจริง ๆ เท่านั้น
Deep Copy อย่างชัดเจนด้วย .clone()
หัวข้อที่มีชื่อว่า “Deep Copy อย่างชัดเจนด้วย .clone()”เมื่อคุณต้องการ copy อิสระเต็มรูปแบบของค่าที่เป็นเจ้าของ heap ให้เรียก .clone() นี่คือการ opt-in อย่างชัดเจนให้กับ operation ที่อาจมีต้นทุนสูง
fn main() { let original = String::from("keep me"); let copy = original.clone(); // deep copy println!("original={} copy={}", original, copy);}ลำดับการ Drop และ RAII
หัวข้อที่มีชื่อว่า “ลำดับการ Drop และ RAII”ค่าใด ๆ ที่มี resource ต้องปล่อย — หน่วยความจำ heap, file handle, lock — สามารถ implement trait Drop ได้ และ Rust จะเรียก drop ให้อัตโนมัติเมื่อเจ้าของค่าออกจาก scope pattern นี้ ที่การได้มาซึ่ง resource กับการเป็นเจ้าของค่าคือการกระทำเดียวกัน เรียกว่า RAII (Resource Acquisition Is Initialization): คุณไม่ต้องเรียก method close() หรือ free() เองเลย เพราะคอมไพเลอร์รู้อยู่แล้วว่า scope ของแต่ละค่าจบตรงไหนเป๊ะ
ลำดับ ที่ Drop รันคือกฎจริงที่สังเกตเห็นได้ ไม่ใช่รายละเอียด implementation:
- ตัวแปร local จะ drop ตาม ลำดับย้อนกลับของการประกาศ (ประกาศทีหลังสุด, drop ก่อนสุด)
- field ของ struct จะ drop ตามลำดับที่ประกาศ (field แรกก่อน)
- ค่า temporary ที่ไม่มีชื่อ (เช่น
Guard("...")ที่ใช้ตรง ๆ ใน expression) จะ drop ที่ จุดจบของ statement ที่มันปรากฏอยู่
struct Guard(&'static str);
impl Drop for Guard { fn drop(&mut self) { println!("dropping {}", self.0); }}
struct Holder { first: Guard, second: Guard,}
fn main() { let _a = Guard("a (local, declared first)"); let _b = Guard("b (local, declared second)"); let h = Holder { first: Guard("h.first"), second: Guard("h.second") }; println!("holder fields: {} / {}", h.first.0, h.second.0); println!("temporary: {}", Guard("temporary (end of this statement)").0); println!("end of main");}holder fields: h.first / h.secondtemporary: temporary (end of this statement)dropping temporary (end of this statement)end of maindropping h.firstdropping h.seconddropping b (local, declared second)dropping a (local, declared first)Guard ที่ไม่มีชื่อจะ drop ทันทีหลัง statement println! ที่สร้างมัน — ก่อน main จะจบนานมาก จากนั้น ณ จุดจบจริงของ main field ของ h จะ drop จากตัวแรกไปตัวที่สอง (ตามลำดับประกาศ) แล้วค่อยถึงคิว local _a กับ _b ที่ drop โดยประกาศทีหลังสุดก่อน (_b ก่อน _a)
mem::take, mem::replace, และ mem::swap
หัวข้อที่มีชื่อว่า “mem::take, mem::replace, และ mem::swap”บางครั้งคุณต้อง move ค่าออกจาก place ที่คุณไม่ได้เป็นเจ้าของแบบเต็ม ๆ — field ของ struct หรือ mutable reference — ในขณะที่ต้องทิ้งอะไรที่ valid ไว้แทน (คุณทิ้ง field ของ struct ให้ว่างเปล่าจริง ๆ ไม่ได้ ทุก field ต้องมีค่าของ type ของมันตลอดเวลา) ฟังก์ชันใน std::mem มีไว้เพื่อสิ่งนี้พอดี:
use std::mem;
fn main() { // mem::take leaves the default value (empty String) behind let mut s1 = String::from("original"); let taken = mem::take(&mut s1); println!("taken={:?} s1={:?}", taken, s1);
// mem::replace swaps in a new value and gives back the old one let mut s2 = String::from("old value"); let old = mem::replace(&mut s2, String::from("new value")); println!("old={:?} s2={:?}", old, s2);
// mem::swap exchanges the contents of two bindings in place let mut a = String::from("a-value"); let mut b = String::from("b-value"); mem::swap(&mut a, &mut b); println!("a={:?} b={:?}", a, b);}taken="original" s1=""old="old value" s2="new value"a="b-value" b="a-value"mem::take(&mut place) move ค่าของ place ออกมาแล้วทิ้ง Default::default() ไว้แทน — ใช้ได้เฉพาะ type ที่ implement Default ซึ่ง String กับ Vec<T> ทำได้ (string ว่าง, vec ว่าง) mem::replace(&mut place, new) คือรูปแบบทั่วไปกว่า: มันทิ้งค่า new ที่คุณระบุไว้ ไม่ใช่แค่ค่า default ของ type mem::swap(&mut a, &mut b) สลับค่าของสองตัวโดยไม่มีตัวไหนกลายเป็น “moved-from” ที่ใช้งานไม่ได้เลย — มีประโยชน์กับ algorithm แบบ in-place ที่ต้องสลับ field ของ struct หรือ element ของ vector
Partial Moves
หัวข้อที่มีชื่อว่า “Partial Moves”การ move field เดียวออกจาก struct หรือ tuple จะทำให้ เฉพาะ field นั้น ใช้งานไม่ได้ — ส่วนที่เหลือยังใช้ได้ตามปกติ แม้ว่าตัว struct ทั้งก้อนจะใช้งานหรือ move ต่อไม่ได้อีกแล้ว:
struct Pair { a: String, b: String,}
fn main() { let p = Pair { a: String::from("a"), b: String::from("b") }; let taken = p.a; // partial move: only p.a moves out println!("{} {}", taken, p.b); // p.b is still usable}บท Structs จะพูดถึงเรื่องนี้ลึกกว่านี้ รวมถึงว่า partial move มีผลกับ struct update syntax (..base) ยังไง — เวอร์ชันสั้น ๆ ตรงนี้เพียงพอให้จำ pattern นี้ได้เมื่อเจอ
การคืน Ownership
หัวข้อที่มีชื่อว่า “การคืน Ownership”ฟังก์ชันสามารถคืน ownership ให้ caller ได้โดยการ return ค่านั้น:
fn take_and_give_back(s: String) -> String { s // ownership moves out of the function}
fn main() { let s1 = String::from("hello"); let s2 = take_and_give_back(s1); println!("{}", s2);}วิธีนี้ถูกต้องแต่ยืดยาว ในทางปฏิบัติ คุณจะเลือก borrowing (บทถัดไป) มากกว่าการ move-แล้ว-return แทบทุกครั้ง
fn take_ownership(s: String) -> String { println!("inside take_ownership: {}", s); s // return ownership to caller}
fn main() { // String is NOT Copy — it moves let s1 = String::from("hello"); let s2 = take_ownership(s1); // s1 is moved println!("s2 now owns: {}", s2);
// i32 IS Copy — it is copied let x = 42; let y = x; println!("x={} y={}", x, y);
// .clone() makes an explicit deep copy let original = String::from("keep me"); let cloned = original.clone(); println!("original={} cloned={}", original, cloned);}Compiling…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”การ move คือการปลูกถ่าย header ไม่ใช่การ copy ข้อมูล: สำหรับ type ที่เป็นเจ้าของ heap มีแค่ record ขนาดคงที่เล็ก ๆ (pointer/length/capacity) เท่านั้นที่เปลี่ยนมือ และคอมไพเลอร์ลบความสามารถในการใช้ชื่อเดิม เพื่อไม่ให้มีจังหวะไหนที่สองชื่ออ้าง heap buffer เดียวกันพร้อมกัน Copy type ข้ามขั้นตอนนี้ไปเลยเพราะไม่มี heap buffer ให้เป็นเจ้าของซ้ำสอง — bitwise duplicate ปลอดภัยเสมอ คอมไพเลอร์เลยทำมันเงียบ ๆ แทนที่จะบังคับให้คุณ track ว่าชื่อไหน “current” อยู่
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”การ move เป็น default นั้นเกือบไม่มีต้นทุนเลย (copy header 24 byte ไม่ใช่ข้อมูลจริง) และตัดปัญหา use-after-free กับ double-free ออกไปตอน compile time — แต่มันแปลว่าการส่งค่า heap ขนาดใหญ่เข้าฟังก์ชันแบบ เอา ownership ไป ทั้งที่ตั้งใจแค่จะอ่าน จะบังคับให้ caller ทุกคนต้องตัดสินใจเรื่องการออกแบบที่ไม่จำเป็น .clone() ซื้อความยืดหยุ่นคืนมาด้วยต้นทุน runtime ที่มองเห็นได้จริง (heap allocation เต็มรูปแบบและ copy ทีละ byte) ซึ่งเป็นเหตุผลพอดีว่าทำไมภาษาถึงทำให้มันเป็นการเรียก method อย่างชัดเจน แทนที่จะเกิดขึ้นแบบ implicit ตอน assign เหมือนภาษาที่ใช้ reference semantics ทุกที่
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
.clone()คือทางแก้ที่คอมไพเลอร์แนะนำ มันต้องถูกต้องแน่ ๆ” คำแนะนำนั้นคือทางแก้ แบบหนึ่ง ที่ใช้ได้เสมอ ไม่ใช่ทางแก้ที่ ดีที่สุด เสมอไป ส่วนใหญ่ move error แปลว่าโค้ดควร borrow (&s1) แทนการเอา ownership ไป — หยิบ.clone()มาใช้เฉพาะตอนที่คุณต้องการ copy อิสระสองชุดที่แยกกันพัฒนาต่อได้จริง ๆ - “struct ที่มี field เป็น
Stringแค่เติม#[derive(Copy)]ก็ใช้ได้เหมือน struct เล็ก ๆ ที่เคยเห็น”Copyต้องการให้ทุก field เป็นCopyและString/Vec<T>/type ที่เป็นเจ้าของ heap ใด ๆ ไม่เคยเป็นเลย —#[derive(Copy)]บน struct แบบนั้นคือerror[E0204]ไม่ใช่ warning ที่เพิกเฉยได้ - “ลำดับ
Dropไม่สำคัญ ตราบใดที่สุดท้ายทุกอย่างถูกเก็บกวาด” มันสำคัญทุกครั้งที่ destructor ของค่าหนึ่งขึ้นอยู่กับอีกค่าที่ยังต้อง valid อยู่ — struct ที่ถือ database connection กับ struct ที่ถือ transaction บน connection นั้นต้อง drop ตามลำดับที่ถูกต้อง และกฎตายตัวของ Rust (field ตามลำดับประกาศ, local ย้อนกลับ) คือสิ่งที่ทำให้ลำดับนั้นคาดเดาได้ แทนที่จะขึ้นกับ implementation
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”รูปแบบที่พบบ่อยที่สุดของ clone-to-silence smell คือการ clone collection ทั้งก้อนแค่เพื่อ iterate อ่านมันเฉย ๆ — การ clone เพิ่ม deep copy เต็มรูปแบบโดยไม่ได้ประโยชน์อะไรเลย เพราะการ iterate ไม่เคยต้องการ ownership ตั้งแต่แรก
🐛 หา bug ในโค้ดนี้
fn total_length(names: &Vec<String>) -> usize { let cloned = names.clone(); // unnecessary — we only read it cloned.iter().map(|s| s.len()).sum()}
fn main() { let names = vec![String::from("Ada"), String::from("Grace")]; println!("{}", total_length(&names));}เฉลย
names เป็น &Vec<String> อยู่แล้ว — reference แบบอ่านอย่างเดียว การเรียก .clone() จะ deep-copy String ทุกตัวใน vector (แต่ละตัวคือ heap allocation แยกกัน) แค่เพื่อเรียก .iter() บน copy นั้น ทั้งที่ .iter() ทำงานเหมือนกันทุกประการบน reference ตัวเดิม ทางแก้คือลบบรรทัด .clone() ทิ้งไปเลย แล้วเรียก names.iter() ตรง ๆ — ส่วนที่เหลือของฟังก์ชันไม่ต้องเปลี่ยนอะไรเลย