Rust Problem-Solving Handbook (TH)
Rust Problem-Solving Handbook (TH) คือคู่มือแก้ปัญหา Rust Programming ภาษาไทย ที่รวบรวมปัญหาซึ่งนักพัฒนา Rust มักเจอในการทำงานจริง พร้อมวิธีแก้ไขที่อธิบายอย่างละเอียดผ่านกรณีศึกษา
เหมาะสำหรับนักพัฒนาที่เริ่มเขียน Rust แล้วเจอ Error ที่เข้าใจยาก หรือผู้ที่ต้องการเข้าใจแนวคิดเบื้องลึกของ Rust เช่น Ownership, Lifetime และ Borrow Checker ให้ถ่องแท้มากขึ้น
แต่ละบทเรียนเขียนขึ้นจากปัญหาจริง อธิบายที่มาของ Error และนำเสนอวิธีแก้ไขพร้อมโค้ดตัวอย่างที่รันได้ทันที
โครงสร้างเนื้อหา
- Part I: พื้นฐานการจัดการ Ownership, Borrowing และ Lifetime
- Part II: ระบบประเภทข้อมูล (Type System)
- Part III: การจัดการข้อผิดพลาด (Error Handling)
- Part IV: Concurrency และ Parallelism
- Part V: Unsafe Rust และ FFI
- Part VI: Patterns และ Idioms
วิธีใช้งานหนังสือเล่มนี้
- สำหรับมือใหม่: แนะนำให้อ่านตามลำดับรายตอน โดยเฉพาะ Part I: Ownership & Borrowing
- สำหรับการแก้ปัญหาเฉพาะจุด: สามารถค้นหา Error Message หรือชื่อหัวข้อที่ตรงกับปัญหาในสารบัญ แล้วเลือกข้ามไปอ่านในส่วน Troubleshooting ได้เลย
ทุกคนสามารถร่วมกันพัฒนาเนื้อหาหนังสือเล่มนี้ได้ทางบอร์ด GitHub Issues & Pull Requests ของโปรเจกต์นี้
พื้นฐานการจัดการ Ownership, Borrowing และ Lifetime
ระบบ Ownership คือหัวใจสำคัญที่ทำให้ Rust แตกต่างจากภาษาอื่น มันคือกลไกที่ช่วยจัดการหน่วยความจำอย่างปลอดภัย โดยไม่ต้องพึ่ง Garbage Collector และตรวจจับบั๊กที่เกี่ยวกับ Memory ได้ตั้งแต่ตอนคอมไพล์
แต่ระบบนี้ก็มาพร้อมกับ กับดัก และ ความสับสน มากมายที่แม้แต่นักพัฒนาที่มีประสบการณ์ก็ยังเจอ Part นี้จะพาคุณไปเจาะลึกปัญหาเหล่านั้นผ่านกรณีศึกษาจริง
เนื้อหาในส่วนนี้
- OB 001: การเป็นเจ้าของของ self ในฟังก์ชันเมมเบอร์ (Self Ownership)
- OB 002: Reborrowing ในเมธอดและการแปลงโครงสร้างข้อมูล
- OB 003: กับดักของ read_line ในลูป (The Infinite Append & Borrowing)
OB 001: การเป็นเจ้าของของ self ในฟังก์ชันเมมเบอร์ (Self Ownership)
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 ระหว่างกัน
OB 003: กับดักของ read_line ในลูป (The Infinite Append & Borrowing)
OB 003: กับดักของ read_line ในลูป (The Infinite Append & Borrowing)
การรับ Input จากผู้ใช้ผ่าน Console เป็นเรื่องพื้นฐาน แต่ใน Rust การใช้ read_line ภายในลูป (loop, while) มีหลุมพรางซ่อนอยู่ 2 ชั้น ได้แก่ “บั๊กเงียบที่โปรแกรมไม่พังแต่ทำงานผิด” และ “Error จาก Borrow Checker”
บทความนี้จะพาไปดูสาเหตุ เบื้องหลังการออกแบบ และวิธีจัดการให้ถูกต้องครับ
ทำไม read_line ถึงออกแบบให้รับ Buffer เข้ามา?
ก่อนจะเข้าเรื่องกับดัก เรามาทำความเข้าใจก่อนว่า ทำไม read_line ถึงไม่ได้ออกแบบให้ return ค่า กลับมาตรงๆ แบบนี้
#![allow(unused)]
fn main() {
// ❌ แบบนี้ไม่มีจริง แค่สมมติ
let input = io::stdin().read_line().unwrap();
}
แต่กลับออกแบบให้ รับ Buffer เข้าไปแก้ไขแทน:
#![allow(unused)]
fn main() {
// ✅ แบบที่ Rust ใช้จริง
let mut buf = String::new();
io::stdin().read_line(&mut buf).unwrap();
}
เหตุผลหลักคือ Performance — เมื่อเราเป็นผู้ถือ Buffer เอง เราสามารถ นำ Buffer กลับมาใช้ซ้ำ ได้ในรอบถัดไป โดยไม่ต้อง Allocate หน่วยความจำใหม่ทุกครั้ง ซึ่งในงานที่ต้องอ่านข้อมูลจำนวนมาก (เช่น อ่านไฟล์หลายล้านบรรทัด) ความต่างนี้มีผลอย่างมาก
นี่คือ Pattern ดั้งเดิมที่ใช้กันมาตั้งแต่ C โดยมีข้อดี 3 ประการ:
- ลด Allocation — ใช้ Buffer เดิมซ้ำได้ ไม่ต้องจองหน่วยความจำใหม่ทุกรอบ
- แยกความรับผิดชอบ — ตัว I/O ทำหน้าที่แค่อ่าน ส่วนการจัดการ Memory เป็นเรื่องของผู้เรียก
- ยืดหยุ่นเรื่อง Lifetime — Buffer และ Stream มี Lifetime เป็นอิสระต่อกัน
แต่การออกแบบนี้ก็แลกมาด้วย “กับดัก” ที่เราจะพูดถึงต่อไป
1. กับดักที่ 1: “บั๊กเงียบ” (The Phantom Input)
ลองดูโค้ดตัวอย่างที่มือใหม่ (และมือเก๋าที่เผลอ) มักเขียน:
use std::io;
fn main() {
let mut input = String::new(); // สร้าง buffer นอกลูป
loop {
println!("Type something:");
// อ่านค่าใส่ buffer ตัวเดิม
io::stdin().read_line(&mut input).expect("Failed to read");
println!("You typed: {:?}", input.trim()); // ใช้ {:?} เพื่อดูค่าจริงๆ
if input.trim() == "exit" { break; }
}
}
ผลลัพธ์ที่ได้ (ความหายนะ):
| รอบ | พิมพ์ | ค่าจริงใน buffer | .trim() ได้ | ตรวจ == "exit"? |
|---|---|---|---|---|
| 1 | A | "A\n" | "A" | ❌ |
| 2 | B | "A\nB\n" | "A\nB" | ❌ |
| 3 | exit | "A\nB\nexit\n" | "A\nB\nexit" | ❌ ตลอดกาล! |
สาเหตุ:
ฟังก์ชัน read_line ถูกออกแบบมาให้ Append (ต่อท้าย) ข้อมูลลงใน String buffer เสมอ ไม่ใช่ Overwrite (เขียนทับ) — เอกสารของ Rust ระบุชัดเจนว่า:
“Previous content of the buffer will be preserved. To avoid appending to the buffer, you need to
clearit first.”
นอกจากนี้ read_line ยัง รวมตัวอักษร \n (newline) เข้ามาใน buffer ด้วย ทำให้เกิดปัญหาซ้อนกัน 2 ชั้น:
- ค่าเก่าพอกพูน — ข้อมูลจากรอบก่อนๆ ไม่หายไปไหน
- เงื่อนไขออกจากลูปพังไปด้วย —
input.trim()จะไม่มีทางเท่ากับ"exit"อีกเลยหลังรอบแรก เพราะมีข้อมูลเก่าค้างอยู่ข้างหน้า ทำให้เกิด Infinite Loop โดยไม่รู้ตัว
💡 สังเกต:
.trim()ตัดแค่ Whitespace ที่ หัว และ ท้าย เท่านั้น ไม่ได้ตัด\nที่อยู่ ตรงกลาง ของ String
วิธีแก้ (Solution A): ล้าง Buffer ทุกรอบ
ต้องสั่ง .clear() ก่อนอ่านค่าใหม่ในแต่ละรอบ:
#![allow(unused)]
fn main() {
loop {
input.clear(); // ✅ ล้างค่าเก่าทิ้งก่อนอ่านใหม่
println!("Type something:");
io::stdin().read_line(&mut input).unwrap();
println!("You typed: {}", input.trim());
if input.trim() == "exit" { break; } // ✅ ทำงานถูกต้องแล้ว
}
}
2. กับดักที่ 2: Borrow Checker Error (E0502)
ปัญหานี้จะเกิดขึ้นเมื่อเราพยายาม “ยืมค่า” จาก input มาถือไว้ แล้ววนลูปกลับไปเรียก read_line ใหม่ โดยที่ Reference เก่ายังไม่ถูกคืน
ตัวอย่างโค้ดที่ Compile ไม่ผ่าน:
fn main() {
let mut input = String::new();
let mut history = Vec::new(); // เราอยากเก็บประวัติทุกอย่างที่ผู้ใช้พิมพ์
loop {
input.clear();
std::io::stdin().read_line(&mut input).unwrap();
let trimmed = input.trim(); // ได้ &str ที่ชี้ไปยังข้อมูลใน input
history.push(trimmed); // ❌ Error! เก็บ reference ของ input ใส่ vector
// จบรอบ -> วนกลับไป read_line(&mut input)
// 💥 BOOM: cannot borrow `input` as mutable because it is also borrowed as immutable
}
}
ทำไมถึง Compile ไม่ผ่าน?
input.trim()คืนค่าเป็น&strซึ่งเป็น Immutable Reference ที่ชี้ไปยังข้อมูลภายในinputhistory.push(trimmed)ทำให้historyถือ Reference นั้นค้างไว้ — มันยังไม่ถูก Drop- เมื่อวนลูปกลับไป
read_line(&mut input)ต้องการยืมinputแบบ Mutable เพื่อแก้ไขค่า - กฎเหล็กของ Rust: ณ เวลาใดเวลาหนึ่ง ถ้ามีใครถือ Immutable Reference (
&) อยู่ จะไม่อนุญาตให้เกิด Mutable Borrow (&mut) ซ้อนขึ้นมาอีก - เนื่องจาก
historyยังคงถือ&strที่ชี้มาที่inputอยู่ Rust จึงปฏิเสธไม่ให้read_lineแก้ไขinput
📝 หมายเหตุ: Error นี้คือ E0502 (cannot borrow as mutable because it is also borrowed as immutable) ไม่ใช่ E0499 ซึ่งเป็น error ของการยืม Mutable ซ้ำ 2 ครั้ง
วิธีแก้ (Solution B): สร้างค่าใหม่ที่เป็น Owned Type
ถ้าต้องการเก็บค่าข้ามรอบลูป เราเก็บ Reference ไม่ได้ ต้อง สร้าง String ใหม่ ที่เป็นเจ้าของข้อมูลเอง:
#![allow(unused)]
fn main() {
// เปลี่ยน history ให้เก็บ String (Owned) แทน &str (Borrowed)
let mut history: Vec<String> = Vec::new();
loop {
input.clear();
std::io::stdin().read_line(&mut input).unwrap();
history.push(input.trim().to_string()); // ✅ .to_string() สร้าง String ใหม่ที่เป็นอิสระจาก input
// ...
}
}
.to_string() (หรือ String::from(...)) จะ Copy ข้อมูลออกมาเป็น String ใหม่ ทำให้ history ไม่ได้ถือ Reference ที่ชี้ไปยัง input อีกต่อไป — Borrow Checker จึงพอใจ
3. แนวทางการเขียนลูปรับ Input (Best Practices)
มี 3 แนวทางหลัก ขึ้นอยู่กับว่าคุณให้ความสำคัญกับ Performance หรือ Convenience มากกว่ากัน
ท่าที่ 1: Buffer Reuse (เน้น Performance)
เหมาะกับงานที่ต้องอ่านข้อมูลจำนวนมหาศาล (เช่น อ่านไฟล์หลายล้านบรรทัด) เพราะจอง Memory เพียงครั้งเดียวแล้วใช้ซ้ำ:
#![allow(unused)]
fn main() {
let mut input = String::new(); // Allocate ครั้งเดียว
loop {
input.clear(); // Reset length เป็น 0 แต่ไม่คืน capacity (เร็วมาก)
std::io::stdin().read_line(&mut input)?;
// ประมวลผล input...
}
}
🔍 ทำไมเร็วกว่า? —
String::clear()แค่ตั้ง length กลับเป็น 0 โดยไม่ Deallocate หน่วยความจำที่จองไว้ ดังนั้นread_lineในรอบถัดไปไม่ต้อง Allocate ใหม่ ยกเว้นบรรทัดนั้นยาวกว่า capacity เดิม
ท่าที่ 2: Fresh Scope (เน้นความปลอดภัยและความชัดเจน)
เหมาะกับ CLI ทั่วไป เขียนง่าย ปลอดภัย ไม่ต้องกลัวลืม clear() และไม่มีปัญหา Borrow Checker ข้ามรอบ:
#![allow(unused)]
fn main() {
loop {
let mut input = String::new(); // สร้างใหม่ทุกรอบ (Scope อยู่แค่ในลูป)
std::io::stdin().read_line(&mut input)?;
// ประมวลผล input...
// input ถูก Drop เมื่อจบรอบ — ไม่มีทาง state ค้าง
}
}
Trade-off: ท่านี้ Allocate ใหม่ทุกรอบ ช้ากว่าท่าที่ 1 เล็กน้อย แต่สำหรับ Interactive CLI ที่ผู้ใช้พิมพ์ทีละบรรทัด ความต่างนี้ไม่มีผลในทางปฏิบัติเลย
ท่าที่ 3: Iterator — The Rustacean Way
ใช้ lines() Iterator ซึ่งจัดการเรื่อง Buffer, Newline และ Error ให้เราอย่างสวยงาม:
use std::io::{self, BufRead};
fn main() {
let stdin = io::stdin();
// lock() ล็อก stdin ไว้ครั้งเดียว ไม่ต้อง lock/unlock ทุกรอบ
for line in stdin.lock().lines() {
let content = line.unwrap(); // ได้ String ใหม่ (ไม่มี \n ติดมา)
println!("You typed: {}", content);
if content == "exit" { break; }
}
}
สิ่งที่
lines()ทำให้อัตโนมัติ:
- ตัด
\nออกให้เรียบร้อย- คืน
Stringใหม่ (Owned) ทุกบรรทัด — ไม่มีปัญหา Borrow ข้ามรอบ- Wrap ใน
Resultเพื่อจัดการ I/O Error ได้ข้อควรรู้:
lines()สร้างStringใหม่ทุกบรรทัดเช่นเดียวกับท่าที่ 2 จึงช้ากว่าท่าที่ 1 (Buffer Reuse) ในงานที่ต้องอ่านจำนวนมาก แต่สำหรับงานทั่วไปถือว่าเหมาะสมที่สุด
Checklist สรุป
read_line= Append เสมอ — ถ้าใช้ Buffer เดิมซ้ำ ต้องสั่ง.clear()ก่อนอ่านทุกครั้ง ไม่งั้นข้อมูลเก่าจะค้างและเงื่อนไขต่างๆ จะพังread_lineรวม\nมาด้วย — ใช้.trim()เพื่อตัดออก แต่ระวังว่า.trim()ตัดแค่หัวท้าย ไม่ตัดตรงกลาง- ระวัง Reference ค้างข้ามรอบ — ถ้าประกาศ Buffer นอกลูป ห้ามเก็บ
&strที่ชี้ไปยัง Buffer นั้นข้ามรอบลูป ให้ใช้.to_string()สร้างค่าใหม่แทน - เลือกท่าให้เหมาะกับงาน:
- เน้นเร็ว → Buffer Reuse +
.clear() - เน้นชัวร์/เขียนง่าย → ประกาศ
String::new()ในลูป - เน้นสไตล์ Idiomatic Rust → ใช้
stdin().lock().lines()
- เน้นเร็ว → Buffer Reuse +
แหล่งอ้างอิง
- Rust Documentation: std::io::Stdin::read_line
- Rust by Example: Read Lines
- Rust Forum: Error when using read_line in a loop
- Rust Forum: Why using the read_lines() iterator is much slower than using read_line()?
- SE: Intuition behind why Rust’s io::stdin().read_line() relies on a side effect?
ระบบประเภทข้อมูล (Type System)
ระบบ Type System ของ Rust เป็นหนึ่งในจุดเด่นที่ทำให้ภาษานี้ทั้งปลอดภัยและยืดหยุ่น Rust ใช้ Static Typing ร่วมกับ Trait System ที่ทรงพลัง ทำให้สามารถเขียนโค้ดที่ Generic และ Reusable ได้โดยไม่เสียประสิทธิภาพ
ความท้าทายที่นักพัฒนามักเจอในหมวดนี้คือการเลือกระหว่าง Generic Type Parameters กับ Associated Types, การตัดสินใจใช้ Static Dispatch หรือ Dynamic Dispatch, และการออกแบบ API ที่ใช้ Trait Bounds อย่างเหมาะสมโดยไม่ทำให้ Type Signature ซับซ้อนเกินไป
Part นี้จะพาคุณเจาะลึกแนวคิดเหล่านี้ผ่านปัญหาจริงที่เกิดขึ้นในการพัฒนาซอฟต์แวร์ด้วย Rust ตั้งแต่การใช้ Generics อย่างมีประสิทธิภาพ ไปจนถึงการทำ Type Erasure สำหรับ Dynamic Dispatch
เนื้อหาในส่วนนี้
การจัดการข้อผิดพลาด (Error Handling)
การจัดการข้อผิดพลาดเป็นหัวใจสำคัญของการเขียนโปรแกรมที่เชื่อถือได้ Rust ใช้แนวทาง Explicit Error Handling ผ่าน Result<T, E> และ Option<T> แทนการใช้ Exception เหมือนภาษาอื่น ซึ่งบังคับให้โปรแกรมเมอร์จัดการกับทุกกรณีที่อาจเกิดข้อผิดพลาดตั้งแต่ตอนเขียนโค้ด
ความท้าทายที่นักพัฒนามักเจอคือการตัดสินใจว่าจะใช้ Result หรือ Option เมื่อไหร่ การออกแบบ Custom Error Types ที่รองรับการใช้งานจริงในหลายระดับ และการใช้ ? Operator เพื่อส่งต่อข้อผิดพลาดอย่างกระชับโดยไม่สูญเสีย Context ของปัญหา
Part นี้จะครอบคลุมแนวคิดตั้งแต่พื้นฐานของ Result/Option ไปจนถึงเทคนิคขั้นสูงอย่าง Error Propagation Patterns และการสร้าง Error Types ที่ยืดหยุ่น
เนื้อหาในส่วนนี้นนี้
Concurrency และ Parallelism
Concurrency เป็นหนึ่งในจุดแข็งที่โดดเด่นของ Rust ด้วยแนวคิด “Fearless Concurrency” ที่ระบบ Ownership และ Type System ช่วยป้องกัน Data Race ได้ตั้งแต่ตอนคอมไพล์ ทำให้สามารถเขียนโปรแกรม Multi-threaded ได้อย่างปลอดภัยโดยไม่ต้องพึ่งพา Runtime Overhead
ความท้าทายที่นักพัฒนามักเจอในหมวดนี้คือการทำความเข้าใจว่า Ownership Rules ส่งผลต่อ Multi-threading อย่างไร การเลือกระหว่าง Channel (Message Passing) กับ Shared State (Mutex, RwLock) และการใช้ Arc เพื่อแชร์ข้อมูลข้าม Thread อย่างถูกต้อง
Part นี้จะครอบคลุมแนวคิดหลักของ Concurrent Programming ใน Rust ตั้งแต่พื้นฐานของ Thread Spawning ไปจนถึง Synchronization Primitives ที่ใช้กันในการพัฒนาจริง
เนื้อหาในส่วนนี้
Unsafe Rust และ FFI
Rust ถูกออกแบบมาให้ปลอดภัยเป็นค่าเริ่มต้น แต่มีบางสถานการณ์ที่ต้องก้าวข้าม Safety Guarantee เหล่านั้น เช่น การเข้าถึง Hardware โดยตรง การ Optimize Performance ในระดับต่ำ หรือการเรียกใช้โค้ดจากภาษาอื่น Rust จึงมี unsafe Keyword เพื่อเปิดช่องทางให้ทำสิ่งเหล่านี้ได้อย่างมีขอบเขตชัดเจน
ความท้าทายที่นักพัฒนามักเจอคือการตัดสินใจว่าเมื่อไหร่ที่ unsafe จำเป็นจริงๆ การเขียน Safety Invariants ที่ครบถ้วน และการสร้าง Safe Abstraction ที่ครอบ Unsafe Code เพื่อป้องกันไม่ให้ผู้ใช้ API ต้องรับภาระด้านความปลอดภัยเอง นอกจากนี้ การทำ FFI (Foreign Function Interface) เพื่อเรียกใช้ C Library ก็เป็นอีกหัวข้อที่ต้องเข้าใจ Unsafe อย่างถ่องแท้
Part นี้จะครอบคลุมตั้งแต่ Raw Pointers และ Unsafe Block ไปจนถึงการเขียน FFI Binding ที่ปลอดภัยและใช้งานได้จริง
เนื้อหาในส่วนนี้
Patterns และ Idioms
การเขียนโค้ด Rust ให้ดีไม่ใช่แค่เรื่องของ Syntax แต่ต้องเข้าใจ Design Patterns และ Idioms ที่ชุมชน Rust ยอมรับและใช้งานกันอย่างแพร่หลาย Patterns เหล่านี้ช่วยให้โค้ดอ่านง่าย บำรุงรักษาได้ และใช้ประโยชน์จากระบบ Type System ของ Rust ได้เต็มที่
ความท้าทายที่นักพัฒนามักเจอคือการเลือกใช้ Pattern ที่เหมาะสมกับสถานการณ์ เช่น เมื่อไหร่ควรใช้ Builder Pattern แทน Constructor ธรรมดา เมื่อไหร่ที่ Interior Mutability เป็นทางออกที่ดีกว่า &mut และการออกแบบ API ที่ใช้ RAII เพื่อจัดการทรัพยากรโดยอัตโนมัติ
Part นี้จะครอบคลุม Patterns ที่สำคัญที่สุดใน Rust ตั้งแต่ Builder Pattern สำหรับการสร้าง API ที่ใช้งานง่าย ไปจนถึง Interior Mutability ที่เป็นแนวคิดเฉพาะตัวของ Rust
เนื้อหาในส่วนนี้
Glossary — ศัพท์เทคนิค
รวมคำศัพท์เทคนิคที่พบบ่อยในหนังสือเล่มนี้และในโลกของ Rust พร้อมคำอธิบายภาษาไทยที่เข้าใจง่าย เรียงตามลำดับตัวอักษร A–Z
A
Aliasing
การที่มี Reference มากกว่าหนึ่งตัวชี้ไปยังข้อมูลเดียวกันในเวลาเดียวกัน Rust บังคับกฎ Aliasing XOR Mutation คือ ณ เวลาใดเวลาหนึ่ง จะมีได้แค่ Immutable Reference หลายตัว หรือ Mutable Reference เพียงตัวเดียว แต่จะมีทั้งสองแบบพร้อมกันไม่ได้
Anonymous Lifetime ('_)
Lifetime แบบไม่ระบุชื่อ ใช้เพื่อบอก Rust ว่า “ให้คำนวณ Lifetime ที่เหมาะสมจากบริบทเอง” มักใช้ใน Return Type ของฟังก์ชันที่คืน Reference ซึ่งผูกกับ &self หรือ &mut self
Associated Type
Type ที่ถูกกำหนดไว้ภายใน Trait Definition ทำให้ผู้ Implement สามารถเลือก Concrete Type ได้ เช่น type Item ใน Iterator Trait
B
Borrow / Borrowing (การยืม)
การเข้าถึงข้อมูลโดยไม่ย้าย Ownership มี 2 แบบ:
- Shared Borrow (
&T) — ยืมอ่านอย่างเดียว มีได้หลายตัวพร้อมกัน - Mutable Borrow (
&mut T) — ยืมไปแก้ไข มีได้เพียงตัวเดียว
Borrow Checker
ส่วนของ Rust Compiler ที่ตรวจสอบว่ากฎ Borrowing ถูกปฏิบัติตามอย่างถูกต้อง หากมีการละเมิดกฎ (เช่น มี Mutable Borrow ซ้อนกับ Immutable Borrow) จะแจ้ง Compile Error
Builder Pattern
รูปแบบการออกแบบที่ใช้ Method Chaining เพื่อสร้าง Object ทีละขั้นตอน เหมาะสำหรับ Struct ที่มี Field จำนวนมากหรือมี Optional Field
C
Clone
Trait ที่ให้ความสามารถในการ “ทำสำเนาเชิงลึก” (Deep Copy) ของข้อมูล ต่างจาก Copy ตรงที่อาจมีต้นทุนสูง (เช่น ต้อง Allocate หน่วยความจำใหม่)
Copy
Trait ที่บ่งบอกว่า Type นั้นสามารถ ทำสำเนาบิต (Bitwise Copy) ได้โดยอัตโนมัติ เมื่อ Type มี Copy การส่งค่าเข้าฟังก์ชันจะเป็นการ Copy แทนการ Move ตัวอย่าง: i32, f64, bool, char
Concurrency (การทำงานพร้อมกัน)
ความสามารถในการจัดการงานหลายอย่างในเวลาที่ทับซ้อนกัน ใน Rust การทำ Concurrency มีความปลอดภัยสูงเพราะ Borrow Checker ช่วยป้องกัน Data Race ตั้งแต่ตอน Compile
D
Data Race
สถานการณ์ที่ Thread สองตัว (หรือมากกว่า) เข้าถึงข้อมูลเดียวกันพร้อมกัน โดยอย่างน้อยหนึ่งตัวกำลังเขียนข้อมูล และไม่มีการ Synchronization ระบบ Ownership ของ Rust ป้องกันปัญหานี้ได้ในระดับ Compile Time
Dereference (การเข้าถึงค่าจาก Reference)
การใช้ * เพื่อเข้าถึงค่าที่ Reference ชี้อยู่ เช่น *ptr จะได้ค่าที่ ptr ชี้ไป
Drop
Trait ที่กำหนดพฤติกรรมเมื่อค่าถูกทำลาย (ออกจาก Scope) ใช้สำหรับจัดการทรัพยากรเช่นการปิดไฟล์หรือคืนหน่วยความจำ เป็นพื้นฐานของรูปแบบ RAII
Dynamic Dispatch
กลไกที่ Rust เลือกฟังก์ชันที่จะเรียกตอน Runtime (ผ่าน vtable) โดยใช้ Trait Object (dyn Trait) ต่างจาก Static Dispatch ที่เลือกตอน Compile Time
E
Enum (Enumeration)
ชนิดข้อมูลที่สามารถเป็นได้หลาย “รูปแบบ” (Variant) แต่ละ Variant สามารถมีข้อมูลแนบได้ เช่น Option<T> (Some(T) หรือ None) และ Result<T, E> (Ok(T) หรือ Err(E))
F
FFI (Foreign Function Interface)
กลไกสำหรับเรียกใช้ฟังก์ชันจากภาษาอื่น (โดยเฉพาะ C) หรือให้ภาษาอื่นเรียกใช้ฟังก์ชัน Rust ต้องใช้ extern block และมักต้องเขียนภายใน unsafe
G
Generic Type
Type ที่ยังไม่ระบุ Concrete Type เช่น Vec<T> โดยที่ T จะถูกแทนที่ด้วย Type จริงตอนใช้งาน ช่วยให้เขียนโค้ดที่ใช้ซ้ำได้กับหลาย Type
I
Immutable (ไม่เปลี่ยนแปลง)
ค่าเริ่มต้นของตัวแปรใน Rust — ไม่สามารถแก้ไขค่าได้หลังจากกำหนดค่าแล้ว หากต้องการแก้ไขต้องประกาศด้วย mut
Interior Mutability
รูปแบบที่ยอมให้แก้ไขข้อมูลภายในได้แม้จะถือ Immutable Reference อยู่ ใช้ Types เช่น Cell<T>, RefCell<T> หรือ Mutex<T> ซึ่งเลื่อนการตรวจสอบ Borrowing Rules ไปทำตอน Runtime แทน
Iterator
Trait ที่ให้ความสามารถในการวนซ้ำ (Iterate) ผ่านลำดับของค่า ต้อง Implement fn next(&mut self) -> Option<Self::Item> เป็นหัวใจของ Functional Programming Style ใน Rust
L
Lifetime (อายุการใช้งาน)
ช่วงเวลาที่ Reference ยังคงใช้งานได้ (Valid) Rust ใช้ Lifetime Annotation ('a, 'b, ฯลฯ) เพื่อบอก Compiler ว่า Reference ตัวไหนต้องมีอายุยาวนานเพียงใดเพื่อป้องกัน Dangling Reference
M
Match
Expression ที่ใช้สำหรับ Pattern Matching กับ Enum หรือค่าต่างๆ คล้ายกับ switch ในภาษาอื่น แต่ทรงพลังกว่ามากเพราะ Rust บังคับให้จัดการทุก Case (Exhaustive Matching)
Move (การย้ายความเป็นเจ้าของ)
การโอน Ownership จากตัวแปรหนึ่งไปยังอีกตัวแปรหนึ่ง หลังจาก Move แล้ว ตัวแปรเดิมจะใช้งานไม่ได้อีก เกิดขึ้นกับ Type ที่ไม่ได้ Implement Copy
Mutable (mut)
คำสำคัญที่ใช้ประกาศว่าตัวแปรสามารถเปลี่ยนแปลงค่าได้ เช่น let mut x = 5;
Mutex (Mutual Exclusion)
โครงสร้างข้อมูลสำหรับ Synchronization ที่อนุญาตให้ Thread เดียวเท่านั้นเข้าถึงข้อมูลภายในได้ในเวลาใดเวลาหนึ่ง ใช้คู่กับ Arc สำหรับ Shared Ownership ข้าม Thread
O
Option<T>
Enum ที่แทน “มีค่า” (Some(T)) หรือ “ไม่มีค่า” (None) ใช้แทน null ในภาษาอื่น ช่วยป้องกัน Null Pointer Exception ได้ตั้งแต่ Compile Time
Ownership (ความเป็นเจ้าของ)
กฎหลักของ Rust ที่กำหนดว่า ข้อมูลทุกชิ้นในหน่วยความจำมี “เจ้าของ” (Owner) ได้เพียงตัวเดียว เมื่อ Owner ออกจาก Scope ข้อมูลจะถูกทำลาย (Drop) โดยอัตโนมัติ
P
Pattern Matching
กลไกการเปรียบเทียบค่ากับรูปแบบ (Pattern) ต่างๆ ใช้ผ่าน match, if let, while let เป็นเครื่องมือสำคัญสำหรับจัดการ Option, Result และ Enum
Pointer (ตัวชี้)
ค่าที่เก็บ Memory Address ของข้อมูล ใน Rust มี 2 ประเภทหลัก:
- Reference (
&T,&mut T) — ปลอดภัย ถูกตรวจสอบโดย Borrow Checker - Raw Pointer (
*const T,*mut T) — ไม่ถูกตรวจสอบ ต้องใช้ภายในunsafe
R
RAII (Resource Acquisition Is Initialization)
รูปแบบการจัดการทรัพยากรที่ผูกอายุของทรัพยากร (เช่น ไฟล์, หน่วยความจำ, Lock) กับอายุของ Object เมื่อ Object ถูก Drop ทรัพยากรจะถูกคืนอัตโนมัติ Rust ใช้รูปแบบนี้เป็นพื้นฐาน
Reborrowing (การยืมซ้ำ)
กลไกที่ Rust สร้าง Reference ใหม่จาก Reference ที่มีอยู่แล้ว โดย Reference ใหม่จะมี Lifetime สั้นกว่า ช่วยให้ส่งต่อ &mut T ให้ฟังก์ชันอื่นได้โดยไม่ต้อง Move Mutable Reference ต้นฉบับ
Reference (การอ้างอิง)
ค่าที่ “ชี้” ไปยังข้อมูลของตัวแปรอื่น โดยไม่ย้าย Ownership ใช้ & สำหรับ Shared Reference และ &mut สำหรับ Mutable Reference
Result<T, E>
Enum ที่แทน “สำเร็จ” (Ok(T)) หรือ “ผิดพลาด” (Err(E)) เป็น Type หลักสำหรับ Error Handling ใน Rust ใช้ร่วมกับ ? Operator เพื่อส่งต่อ Error ได้อย่างกระชับ
S
Scope (ขอบเขต)
ช่วงของโค้ดที่ตัวแปรยังคง Valid อยู่ โดยทั่วไปคือตั้งแต่จุดที่ประกาศจนถึงปีกกาปิด } เมื่อออกจาก Scope ค่าจะถูก Drop
Self / self
Self(ตัวใหญ่) — อ้างถึง Type ของimplblock ปัจจุบันself(ตัวเล็ก) — อ้างถึง Instance ของ Type ปัจจุบัน ใช้เป็น Parameter แรกของ Method
Slice
“มุมมอง” (View) ที่ชี้ไปยังส่วนหนึ่งของข้อมูลที่ต่อเนื่องกันในหน่วยความจำ เช่น &[T] คือ Slice ของ Array/Vec และ &str คือ String Slice
Static Dispatch
กลไกที่ Rust เลือกฟังก์ชันที่จะเรียกตอน Compile Time (ผ่าน Monomorphization) ทำให้ไม่มี Runtime Cost เพิ่ม ใช้กับ Generics + Trait Bounds
String vs &str
String— เป็น Owned Type เก็บข้อมูลบน Heap สามารถแก้ไขได้&str— เป็น Borrowed Type (String Slice) ชี้ไปยังข้อมูลที่อยู่ที่อื่น ไม่สามารถแก้ไขได้โดยตรง
T
Trait
ชุดของ Method Signatures ที่กำหนด “ความสามารถ” ของ Type คล้ายกับ Interface ในภาษาอื่น แต่ทรงพลังกว่า เช่น Display, Debug, Clone, Iterator
Trait Bound
เงื่อนไขที่กำหนดว่า Generic Type ต้อง Implement Trait บางตัว เช่น fn foo<T: Display>(x: T) หมายความว่า T ต้อง Implement Display
Trait Object (dyn Trait)
ค่าที่ Type จริงจะถูกกำหนดตอน Runtime ใช้สำหรับ Dynamic Dispatch เช่น Box<dyn Error> สามารถเก็บ Error Type ใดก็ได้ที่ Implement Error Trait
U
Unsafe
คำสำคัญที่ใช้ระบุว่าโค้ดส่วนนั้นอยู่นอกเหนือการรับประกันความปลอดภัยของ Rust Compiler ทำให้สามารถ:
- Dereference Raw Pointer
- เรียกใช้ Unsafe Function
- เข้าถึง/แก้ไข Static Mutable Variable
- Implement Unsafe Trait
V
Vec<T> (Vector)
โครงสร้างข้อมูลแบบ Dynamic Array ที่เก็บข้อมูลบน Heap สามารถเพิ่ม/ลดขนาดได้ เป็น Collection ที่ใช้บ่อยที่สุดใน Rust
Z
Zero-Cost Abstraction
หลักการออกแบบของ Rust ที่ว่า Abstraction ต่างๆ (เช่น Generics, Iterators, Closures) จะไม่มีค่าใช้จ่ายเพิ่มเติมตอน Runtime เทียบกับการเขียนโค้ดเองด้วยมือ (Hand-written code)
หมายเหตุ: Glossary นี้จะถูกอัปเดตเพิ่มเติมเมื่อมีบทเรียนใหม่ๆ ในหนังสือ หากพบคำศัพท์ที่ต้องการให้เพิ่ม สามารถ แจ้งได้ที่ GitHub Issues
FAQ — คำถามที่พบบ่อย
รวบรวมคำถามที่นักพัฒนามักถามเกี่ยวกับ Rust และเนื้อหาในหนังสือเล่มนี้ พร้อมคำตอบสั้นๆ และลิงก์ไปยังบทที่เกี่ยวข้อง
ทั่วไป
Q: หนังสือเล่มนี้เหมาะกับใคร?
A: เหมาะกับนักพัฒนาที่มีพื้นฐาน Rust เบื้องต้นแล้ว (เช่น อ่าน The Rust Programming Language จบแล้ว) และต้องการเข้าใจปัญหาที่พบบ่อยในการเขียน Rust จริงๆ พร้อมวิธีแก้ไขอย่างเป็นระบบ
Q: ต้องอ่านตามลำดับไหม?
A: ไม่จำเป็นครับ แต่ละบทเขียนให้อ่านแยกกันได้ อย่างไรก็ตาม หากคุณเป็นมือใหม่ แนะนำให้อ่านตามลำดับ Part โดยเริ่มจาก Part I: Ownership, Borrowing และ Lifetime
Q: Rust เวอร์ชันไหนที่ใช้ในหนังสือ?
A: โค้ดตัวอย่างทั้งหมดทดสอบกับ Rust 1.75 ขึ้นไป (Stable) แต่แนวคิดส่วนใหญ่ใช้ได้กับทุกเวอร์ชันที่รองรับ Edition 2021
Ownership, Borrowing & Lifetime
Q: ทำไมเรียก angle.cos() แล้ว angle ยังใช้ได้อยู่ ทั้งๆ ที่ cos รับ self?
A: เพราะ f32 implement Copy Trait ทำให้ Rust ทำสำเนาค่าไปใช้ในฟังก์ชันแทนการ Move ตัวแปรต้นทางจึงยังใช้ได้ อ่านเพิ่มเติมได้ที่ การเป็นเจ้าของของ self
Q: self, &self, &mut self ต่างกันอย่างไร?
A:
self— ยึดครอง (Move) หรือทำสำเนา (Copy) ค่าเข้าไปในฟังก์ชัน&self— ยืมอ่านอย่างเดียว (Shared/Immutable Borrow)&mut self— ยืมไปแก้ไข (Exclusive/Mutable Borrow)
อ่านรายละเอียดและตัวอย่างได้ที่ การเป็นเจ้าของของ self
Q: Anonymous Lifetime '_ คืออะไร? ใช้เมื่อไหร่?
A: '_ คือ Lifetime ที่บอกให้ Rust อนุมาน (infer) ให้อัตโนมัติ ใช้เมื่อต้องการระบุว่า Return Type มี Lifetime แต่ไม่ต้องการเขียน Lifetime Parameter แบบเต็ม เช่น:
#![allow(unused)]
fn main() {
fn to_other_ctx(&mut self) -> OtherCtx<'_> { ... }
}
อ่านเพิ่มเติมได้ที่ Reborrowing และการแปลงโครงสร้างข้อมูล
Q: ทำไม read_line ถึงต่อท้าย (append) ข้อมูลแทนที่จะเขียนทับ?
A: เป็นการออกแบบเพื่อ Performance — ผู้เรียกสามารถนำ Buffer กลับมาใช้ซ้ำได้โดยไม่ต้อง Allocate หน่วยความจำใหม่ทุกครั้ง วิธีแก้คือเรียก .clear() ก่อน read_line ทุกรอบของลูป อ่านเพิ่มเติมได้ที่ กับดักของ read_line ในลูป
Q: Reborrowing คืออะไร? ต่างจาก Borrowing ปกติอย่างไร?
A: Reborrowing คือการที่ Rust ยืม Reference ที่มีอยู่แล้วมาใช้ต่อ โดยสร้าง Reference ใหม่ที่มี Lifetime สั้นกว่า Reference ต้นฉบับ ซึ่งช่วยให้ Mutable Reference สามารถถูกส่งต่อไปยังฟังก์ชันอื่นได้โดยไม่เสีย Ownership ของ Reference เดิม อ่านเพิ่มเติมได้ที่ Reborrowing และการแปลงโครงสร้างข้อมูล
การมีส่วนร่วม
Q: พบข้อผิดพลาดในหนังสือ จะแจ้งได้ที่ไหน?
A: สามารถเปิด Issue ได้ที่ GitHub Repository ของโปรเจกต์ครับ
Q: อยากเขียนบทใหม่หรือแก้ไขเนื้อหา ทำได้ไหม?
A: ยินดีเลยครับ สามารถ Fork Repository แล้วส่ง Pull Request เข้ามาได้ ดูรายละเอียดเพิ่มเติมที่ รายชื่อผู้ร่วมพัฒนา
มีคำถามอื่นที่ไม่อยู่ในรายการนี้? สามารถเปิด Issue ใน GitHub Repository ได้เลยครับ เราจะเพิ่มคำตอบเข้ามาในหน้านี้
Troubleshooting Index — ดัชนีแก้ปัญหา
รวมปัญหาและ Error ที่พบบ่อยในการเขียน Rust จัดเรียงตามหมวดหมู่เพื่อให้ค้นหาได้ง่าย พร้อมลิงก์ไปยังบทเรียนที่อธิบายรายละเอียดเพิ่มเติม
Ownership & Borrowing Errors
E0382: Use of moved value
อาการ: ใช้ตัวแปรหลังจากที่มันถูก Move ไปแล้ว
#![allow(unused)]
fn main() {
let s = String::from("hello");
let t = s; // s ถูก Move ไปที่ t
println!("{}", s); // ❌ E0382: value used here after move
}
สาเหตุ: ตัวแปรที่ไม่ได้ implement Copy trait จะถูกย้ายความเป็นเจ้าของ (Move) เมื่อ assign ให้ตัวแปรอื่นหรือส่งเข้าฟังก์ชัน
วิธีแก้:
- ใช้
.clone()เพื่อสร้างสำเนา - ใช้ Reference (
&s) แทนการส่งค่าตรงๆ - ปรับ API ให้รับ
&selfแทนself
📖 อ่านเพิ่มเติม: การเป็นเจ้าของของ self
E0502: Cannot borrow as mutable because it is also borrowed as immutable
อาการ: พยายาม Mutable Borrow ขณะที่ยังมี Immutable Borrow ค้างอยู่
#![allow(unused)]
fn main() {
let mut input = String::new();
let trimmed = input.trim(); // Immutable borrow
std::io::stdin().read_line(&mut input).unwrap(); // ❌ E0502
println!("{}", trimmed);
}
สาเหตุ: Rust ไม่อนุญาตให้มี &mut และ & ของตัวแปรเดียวกันอยู่พร้อมกัน เพื่อป้องกัน Data Race
วิธีแก้:
- ใช้
.to_string()หรือ.to_owned()เพื่อสร้าง Owned Value แทนการเก็บ Reference - จัดลำดับ Scope ให้ Immutable Borrow จบก่อนที่จะเริ่ม Mutable Borrow
📖 อ่านเพิ่มเติม: กับดักของ read_line ในลูป — Borrow Checker Error
E0499: Cannot borrow as mutable more than once at a time
อาการ: พยายาม Mutable Borrow ตัวแปรเดียวกัน 2 ครั้งพร้อมกัน
#![allow(unused)]
fn main() {
let mut v = vec![1, 2, 3];
let first = &mut v[0];
let second = &mut v[1]; // ❌ E0499
}
สาเหตุ: กฎ Aliasing XOR Mutation — Mutable Reference ต้องมีได้เพียงตัวเดียวในขณะใดขณะหนึ่ง
วิธีแก้:
- แยก Scope ของแต่ละ Mutable Borrow
- ใช้
split_at_mut()สำหรับ Slice - พิจารณาใช้
CellหรือRefCellสำหรับ Interior Mutability
Lifetime Errors
Lifetime may not live long enough
อาการ: พยายาม Return Reference ที่มี Lifetime สั้นกว่าที่ Signature กำหนด
#![allow(unused)]
fn main() {
fn to_other_ctx(&mut self) -> OtherCtx<'s> {
OtherCtx { window_state: self.window_state }
// ❌ lifetime may not live long enough
}
}
สาเหตุ: การเข้าถึง Field ผ่าน &mut self เป็น Reborrow ที่มี Lifetime สั้นกว่า Lifetime ดั้งเดิมของ Field
วิธีแก้:
- ใช้ Anonymous Lifetime
'_ใน Return Type:-> OtherCtx<'_> - หรือระบุ Lifetime แบบ Explicit ที่ผูกกับ
&mut self
📖 อ่านเพิ่มเติม: Reborrowing และการแปลงโครงสร้างข้อมูล
E0106: Missing lifetime specifier
อาการ: ลืมระบุ Lifetime ใน Struct หรือ Function Signature ที่มี Reference
#![allow(unused)]
fn main() {
struct Foo {
data: &str, // ❌ E0106: missing lifetime specifier
}
}
วิธีแก้:
- เพิ่ม Lifetime Parameter:
struct Foo<'a> { data: &'a str } - หากเป็นกรณีง่ายใน Function ให้อาศัย Lifetime Elision Rules
Common Runtime Issues
Silent Bug: Buffer ไม่ถูก Clear ในลูป
อาการ: ข้อมูลเก่าสะสมใน String buffer ทำให้เงื่อนไขในลูปไม่ทำงานตามที่คาดหวัง โปรแกรมไม่พังแต่ทำงานผิด
#![allow(unused)]
fn main() {
let mut input = String::new();
loop {
// ❌ ลืม input.clear()
io::stdin().read_line(&mut input).unwrap();
if input.trim() == "exit" { break; } // ไม่มีวันเป็น true หลังรอบแรก
}
}
สาเหตุ: read_line() ออกแบบมาให้ Append ข้อมูลต่อท้ายเสมอ ไม่ใช่ Overwrite
วิธีแก้:
- เรียก
.clear()ก่อนread_line()ทุกรอบ - หรือประกาศ
String::new()ภายในลูป - หรือใช้
stdin().lock().lines()แทน
📖 อ่านเพิ่มเติม: กับดักของ read_line ในลูป — บั๊กเงียบ
Silent Bug: mut self กับ Copy Types
อาการ: เรียกเมธอดที่รับ mut self บน Copy Type แล้วค่าไม่เปลี่ยน
#![allow(unused)]
fn main() {
#[derive(Clone, Copy)]
struct Point { x: i32, y: i32 }
impl Point {
fn move_wrong(mut self, dx: i32) {
self.x += dx; // แก้ไขแค่สำเนา!
}
}
}
สาเหตุ: สำหรับ Copy Type การรับ self (by value) จะทำสำเนาเข้ามา การแก้ไขจึงเกิดขึ้นกับสำเนาเท่านั้น
วิธีแก้:
- ใช้
&mut selfหากต้องการแก้ไขค่าเดิม - หรือ Return
Selfกลับไป (Functional style):fn moved(self, dx: i32) -> Self
📖 อ่านเพิ่มเติม: การเป็นเจ้าของของ self — หลุมพราง Silent Bug
Quick Lookup by Error Code
| Error Code | ชื่อ Error | หมวดหมู่ | ลิงก์ |
|---|---|---|---|
| E0106 | Missing lifetime specifier | Lifetime | ด้านบน |
| E0382 | Use of moved value | Ownership | ด้านบน |
| E0499 | Cannot borrow &mut more than once | Borrowing | ด้านบน |
| E0502 | Cannot borrow &mut while & exists | Borrowing | ด้านบน |
แหล่งข้อมูลเพิ่มเติม
- Rust Compiler Error Index — รวม Error Code ทั้งหมดจาก Rust Compiler
- Rust Reference: Borrow Checker — เอกสารอ้างอิงกฎการยืม
- Common Rust Lifetime Misconceptions — บทความยอดนิยมเรื่อง Lifetime
รายชื่อผู้ร่วมพัฒนา (Contributors)
ขอขอบคุณทุกท่านที่มีส่วนร่วมในการสร้างและพัฒนา Rust Problem-Solving Handbook (TH) ให้เป็นแหล่งเรียนรู้ที่มีคุณภาพสำหรับชุมชนนักพัฒนา Rust ภาษาไทย
ผู้เขียนหลัก (Main Authors)
| ชื่อ | บทบาท | GitHub |
|---|---|---|
| Suradet Pratomsak | ผู้เขียนหลัก / Documentation Architect | @suradet-ps |
วิธีมีส่วนร่วม (How to Contribute)
เรายินดีต้อนรับการมีส่วนร่วมจากทุกคน ไม่ว่าจะเป็น
เนื้อหา (Content)
- เขียนบทความใหม่เกี่ยวกับปัญหาที่พบในภาษา Rust
- ปรับปรุงคำอธิบายหรือตัวอย่างโค้ดที่มีอยู่
- แปลเนื้อหาเป็นภาษาอื่น
แจ้งปัญหา (Bug Reports)
- หากพบข้อผิดพลาดในเนื้อหา สามารถเปิด Issue ได้เลยครับ
- แจ้ง Typo, ลิงก์เสีย หรือโค้ดตัวอย่างที่ไม่ถูกต้อง
เสนอหัวข้อใหม่ (Feature Requests)
- มีปัญหา Rust ที่น่าสนใจ เสนอเป็นหัวข้อบทความใหม่ได้ครับ
- แนะนำการปรับปรุงโครงสร้างหรือ UX ของหนังสือ
ขั้นตอนการ Contribute
- Fork Repository จาก GitHub
- สร้าง Branch ใหม่สำหรับการแก้ไข
- เขียนเนื้อหาหรือแก้ไขตามต้องการ
- ทดสอบด้วย
mdbook buildให้แน่ใจว่า Build ผ่าน - เปิด Pull Request พร้อมคำอธิบายสิ่งที่เปลี่ยนแปลง
ขอบคุณเป็นพิเศษ (Special Thanks)
- ชุมชน Rust Dev Community — สำหรับแรงบันดาลใจและ Feedback ที่มีค่า
- Rust Users Forum — แหล่งที่มาของคำถามและกรณีศึกษาหลายบท
- mdBook — เครื่องมือที่ใช้สร้างหนังสือเล่มนี้
“การแบ่งปันความรู้คือการลงทุนที่ไม่มีวันขาดทุน”