OB 001: การเป็นเจ้าของของ self ในฟังก์ชันเมมเบอร์
OB 001: การเป็นเจ้าของของ self ในฟังก์ชันเมมเบอร์ (Self Ownership)
ในหัวข้อนี้ เราจะมาเจาะลึกคำถามพื้นฐานที่สำคัญมากสำหรับการออกแบบ API ใน Rust นั่นคือ
“ถ้าเมธอดรับ
selfแปลว่าฟังก์ชันจะ ‘ยึดครอง’ (Take Ownership) ค่าที่เราส่งเข้าไปจนเราใช้ต่อไม่ได้… จริงหรือ?”
คำตอบของเรื่องนี้ไม่ได้มีแค่ “ใช่” หรือ “ไม่ใช่” แต่ขึ้นอยู่กับคุณสมบัติของ Type นั้นๆ และเหตุผลเบื้องหลังเรื่องประสิทธิภาพ (Performance) ที่ Rust ออกแบบมาอย่างตั้งใจ
จุดเริ่มต้นของความสงสัย
ลองดูตัวอย่างคลาสสิกจาก Standard Library ของ Rust คือฟังก์ชัน cos สำหรับคำนวณค่า cosine
#![allow(unused)]
fn main() {
// Signature ใน std library
pub fn cos(self) -> f32
}
สังเกตว่ามันรับ self (ไม่ใช่ &self) ซึ่งตามกฎ Ownership แล้ว การรับแบบนี้ควรจะเป็นการ Move หรือย้ายสิทธิ์ความเป็นเจ้าของเข้าไปในฟังก์ชัน
แต่เมื่อเราเขียนโค้ดจริง
fn main() {
let angle = 0.0f32;
let cosine = angle.cos(); // เรียกเมธอดที่รับ self
// เอ๊ะ! ทำไมยังเอา angle มาปริ้นต์ต่อได้? ไม่โดน Move ไปแล้วเหรอ?
println!("angle = {angle}, cosine = {cosine}");
}
โค้ดนี้กลับคอมไพล์ผ่านและทำงานได้ปกติ นี่จึงเป็นที่มาของความสับสนว่าตกลง self ทำงานอย่างไรกันแน่
กฎพื้นฐาน 3 ร่างของ self
ก่อนอื่นต้องแม่นยำเรื่อง Syntax ของ self ใน Rust เมธอดรับ self ได้ 3 แบบหลักๆ
1. self (Take Ownership)
- ความหมาย: ยึดครองความเป็นเจ้าของ (Take Ownership)
- ผลลัพธ์ต่อตัวแปรเดิม: ตัวแปรเดิมจะถูก Move (ย้ายสิทธิ์) หรือ Copy (ทำสำเนา) ไป
- สถานการณ์ที่ใช้: เมื่อต้องการ “กิน” ค่าเข้าไป (Consuming) หรือคำนวณแล้วคืนค่าใหม่ (Transformation)
2. &self (Shared Borrow)
- ความหมาย: ยืมอ่าน (Shared Borrow)
- ผลลัพธ์ต่อตัวแปรเดิม: ตัวแปรเดิมยังคงอยู่ และสามารถถูกอ่านจากที่อื่นพร้อมกันได้
- สถานการณ์ที่ใช้: เมื่อต้องการแค่อ่านค่าเพื่อคำนวณ แต่ไม่ต้องการแก้ไข
3. &mut self (Mutable Borrow)
- ความหมาย: ยืมไปแก้ (Mutable Borrow)
- ผลลัพธ์ต่อตัวแปรเดิม: ตัวแปรเดิมยังคงอยู่ แต่จะถูกล็อก (Lock) ห้ามใครใช้อ่านหรือเขียนแทรกชั่วคราว
- สถานการณ์ที่ใช้: เมื่อต้องการแก้ไขค่าภายใน (Update state)
ทำไม angle.cos() ถึงไม่กิน angle หายไป?
คำตอบอยู่ที่ Trait Copy ครับ
ใน Rust ชนิดข้อมูลพื้นฐาน (Primitives) ที่มีขนาดเล็ก เช่น f32, i32, bool, char จะถูก implement trait ที่ชื่อว่า Copy โดยอัตโนมัติ
ความลับของ Copy Trait (Semantics vs Mechanics)
นี่คือจุดที่น่าสนใจจากมุมมองเชิงลึก
- ในทางความหมาย (Semantics) การประกาศ
fn(self)คือการประกาศเจตนาว่า “ฉันต้องการครอบครองค่านี้” (Move semantics) เสมอ - ในทางกลไก (Mechanics) แต่ถ้า Type นั้นมี
Copytrait แปะอยู่ คอมไพเลอร์จะเปลี่ยนพฤติกรรมจากการ “ย้ายความเป็นเจ้าของ” (ทำให้ตัวเดิมใช้ไม่ได้) เป็นการ “ทำสำเนาบิต” (Bitwise Copy) เข้าไปแทน
ดังนั้น angle.cos() จึงแค่ “สำเนา” ค่าของ angle เข้าไปคำนวณ ไม่ได้ยึดตัวแปรต้นทางไป ตัวแปรเดิมจึงยังใช้งานได้ครับ
💡 Deep Dive: ทำไมใช้ self ถึงดีกว่า &self?
ทำไมฟังก์ชันคณิตศาสตร์อย่าง cos, sin, abs ถึงเลือกใช้ self? ทำไมไม่ใช้ &self เพื่อความชัดเจน? คำตอบคือ Performance ครับ
self(Pass-by-value) สำหรับข้อมูลขนาดเล็ก (เช่นf32= 4 bytes) CPU สามารถโหลดค่านี้เข้าไปใน Register ได้โดยตรงและคำนวณได้ทันที นี่คือวิธีที่เร็วที่สุด&self(Pass-by-reference) คอมพิวเตอร์ต้องส่ง Pointer (ซึ่งขนาด 8 bytes บนเครื่อง 64-bit) ไปที่ฟังก์ชัน จากนั้นฟังก์ชันต้องเสียเวลาวิ่งกลับไปอ่านค่าที่ Memory ปลายทางอีกที (Dereference)
การใช้ self กับ Primitive types จึงทั้งเร็วกว่าและประหยัดหน่วยความจำกว่าครับ
แล้วถ้า Type ไม่เป็น Copy ล่ะ?
สำหรับ Type ที่ซับซ้อน เช่น String, Vec<T> หรือ Struct ทั่วไป Rust จะ ไม่ ให้เป็น Copy โดยอัตโนมัติ
ถ้าเราประกาศเมธอดรับ self กับ Type เหล่านี้ จะเกิดการ Move ทันทีครับ
struct MyData(String);
impl MyData {
fn consume_me(self) { // รับ self และ Type นี้ไม่ใช่ Copy
println!("Inside: {}", self.0);
} // self ถูก Drop (ทำลาย) ทิ้งเมื่อจบฟังก์ชันนี้
}
fn main() {
let d = MyData(String::from("Hello"));
d.consume_me(); // Ownership ถูกย้ายเข้าไป
// println!("{:?}", d); // ❌ ERROR: value used here after move
}
⚠️ ระวังหลุมพราง “Silent Bug” ของ Copy Types
ข้อนี้สำคัญมาก และมักเป็นจุดตายของมือใหม่ คือการใช้ mut กับ self ใน Type ที่เป็น Copy
สิ่งที่มักเข้าใจผิด
เราอาจเผลอเขียนเมธอดที่รับค่าเข้ามาแก้ไข (Mutate) แต่ดันรับมาแบบ self (Pass by value)
#[derive(Clone, Copy, Debug)]
struct Point { x: i32, y: i32 }
impl Point {
// ⚠️ ระวัง! รับ mut self (Value) ไม่ใช่ &mut self (Reference)
fn move_wrong(mut self, dx: i32) {
self.x += dx;
// สิ่งที่ถูกแก้คือ "ตัวสำเนา" (Copy) ที่อยู่ในฟังก์ชันนี้เท่านั้น
} // ตัวสำเนาถูกทำลายทิ้งตรงนี้
}
fn main() {
let p = Point { x: 0, y: 0 };
p.move_wrong(10);
println!("{:?}", p); // 😱 ผลลัพธ์: Point { x: 0, y: 0 } ค่าไม่เปลี่ยน
}
วิธีแก้ไข
- ถ้าจะแก้ค่าเดิม ใช้
&mut selfเสมอ - ถ้าจะคืนค่าใหม่ (Functional style) รับ
selfแล้วคืนSelfกลับไป
#![allow(unused)]
fn main() {
impl Point {
// ✅ แบบคืนค่าใหม่
fn moved(self, dx: i32) -> Self {
Point { x: self.x + dx, y: self.y }
}
}
}
เมื่อไหร่ที่ไม่ควรทำเป็น Copy? (กรณีศึกษา Range)
บางครั้งข้อมูลมีขนาดเล็ก แต่เราก็ไม่ควรให้มันเป็น Copy ตัวอย่างที่ดีที่สุดคือ Range (เช่น 0..10)
ถึงแม้ Range จะเก็บแค่ตัวเลข start กับ end แต่ Rust ไม่ให้มันเป็น Copy เพราะมันทำหน้าที่เป็น Iterator ด้วย
#![allow(unused)]
fn main() {
// สมมติว่า Range เป็น Copy...
let mut r = 0..5;
for _ in r { ... } // r ถูก copy ไปใช้ใน loop จนหมด
// r ตัวเดิมยังอยู่ที่ 0..5 เหมือนเดิม เพราะไม่ได้ถูก Move ไป
for _ in r { ... } // ลูปทำงานซ้ำอีกรอบ!
}
พฤติกรรมนี้จะสร้างบั๊กที่ตามหาได้ยาก Rust จึงบังคับให้ Iterator ต้องถูก Move เสมอ เพื่อให้แน่ใจว่าสถานะการวนลูป (Current state) มีอยู่แค่ที่เดียว
สรุป Checklist ในการออกแบบ
เมื่อคุณเห็นหรือเขียน Method Signature ให้ตีความดังนี้
-
fn method(self)- ถ้าเป็น Copy Type แค่ทำสำเนาค่าไปใช้ (เช่น
angle.cos()) - ถ้าไม่ใช่ Copy Type ยึดครอง (Move) ต้นฉบับไปเลย (เช่น
vec.into_iter()) - เหมาะสำหรับ การแปลงค่า (
into_...) หรือการคำนวณที่คืนค่าใหม่
- ถ้าเป็น Copy Type แค่ทำสำเนาค่าไปใช้ (เช่น
-
fn method(&self)- ขอยืมดูเฉยๆ ไม่ว่าจะเป็น Type อะไร
- เหมาะสำหรับ การอ่านค่า, Getter (
.len(),.is_empty())
-
fn method(&mut self)- ขอยืมไปแก้ไข (Mutate)
- เหมาะสำหรับ การเปลี่ยน state ภายใน (
.push(),.clear())
Naming Convention Tips
into_*(เช่นinto_string) → กินค่า (self)to_*(เช่นto_string) → ยืมแล้วก๊อปปี้ (&self)as_*(เช่นas_bytes) → ยืมแล้วแปลงมุมมอง (&self)
แหล่งอ้างอิง:
- Rust Forum: Does a member function take ownership of a self argument?
- The Rust Programming Language - Ownership
- Rust Reference: Copy Trait
OB 002: Reborrowing ในเมธอดและการแปลงโครงสร้างข้อมูล
ในบทนี้ เราจะมาเจาะลึกแนวคิดเรื่อง Reborrowing (การยืมซ้ำ) เมื่อใช้งานกับโครงสร้างข้อมูลที่มีการยืมแบบ Mutable (&mut) และการแปลง (Transform) ระหว่าง Context (บริบท) ที่แตกต่างกัน
ปัญหานี้มักจะเกิดขึ้นเมื่อเราพยายามสร้าง Struct ใหม่จากข้อมูลที่มีอยู่ใน Struct เดิม โดยยังคงรักษาความสัมพันธ์ของ Lifetime ไว้ ซึ่งอาจทำให้เกิดความสับสนและข้อผิดพลาดจาก Borrow Checker ได้หากไม่เข้าใจกลไกการทำงานของ Rust อย่างถ่องแท้
1. ปัญหาที่พบ
พิจารณาโค้ดตัวอย่างต่อไปนี้ ซึ่งจำลองสถานการณ์ที่มี LayoutCtx ถือ Mutable Reference ของ State และต้องการแปลงเป็น OtherCtx เพื่อใช้งานชั่วคราว
#![allow(unused)]
fn main() {
struct State { }
struct LayoutCtx<'s> {
window_state: &'s mut State
}
struct OtherCtx<'s> {
window_state: &'s mut State
}
impl<'s> LayoutCtx<'s> {
fn fee(&self) { }
// พยายามแปลงเป็น OtherCtx โดยใช้ lifetime 's เดิม
fn to_other_ctx(&mut self) -> OtherCtx<'s> {
OtherCtx {
window_state: self.window_state
}
}
}
impl<'s> OtherCtx<'s> {
fn faa(&self) { }
}
fn layout(lo_ctx: &mut LayoutCtx) {
let mut other_ctx = lo_ctx.to_other_ctx();
other_ctx.faa();
lo_ctx.fee();
}
}
เมื่อพยายามคอมไพล์โค้ดข้างต้น Rust จะแจ้ง Error ดังนี้:
error: lifetime may not live long enough
--> src/main.rs:15:9
|
13 | fn to_other_ctx(&mut self) -> OtherCtx<'s> {
| - let's call the lifetime of this reference `'1`
|
15 | OtherCtx {
| ^ assignment requires that `'1` must outlive `'s`
Error นี้บอกว่า Method นี้ควรจะ return ข้อมูลที่มี Lifetime 's แต่กำลัง return ข้อมูลที่มี Lifetime '1 (ซึ่งคือ Lifetime ของ &mut self)
2. สิ่งที่กำลังเกิดขึ้น
เพื่อเข้าใจปัญหานี้ เราต้องแยกแยะความแตกต่างระหว่างสองสิ่งนี้ให้ชัดเจน:
- Reference เดิม (
's): คือ&'s mut Stateที่ถูกเก็บไว้ในLayoutCtxซึ่งมีอายุยาวนานเท่ากับ's - Reborrow: คือการยืม Reference นั้นมาใช้ต่อในช่วงเวลาสั้นๆ ภายในฟังก์ชันหรือเมทอด
แผนภาพแสดงช่วงเวลาของ Lifetime
ลองจินตนาการเส้นเวลาของการทำงาน:
's (Lifetime ของ State): [==================================================]
'1 (Lifetime ของ &mut self): [=============] <-- ช่วงเวลาของ to_other_ctx
^
|
เราพยายามคืน 's ออกไปตรงนี้ (ซึ่งทำไม่ได้)
ข้อสังเกต: การทำแบบนี้ ไม่ใช่การย้าย (Move)
window_stateออกจากLayoutCtxแต่เป็นการ ยืมซ้ำ (Reborrow) มาใช้สร้างOtherCtxชั่วคราวเท่านั้น ข้อมูลต้นฉบับยังคงอยู่ที่เดิม แต่ถูก “ล็อก” ไว้ไม่ให้ใช้ซ้อนกันจนกว่าOtherCtxจะคืนสิทธิ์กลับมา
เมื่อเราเรียกเมธอด:
#![allow(unused)]
fn main() {
let mut other_ctx = lo_ctx.to_other_ctx();
}
lo_ctx ถูกส่งเข้ามาในฐานะ &mut LayoutCtx ซึ่งมี Lifetime ชั่วคราว (สมมติว่าเป็น '1) ที่สั้นกว่า 's มาก
ในเมทอด to_other_ctx:
- เราเข้าถึง
self.window_stateผ่าน&mut self - Rust ไม่สามารถให้เรา “ดึง”
&'s mut Stateออกไปตรงๆ ได้ เพราะมันติดอยู่กับ&mut selfที่มีอายุสั้นกว่า ('1) - การพยายาม return
OtherCtx<'s>คือการพยายามบอกว่า “ฉันจะคืน Struct ที่ถือ Reference ยาวนานเท่ากับ's” - แต่ในความเป็นจริง เรากำลังสร้าง Struct จากการ Reborrow ผ่าน
selfซึ่งมีอายุแค่'1เท่านั้น
จึงเกิดความขัดแย้งกันระหว่าง สิ่งที่สัญญาว่าจะคืน ('s) กับ สิ่งที่ทำได้จริง ('1)
3. การแก้ปัญหาโดยใช้ Anonymous Lifetime
วิธีแก้ไขที่ถูกต้องและเป็น Idiomatic Rust คือการยอมรับความจริงว่า OtherCtx ที่เราสร้างขึ้นมาใหม่นี้ เป็นเพียงลูกหนี้ชั่วคราว (Temporary Reborrow) เท่านั้น ไม่ใช่เจ้าของสิทธิ์การยืมยาวนานเท่ากับต้นฉบับ
เราสามารถใช้ Anonymous Lifetime ('_) เพื่อบอก Rust ว่าให้คำนวณ Lifetime ตามบริบทการใช้งานจริง:
#![allow(unused)]
fn main() {
impl<'s> LayoutCtx<'s> {
// แบบย่อ (Recommended)
fn to_other_ctx(&mut self) -> OtherCtx<'_> {
OtherCtx {
window_state: self.window_state
}
}
// แบบเต็ม (Explicit Lifetime) เพื่อให้เห็นภาพชัดเจน
// 'a คือ lifetime ของการยืม &mut self
// เราคืน OtherCtx<'a> ที่มีอายุเท่ากับ 'a
fn to_other_ctx_explicit<'a>(&'a mut self) -> OtherCtx<'a> {
OtherCtx {
window_state: self.window_state
}
}
}
}
ในทางปฏิบัติ การใช้ '_ (Anonymous Lifetime) เป็นที่นิยมมากกว่าเพราะเขียนสั้นกว่าและ Rust สามารถอนุมาน Lifetime 'a ให้เราได้เองโดยอัตโนมัติ แต่ความหมายเบื้องหลังนั้นเหมือนกันคือ “อายุของสิ่งที่คืนกลับไป จะผูกอยู่กับอายุของการยืม self”
ทำไมวิธีนี้ถึงได้ผล?
การใช้ OtherCtx<'_> (หรือเขียนเต็มๆ ว่า OtherCtx<'a> โดยที่ 'a ผูกกับ lifetime ของ &'a mut self) เป็นการบอก Rust ว่า:
“ค่า
OtherCtxที่คืนออกไป จะมีอายุยืนยาวเท่าที่จำเป็นสำหรับความถูกต้องของการยืมนี้เท่านั้น (ไม่เกินอายุของ&mut self)”
ด้วยวิธีนี้ Rust จะเข้าใจว่า:
to_other_ctxทำการ Reborrowwindow_stateจากselfOtherCtxที่ได้มา จะถือ Reference ที่ถูก Reborrow นี้- เมื่อ
OtherCtxหมดอายุ (เช่น จบ scope หรือเลิกใช้งาน) สิทธิ์การยืมจะถูกคืนกลับไปที่LayoutCtxทำให้เราสามารถเรียกlo_ctx.fee()ต่อได้
4. ทำไม OtherCtx<'s> ถึงใช้ไม่ได้?
หากสมมติว่า Rust ยอมให้เรา return OtherCtx<'s> ได้ จะเกิดอะไรขึ้น?
OtherCtxจะถือ&'s mut Stateซึ่งเป็น Reference ตัวเดียวกับที่LayoutCtxถืออยู่- ทั้ง
OtherCtxและLayoutCtxจะมีสิทธิ์เข้าถึง Mutable Reference ของStateพร้อมกัน ในช่วงเวลา'sเดียวกัน - สิ่งนี้จะละเมิดกฎ Aliasing XOR Mutation (มี Mutable Reference ได้เพียงตัวเดียวในขณะใดขณะหนึ่ง) อย่างรุนแรง
การใช้ Anonymous Lifetime ('_) ช่วยป้องกันปัญหานี้โดยการบังคับให้ Lifetime ของ OtherCtx สั้นลงเหลือแค่ช่วงที่เราใช้งานมันจริงๆ (Nested Scope) ทำให้ไม่เกิดการซ้อนทับกันของ Mutable Reference ที่อันตราย
5. ตัวอย่างการใช้งานจริง
สถานการณ์นี้มีประโยชน์มากเมื่อเราต้องการ “แตก” (Split) หรือ “แปลง” (Transform) Context ใหญ่ๆ ให้เป็น Context ย่อยเพื่อส่งให้ฟังก์ชันอื่นทำงานเฉพาะทาง โดยที่ Context หลักยังคงกลับมาใช้งานต่อได้หลังจาก Context ย่อยทำงานเสร็จ
#![allow(unused)]
fn main() {
// จำลองการใช้งานในระบบ UI
fn build_ui(mut layout_ctx: LayoutCtx) {
// 1. แปลงเป็น Context ย่อยเพื่อวาดปุ่ม
{
let mut other_ctx = layout_ctx.to_other_ctx();
other_ctx.faa(); // ใช้ other_ctx ทำงานบางอย่าง...
} // จบ scope: other_ctx คืนสิทธิ์การยืมกลับไปที่ layout_ctx
// 2. LayoutCtx กลับมาใช้งานต่อได้ (ไม่ถูก move หายไป)
layout_ctx.fee();
}
}
หากเราใช้ OtherCtx<'s> (แบบที่ผิด) การเรียก layout_ctx.to_other_ctx() จะทำให้ layout_ctx ถูกล็อกยาวนานเท่ากับ 's (ตลอดอายุของ State) ส่งผลให้บรรทัดที่ 2 (layout_ctx.fee()) คอมไพล์ไม่ผ่านเพราะติดกฎการยืมซ้อน
สรุป
- Reborrowing เป็นกลไกอัตโนมัติ: Rust ทำการ Reborrow Reference ให้เราโดยอัตโนมัติเมื่อมีการส่งต่อ Mutable Reference ผ่านฟังก์ชัน เพื่อลด Lifetime ลงให้เหมาะสมกับบริบทนั้นๆ
- Struct Transformation ต้องระวัง Lifetime: เมื่อแปลง Struct หนึ่งไปเป็นอีก Struct หนึ่งที่ถือ Reference ตัวเดิม หากผ่าน
&mut selfมักจะต้องใช้ Anonymous Lifetime ('_) ใน Return Type เสมอ '_คือเพื่อนที่ดี: การใช้OtherCtx<'_>สื่อความหมายว่า “Struct นี้ยืมข้อมูลมาใช้ชั่วคราว” ซึ่งตรงกับพฤติกรรมที่ Borrow Checker ต้องการ และช่วยให้โค้ดมีความยืดหยุ่น ปลอดภัย
การเข้าใจเรื่องนี้จะช่วยให้คุณออกแบบ API ที่ซับซ้อนขึ้นได้ โดยเฉพาะในรูปแบบ Context Passing หรือ Builder Pattern ที่มีการส่งต่อ State ระหว่างกัน