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

Ownership และ Moves

ค่าทุกตัวใน Rust มีเจ้าของเพียงหนึ่งเดียว เมื่อคุณ assign ค่าให้ตัวแปรอื่น หรือส่งมันเข้าไปในฟังก์ชัน สิ่งที่เกิดขึ้นขึ้นอยู่กับว่า type นั้น implement Copy หรือไม่

Type ที่เป็นเจ้าของหน่วยความจำบน heap — เช่น String, Vec<i32>, หรือ Box<i32> — จะ ไม่ implement Copy เมื่อคุณ assign ตัวหนึ่งให้ตัวแปรอื่น ค่านั้นจะถูก move: binding เดิมจะใช้งานไม่ได้อีกต่อไป

// @expect-error E0382
fn 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` trait
4 | let s2 = s1; // s1 is moved into s2
| -- value moved here
5 |
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
}

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>() = 24

field ประเภท 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
การ move copy header 24 byte เท่านั้น ไม่เคย copy byte บน heap

นี่คือเหตุผลว่าทำไมคอมไพเลอร์ต้องปฏิเสธการใช้ s1 ต่อจากนั้น แทนที่จะปล่อยให้มันเป็น pointer ซ้ำที่ไม่มีพิษภัย: ทั้ง s1 และ s2 จะชี้ไปยัง heap buffer เดียวกันเป๊ะ ถ้าทั้งคู่ออกจาก scope ได้ ทั้งคู่จะพยายามคืน pointer เดียวกัน — เกิด double free แทนที่จะเป็นแบบนั้น คอมไพเลอร์ track s1 ว่า “moved-from” และห้ามการใช้งานมันทุกจุดตอน compile time ทำให้มีแค่ destructor ของ s2 เท่านั้นที่จะรัน

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 expansion
2 | 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 อิสระจริง ๆ เท่านั้น

เมื่อคุณต้องการ 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);
}

ค่าใด ๆ ที่มี 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.second
temporary: temporary (end of this statement)
dropping temporary (end of this statement)
end of main
dropping h.first
dropping h.second
dropping b (local, declared second)
dropping a (local, declared first)

Guard ที่ไม่มีชื่อจะ drop ทันทีหลัง statement println! ที่สร้างมัน — ก่อน main จะจบนานมาก จากนั้น ณ จุดจบจริงของ main field ของ h จะ drop จากตัวแรกไปตัวที่สอง (ตามลำดับประกาศ) แล้วค่อยถึงคิว local _a กับ _b ที่ drop โดยประกาศทีหลังสุดก่อน (_b ก่อน _a)

บางครั้งคุณต้อง 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

การ 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 ให้ 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);
}

การ move คือการปลูกถ่าย header ไม่ใช่การ copy ข้อมูล: สำหรับ type ที่เป็นเจ้าของ heap มีแค่ record ขนาดคงที่เล็ก ๆ (pointer/length/capacity) เท่านั้นที่เปลี่ยนมือ และคอมไพเลอร์ลบความสามารถในการใช้ชื่อเดิม เพื่อไม่ให้มีจังหวะไหนที่สองชื่ออ้าง heap buffer เดียวกันพร้อมกัน Copy type ข้ามขั้นตอนนี้ไปเลยเพราะไม่มี heap buffer ให้เป็นเจ้าของซ้ำสอง — bitwise duplicate ปลอดภัยเสมอ คอมไพเลอร์เลยทำมันเงียบ ๆ แทนที่จะบังคับให้คุณ track ว่าชื่อไหน “current” อยู่

การ 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 ทุกที่

  • “.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

รูปแบบที่พบบ่อยที่สุดของ 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() ตรง ๆ — ส่วนที่เหลือของฟังก์ชันไม่ต้องเปลี่ยนอะไรเลย

เกิดอะไรขึ้นเมื่อคุณ assign String ให้ตัวแปรใหม่ใน Rust?
type ใดต่อไปนี้ implement Copy?
คุณจะได้ copy แบบอิสระอย่างชัดเจนของ String ได้อย่างไร?
หลังจากส่ง String เข้าไปในฟังก์ชันที่เอา ownership ไป ผู้เรียกยังใช้ String ตัวนั้นได้อีกไหม?
โค้ดนี้ compile ไหม?
#[derive(Clone, Copy)]
struct UserId {
    label: String,
    id: u32,
}
fn main() {}
โปรแกรมนี้ print อะไร?
struct Guard(&'static str);
impl Drop for Guard {
    fn drop(&mut self) { println!("drop {}", self.0); }
}
fn main() {
    let _a = Guard("a");
    let _b = Guard("b");
}