Ownership คืออะไร
กฎ 3 ข้อของ Ownership
จำกฎ 3 ข้อนี้ให้ขึ้นใจ:
- ทุกค่ามีเจ้าของ (owner) หนึ่งเดียว
- ในเวลาใดก็ตาม ค่าหนึ่งมีได้แค่เจ้าของเดียว
- เมื่อเจ้าของออกจาก scope ค่าจะถูก drop (ลบ)
⚠️ คำเตือน: ข้อผิดพลาดที่พบบ่อย
- ❌ ใช้ตัวแปรหลัง move
- ❌ ส่ง ownership เข้า function แล้วพยายามใช้ต่อ
- ❌ สร้าง multiple mutable references
- ✅ ใช้
.clone()เมื่อต้องการ copy จริงๆ- ✅ ใช้ references (
&) แทน move เมื่อไม่จำเป็นต้องโอน ownership
🔄 Ownership Flow Diagram
+-------------------------------------------------------------------+
| Ownership Flow Visualization |
+-------------------------------------------------------------------+
| |
| let s1 = String::from("hello"); |
| | |
| v |
| +-------+ |
| | s1 | <-- owner of "hello" |
| +---+---+ |
| | |
| | let s2 = s1; (MOVE) |
| | |
| v |
| +-------+ +-------+ |
| | s1 | --> | s2 | <-- new owner |
| | DEAD | | OK | |
| +-------+ +-------+ |
| (invalid) (valid) |
| |
+-------------------------------------------------------------------+
| Alternatives: |
| 1. CLONE: let s2 = s1.clone(); -> s1 OK, s2 OK (both copies) |
| 2. BORROW: let s2 = &s1; -> s1 OK, s2 OK (temporary ref) |
+-------------------------------------------------------------------+
ตัวอย่างพื้นฐาน
fn main() {
{
let s = String::from("hello"); // s เกิดขึ้น เป็น owner ของ "hello"
println!("{}", s); // ใช้ s ได้
} // s ออกจาก scope -> memory ถูก free อัตโนมัติ
// println!("{}", s); // ❌ Error! s ไม่มีแล้ว
}
Stack vs Heap
เพื่อเข้าใจ Ownership ต้องเข้าใจ Stack และ Heap ก่อน:
Stack
- เก็บข้อมูลขนาดคงที่
- เร็วมาก
- LIFO (Last In, First Out)
- เช่น:
i32,f64,bool,char
Heap
- เก็บข้อมูลขนาดไม่คงที่
- ช้ากว่า Stack
- ต้อง allocate และ deallocate
- เช่น:
String,Vec<T>
+-----------------+
| Stack |
+-----------------+
| ptr ----------+ +---------------+
| len = 5 | | Heap |
| capacity = 5 | +---------------+
+----------------+------>| h e l l o |
| |
+---------------+
Copy Types vs Move Types
Copy Types (อยู่บน Stack)
ข้อมูลขนาดเล็กถูก copy อัตโนมัติ:
fn main() {
let x = 5;
let y = x; // copy ค่า
println!("x = {}, y = {}", x, y); // ✅ ทั้งสองใช้ได้
}
| Copy Types |
|---|
| i32, u32, i64, etc. |
| f32, f64 |
| bool |
| char |
| Tuples ที่มี Copy types เท่านั้น |
Move Types (อยู่บน Heap)
ข้อมูลบน Heap ถูก move:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 ถูก MOVE ไป s2
// println!("{}", s1); // ❌ Error! s1 ถูก move ไปแล้ว
println!("{}", s2); // ✅ OK
}
ทำไมต้อง Move?
ถ้า Rust copy ข้อมูลบน Heap แทนที่จะ move:
+-------------+
s1 ---->| hello |<---- s2 (if copy)
+-------------+
When s1 and s2 go out of scope
-> Both try to free the same memory
-> DOUBLE FREE!
Rust ป้องกันโดยทำให้ s1 ใช้ไม่ได้หลัง move
Ownership และ Functions
ส่งค่าเข้า function = Move
fn main() {
let s = String::from("hello");
takes_ownership(s); // s ถูก move เข้า function
// println!("{}", s); // ❌ Error! s ถูก move ไปแล้ว
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string ถูก drop ที่นี่
Return = ย้าย ownership กลับ
fn main() {
let s1 = gives_ownership(); // s1 ได้รับ ownership
println!("{}", s1); // ✅ OK
}
fn gives_ownership() -> String {
let s = String::from("hello");
s // return และ move ownership
}
ตัวอย่าง: ส่งและรับกลับ
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length) // return tuple
}
ปัญหา: ต้อง return ค่ากลับมาถ้าต้องการใช้ต่อ ยุ่งยากมาก!
ทางออก: ใช้ References (บทถัดไป)
Scope และ Drop
fn main() {
let outer = String::from("outer");
{
let inner = String::from("inner");
println!("{}", inner); // ✅ OK
} // inner ถูก drop ที่นี่
// println!("{}", inner); // ❌ inner ไม่มีแล้ว
println!("{}", outer); // ✅ OK
} // outer ถูก drop ที่นี่
ลองทำดู! 🎯
- ลองเขียนโค้ดที่ move String และดู error
- แก้ไขโดยใช้
.clone() - ลองส่ง String เข้า function และดูว่าเกิดอะไร
🧠 Advanced: Ownership Edge Cases
Case 1: Partial Move
struct Person {
name: String,
age: u32,
}
fn main() {
let person = Person {
name: String::from("Alice"),
age: 30,
};
let name = person.name; // move name ออกจาก struct
// println!("{}", person.name); // ❌ Error! name ถูก move แล้ว
println!("{}", person.age); // ✅ OK! age ยังอยู่ (Copy type)
}
Case 2: Reference ใน Struct
// ❌ ไม่ได้! struct เก็บ reference ต้องมี lifetime
struct BadStruct {
data: &str, // Error: missing lifetime specifier
}
// ✅ ถูกต้อง
struct GoodStruct<'a> {
data: &'a str,
}
Case 3: เมื่อใดใช้ Clone vs Reference
| สถานการณ์ | ใช้ | เหตุผล |
|---|---|---|
| ข้อมูลเล็ก ใช้หลายที่ | .clone() | overhead น้อย |
| ข้อมูลใหญ่ อ่านอย่างเดียว | &T | ประหยัด memory |
| ข้อมูลใหญ่ ต้อง modify | &mut T หรือ move | ขึ้นกับ use case |
| ส่งข้าม threads | Arc<T> + clone | shared ownership |
📝 ทดสอบความเข้าใจ
Q1: ทำไม i32 copy ได้แต่ String copy ไม่ได้?
A: i32 เก็บบน stack มีขนาดคงที่ copy ได้เร็ว ส่วน String เก็บ pointer ไปยัง heap ถ้า copy แบบ shallow ทั้งสองตัวแปรจะชี้ไปที่ memory เดียวกัน เกิด double free
Q2: ถ้าต้องการให้หลายตัวแปร "เป็นเจ้าของ" ข้อมูลเดียวกัน ทำยังไง?
A: ใช้ Rc<T> (single thread) หรือ Arc<T> (multi-thread) เพื่อ shared ownership โดยนับ references
Q3: เมื่อไหร่ควรใช้ .clone() vs borrow?
A:
- Clone: เมื่อ data เล็ก หรือต้องการ ownership แยกจริงๆ
- Borrow: เมื่อ data ใหญ่ หรือแค่ต้องการอ่าน/เขียนชั่วคราว
สรุป
| แนวคิด | คำอธิบาย |
|---|---|
| Owner | ตัวแปรที่ “เป็นเจ้าของ” ค่า |
| Move | ย้าย ownership |
| Drop | ลบข้อมูลเมื่อออกจาก scope |
| Stack | ข้อมูลขนาดเล็ก copy ได้ |
| Heap | ข้อมูลขนาดใหญ่ต้อง move |
👉 ต่อไป: Move & Clone