สมาร์ตพอยน์เตอร์ใน Rust อธิบายครบ: Box, Rc, Arc และ RefCell ปี 2026
อธิบายสมาร์ตพอยน์เตอร์ Box, Rc, Arc และ RefCell ใน Rust พร้อมตัวอย่างที่คอมไพล์ได้ปี 2026 ตารางตัดสินใจ และคำถามสัมภาษณ์ที่พบบ่อย

สมาร์ตพอยน์เตอร์ใน Rust คือเครื่องมือที่ปลดล็อกรูปแบบการเป็นเจ้าของซึ่ง borrow checker เพียงลำพังไม่อาจแสดงออกได้ ได้แก่ การจองหน่วยความจำบนฮีป การเป็นเจ้าของร่วมกัน และการกลายพันธุ์ค่าผ่านการอ้างอิงที่ใช้ร่วมกัน ในขณะที่การอ้างอิงธรรมดา (&T) เพียงยืมค่ามาเท่านั้น สมาร์ตพอยน์เตอร์เป็นเจ้าของข้อมูลของมันและเพิ่มพฤติกรรมพิเศษทับลงไป คู่มือนี้แจกแจงสี่ชนิดที่นักพัฒนา Rust ทุกคนพบทั้งในระบบจริงและในการสัมภาษณ์ ได้แก่ Box, Rc, Arc และ RefCell พร้อมตัวอย่างที่คอมไพล์ได้ซึ่งมุ่งไปที่ Rust รุ่นปี 2024
ใช้ Box<T> สำหรับการจองฮีปที่มีเจ้าของคนเดียว, Rc<T> สำหรับการเป็นเจ้าของร่วมบนเธรดเดียว, Arc<T> สำหรับการเป็นเจ้าของร่วมข้ามเธรด และ RefCell<T> เพื่อกลายพันธุ์ค่าผ่านการอ้างอิงที่ใช้ร่วมกัน คู่ Rc<RefCell<T>> และ Arc<Mutex<T>> ครอบคลุมสถานะที่ใช้ร่วมและแก้ไขได้
สมาร์ตพอยน์เตอร์ใน Rust คืออะไร
สมาร์ตพอยน์เตอร์คือ struct ที่มีพฤติกรรมเหมือนพอยน์เตอร์แต่พกพาเมทาดาทาหรือความสามารถเพิ่มเติมมาด้วย ส่วนใหญ่จะอิมพลีเมนต์เทรต Deref ทำให้ *pointer และการเรียกเมท็อดทำงานเสมือนว่าพอยน์เตอร์เป็นการอ้างอิงธรรมดา และเทรต Drop ทำให้การเก็บกวาดทำงานอัตโนมัติเมื่อค่านั้นออกจากขอบเขต ไลบรารีมาตรฐานมาพร้อมสี่ชนิดที่กล่าวถึงในที่นี้ และการเข้าใจมันขึ้นอยู่กับความเข้าใจที่แน่นในเรื่องการเป็นเจ้าของและการยืม ซึ่งเป็นตัวกำหนดว่าใครจะคืนหน่วยความจำแต่ละก้อนและเมื่อใด
ความแตกต่างสำคัญจากการอ้างอิงคือการเป็นเจ้าของ &T ไม่เคยเป็นเจ้าของข้อมูลที่มันชี้ไป จึงไม่อาจอยู่ได้นานกว่าตัวค่า สมาร์ตพอยน์เตอร์เป็นเจ้าของข้อมูล ควบคุมช่วงอายุของมัน และคืนหน่วยความจำอย่างแน่นอนคาดเดาได้ บทว่าด้วยสมาร์ตพอยน์เตอร์ใน Rust Book จัดพวกมันเป็นหมวดหมู่เดียวกันก็เพราะมันมีรูปแบบการเป็นเจ้าของบวกพฤติกรรมร่วมกันนี้เอง
Box<T>: การจองฮีปสำหรับชนิดเวียนเกิดและชนิดที่รู้ขนาด
Box<T> คือสมาร์ตพอยน์เตอร์ที่ง่ายที่สุด มันเก็บค่าไว้บนฮีปและถือพอยน์เตอร์ที่ชี้ไปยังค่านั้นไว้บนสแตก โดยมีเจ้าของเพียงคนเดียวและไม่มีภาระตอนรันไทม์นอกจากการจองหน่วยความจำเอง งานที่พบบ่อยที่สุดของมันคือการให้ชนิดเวียนเกิดมีขนาดที่รู้แน่ชัด ชนิดที่บรรจุตัวเองโดยตรงจะมีขนาดใหญ่ไม่รู้จบ แต่ Box เป็นเพียงพอยน์เตอร์ ขนาดของมันจึงคงที่ไม่ว่าจะชี้ไปยังอะไร
ตัวอย่างด้านล่างนิยามต้นไม้ทวิภาค แต่ละ Node ถือลูกสองตัว และหากไม่มีการอ้างอิงทางอ้อม คอมไพเลอร์จะคำนวณขนาดของ Tree ไม่ได้ การห่อลูกแต่ละตัวไว้ใน Box จะตัดการเวียนเกิดออกที่ระดับชนิด
#[derive(Debug)]
enum Tree {
Leaf(i32),
// Box<Tree> puts each child on the heap, so Node has a fixed size (two pointers).
Node(Box<Tree>, Box<Tree>),
}
fn sum(tree: &Tree) -> i32 {
match tree {
Tree::Leaf(value) => *value, // base case: return the leaf
Tree::Node(left, right) => sum(left) + sum(right), // recurse into both children
}
}
fn main() {
// Build (1) + ((2) + (3)) = 6
let tree = Tree::Node(
Box::new(Tree::Leaf(1)),
Box::new(Tree::Node(
Box::new(Tree::Leaf(2)),
Box::new(Tree::Leaf(3)),
)),
);
println!("sum = {}", sum(&tree)); // sum = 6
}Box ยังสำคัญเมื่อการย้ายค่าขนาดใหญ่จะมีต้นทุนสูง หรือเมื่อคืนค่าเป็น trait object อย่าง Box<dyn Error> ในทุกกรณีกฎเหมือนเดิม คือมีเจ้าของคนเดียว และถูกคืนอัตโนมัติเมื่อ Box ถูก drop
Rc<T>: การเป็นเจ้าของร่วมในโค้ดเธรดเดียว
บางครั้งค่าหนึ่งต้องการเจ้าของหลายราย และไม่มีรายใดชัดเจนว่าเป็นรายสุดท้ายที่ใช้มัน Rc<T> (นับการอ้างอิง) แก้ปัญหานี้ด้วยการเก็บจำนวนนับแบบเข้ม (strong count) ว่ามีเจ้าของอยู่กี่ราย Rc::clone เพิ่มจำนวนนับนั้นโดยไม่คัดลอกข้อมูลที่อยู่เบื้องล่าง และการ drop แต่ละครั้งจะลดมันลง เมื่อจำนวนนับแตะศูนย์ ค่านั้นจะถูกคืน
ลองพิจารณาออบเจ็กต์การตั้งค่าที่เวิร์กเกอร์หลายตัวอ่านร่วมกัน เวิร์กเกอร์แต่ละตัวควรถือการตั้งค่าไว้นานเท่าที่จำเป็น และหน่วยความจำที่จองไว้ควรหายไปก็ต่อเมื่อเวิร์กเกอร์ตัวสุดท้ายหมดไปแล้ว
use std::rc::Rc;
#[derive(Debug)]
struct Config {
endpoint: String,
timeout_ms: u32,
}
fn main() {
// Rc::new moves Config onto the heap with a strong count of 1.
let config = Rc::new(Config {
endpoint: "https://api.example.com".to_string(),
timeout_ms: 5000,
});
// Rc::clone only bumps the reference count; it does not deep-copy Config.
let worker_a = Rc::clone(&config);
let worker_b = Rc::clone(&config);
// All three handles point to the same allocation.
println!("endpoint: {}", worker_a.endpoint);
println!("timeout: {}", worker_b.timeout_ms);
// strong_count reports how many owners currently hold the value.
println!("owners: {}", Rc::strong_count(&config)); // owners: 3
}การเรียก Rc::clone อย่างชัดแจ้ง (แทนที่จะเป็น config.clone()) เป็นสำนวนที่เหมาะสม มันส่งสัญญาณให้ผู้อ่านรู้ว่าการดำเนินการนี้ราคาถูกและแตะเพียงตัวนับเท่านั้น ข้อควรระวังคือ Rc<T> แจกจ่ายเฉพาะการอ้างอิงที่ใช้ร่วมและเปลี่ยนแปลงไม่ได้เท่านั้น มันไม่สามารถกลายพันธุ์ค่าที่แบ่งปัน และไม่ปลอดภัยที่จะส่งข้ามเธรด เอกสารของ Rc ระบุข้อจำกัดทั้งสองอย่างชัดเจน
RefCell<T> และการเปลี่ยนแปลงภายในใน Rust
โดยปกติ Rust บังคับว่าค่าหนึ่งจะถูกแบ่งปันผ่านการอ้างอิงแบบเปลี่ยนแปลงไม่ได้หลายตัว หรือถูกกลายพันธุ์ผ่านการอ้างอิงแบบเปลี่ยนแปลงได้เพียงตัวเดียวเท่านั้น และมันตรวจสอบสิ่งนี้ในเวลาคอมไพล์ RefCell<T> มอบการเปลี่ยนแปลงภายใน (interior mutability) มันยอมให้โค้ดกลายพันธุ์ค่าผ่านการอ้างอิงที่ใช้ร่วมกันได้ โดยเลื่อนการตรวจสอบเดียวกันนั้นไปไว้ที่เวลารันไทม์ borrow() คืนตัวยาม (guard) สำหรับอ่านแบบใช้ร่วม ส่วน borrow_mut() คืนตัวยามสำหรับเขียนแบบเอกสิทธิ์ กฎเหมือนกันทุกประการ แต่การละเมิดจะทำให้เกิด panic แทนที่จะคอมไพล์ไม่ผ่าน
เมื่อรวมกับ Rc จะได้รูปแบบใช้ร่วมและแก้ไขได้บนเธรดเดียวอันเป็นแบบฉบับ คือ Rc<RefCell<T>> เจ้าของหลายรายที่ทุกรายสามารถอัปเดตค่าเดียวกันได้
use std::rc::Rc;
use std::cell::RefCell;
// Rc gives shared ownership; RefCell allows mutation through a shared reference.
type SharedCounter = Rc<RefCell<u32>>;
fn increment(counter: &SharedCounter) {
// borrow_mut() hands out an exclusive reference, checked at runtime.
*counter.borrow_mut() += 1;
}
fn main() {
let counter: SharedCounter = Rc::new(RefCell::new(0));
let handle_a = Rc::clone(&counter);
let handle_b = Rc::clone(&counter);
increment(&handle_a);
increment(&handle_b);
increment(&counter);
// borrow() gives a shared read guard; the value is now 3.
println!("count = {}", counter.borrow()); // count = 3
}การเรียก borrow_mut() ขณะที่ตัวยาม borrow() หรือ borrow_mut() อีกตัวยังมีชีวิตอยู่ จะเกิด panic พร้อมข้อความ already borrowed: BorrowMutError การรับประกันความปลอดภัยยังคงอยู่ แต่บั๊กเชิงตรรกะที่คอมไพเลอร์น่าจะจับได้กลับกลายเป็นการแครชในเวลารันไทม์ ควรทำให้ตัวยามมีอายุสั้นและหลีกเลี่ยงการถือการยืมข้ามการเรียกฟังก์ชันที่อาจเข้าสู่ RefCell เดิมซ้ำ
การเปลี่ยนแปลงภายในยังเป็นที่ซ่อนตัวของวงจรการอ้างอิงด้วย ค่า Rc<RefCell<T>> สองตัวที่ชี้หากันจะทำให้จำนวนนับแบบเข้มของกันและกันอยู่เหนือศูนย์ตลอดไป ทำให้ไม่มีตัวใดถูกคืนเลย ทางแก้คือ Weak<T> แฮนเดิลที่ไม่เป็นเจ้าของและไม่กระทบต่อจำนวนนับแบบเข้ม คำอธิบายคลาสสิกอยู่ที่ Learn Rust With Entirely Too Many Linked Lists
Arc<T>: การนับการอ้างอิงที่ปลอดภัยต่อเธรดสำหรับงานพร้อมกัน
Arc<T> (นับการอ้างอิงแบบอะตอม) คือพี่น้องฝั่งหลายเธรดของ Rc<T> API สาธารณะแทบจะเหมือนกัน แต่ตัวนับใช้ปฏิบัติการแบบอะตอม ทำให้การโคลนและ drop Arc จากหลายเธรดพร้อมกันยังคงถูกต้อง ความเป็นอะตอมนี้เองคือเหตุผลที่เลือก Arc เฉพาะเมื่อการแบ่งปันข้ามขอบเขตเธรด การเพิ่มค่าแบบอะตอมช้ากว่าการเพิ่มธรรมดาที่ Rc ใช้อย่างวัดได้
เนื่องจาก Arc<T> ยังคงแจกจ่ายการอ้างอิงที่เปลี่ยนแปลงไม่ได้ การกลายพันธุ์ข้ามเธรดจึงต้องใช้พรีมิทีฟการซิงโครไนซ์ Mutex<T> เป็นคู่หูที่พบบ่อย ให้รูปแบบ Arc<Mutex<T>> ซึ่งสะท้อน Rc<RefCell<T>> สำหรับโค้ดที่ทำงานพร้อมกัน ตัวอย่างต่อไปนี้สร้างสี่เธรดที่แต่ละเธรดเพิ่มค่าผลรวมที่ใช้ร่วมกันหนึ่งพันครั้ง
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Arc is safe to share across threads; Mutex serializes access to the inner value.
let total = Arc::new(Mutex::new(0u64));
let mut handles = Vec::new();
for _ in 0..4 {
// Each thread gets its own Arc handle (an atomic count bump).
let total = Arc::clone(&total);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
// lock() blocks until the mutex is free, then returns a guard.
let mut value = total.lock().unwrap();
*value += 1;
}
}));
}
for handle in handles {
handle.join().unwrap(); // wait for every thread before reading
}
println!("total = {}", *total.lock().unwrap()); // total = 4000
}คอมไพเลอร์บังคับใช้ขอบเขตนี้ให้โดยอัตโนมัติ Rc<T> ไม่ใช่ Send การพยายามย้ายมันเข้าไปใน thread::spawn จึงคอมไพล์ไม่ผ่าน ผลักดันให้หันไปใช้ Arc การหยิบ Arc มาใช้ภายในเธรดเดียวเป็นเพียงการจ่ายค่าปฏิบัติการอะตอมที่ไม่เคยได้ใช้ เอกสารของ Arc อธิบายการรับประกันเรื่องลำดับหน่วยความจำโดยละเอียด และ Arc<Mutex<T>> เป็นรากฐานของสถานะที่ใช้ร่วมส่วนใหญ่ใน Rust แบบอะซิงโครนัสด้วย Tokio
พร้อมที่จะพิชิตการสัมภาษณ์ Rust แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Box vs Rc vs Arc vs RefCell: ควรใช้ตัวไหนเมื่อไหร่
ทั้งสี่ชนิดประกอบเข้าด้วยกันตามสองแกน คือ ค่าหนึ่งมีเจ้าของกี่ราย และมันสามารถถูกกลายพันธุ์ผ่านแฮนเดิลที่ใช้ร่วมได้หรือไม่ ตารางนี้สรุปข้อแลกเปลี่ยน
| ชนิด | การเป็นเจ้าของ | กลายพันธุ์ผ่านแฮนเดิลที่ใช้ร่วม | ปลอดภัยต่อเธรด | ภาระ |
|------|-----------|----------------------------|-------------|----------|
| Box<T> | รายเดียว | ไม่ | ได้ ถ้า T: Send | จองฮีปเท่านั้น |
| Rc<T> | ร่วมกัน | ไม่ | ไม่ | นับแบบไม่อะตอม |
| Arc<T> | ร่วมกัน | ไม่ | ได้ | นับแบบอะตอม |
| RefCell<T> | รายเดียว | ได้ (ตรวจตอนรันไทม์) | ไม่ | ธงการยืมตอนรันไทม์ |
ต้นไม้การตัดสินใจในทางปฏิบัตินั้นสั้น ต้องการเจ้าของรายเดียวบนฮีป ใช้ Box ต้องการเจ้าของหลายรายบนเธรดเดียว ใช้ Rc ต้องการเจ้าของหลายรายข้ามเธรด ใช้ Arc ต้องการกลายพันธุ์ค่าที่ใช้ร่วม ให้ห่อชนิดภายในด้วย RefCell (เธรดเดียว) หรือ Mutex (หลายเธรด) คำแนะนำใน Effective Rust ว่าด้วยชนิดของการอ้างอิงและพอยน์เตอร์ ก็ไปถึงการจัดชั้นแบบเดียวกัน เริ่มจากตัวเลือกที่มีอำนาจน้อยที่สุดและเพิ่มความสามารถเฉพาะเมื่อคอมไพเลอร์บังคับเท่านั้น
คำถามสัมภาษณ์เรื่องสมาร์ตพอยน์เตอร์ Rust
สมาร์ตพอยน์เตอร์เป็นหัวข้อสัมภาษณ์ที่เชื่อถือได้ เพราะมันทดสอบว่าผู้สมัครเข้าใจการเป็นเจ้าของหรือเข้าใจเพียงไวยากรณ์ คำถามด้านล่างปรากฏบ่อยครั้ง และยังมีอีกมากที่รวบรวมไว้ใน โมดูลสัมภาษณ์สมาร์ตพอยน์เตอร์ Rust
Rc กับ Arc ต่างกันอย่างไร? ทั้งคู่ให้การเป็นเจ้าของร่วมผ่านการนับการอ้างอิง Rc ใช้ตัวนับแบบไม่อะตอมและถูกจำกัดอยู่บนเธรดเดียว ส่วน Arc ใช้ปฏิบัติการแบบอะตอมและสามารถแบ่งปันข้ามเธรดได้ด้วยต้นทุนรันไทม์เล็กน้อย คอมไพเลอร์บังคับการแยกนี้ Rc ไม่ใช่ทั้ง Send และ Sync จึงข้ามขอบเขตเธรดไม่ได้
ทำไมต้องรวม Rc กับ RefCell? Rc ให้การเป็นเจ้าของร่วมแต่เข้าถึงได้แบบเปลี่ยนแปลงไม่ได้เท่านั้น RefCell เพิ่มการเปลี่ยนแปลงภายใน ให้เจ้าของกลายพันธุ์ค่าผ่านการอ้างอิงที่ใช้ร่วมพร้อมการตรวจสอบการยืมตอนรันไทม์ เมื่อรวมกัน Rc<RefCell<T>> คือหน่วยประกอบใช้-ร่วม-แก้ไข-ได้บนเธรดเดียวที่เป็นสำนวนมาตรฐาน
การนับการอ้างอิงทำให้หน่วยความจำรั่วใน Rust ได้อย่างไร? ค่า Rc (หรือ Arc) สองตัวที่อ้างอิงกันและกันก่อเป็นวงจรที่จำนวนนับแบบเข้มไม่มีวันแตะศูนย์ หน่วยความจำที่จองไว้จึงไม่ถูกคืนเลย การตัดวงจรด้วย Weak<T> ซึ่งถือการอ้างอิงแบบไม่เป็นเจ้าของ จะฟื้นการเก็บกวาดที่ถูกต้องกลับมา
เมื่อไรที่ Box ดีกว่า Rc? เมื่อใดก็ตามที่เจ้าของรายเดียวก็เพียงพอ Box ไม่มีภาระของการนับการอ้างอิง จึงเป็นค่าเริ่มต้นสำหรับการจองฮีป ชนิดเวียนเกิด และ trait object หยิบ Rc มาใช้เฉพาะเมื่อจำเป็นต้องมีการเป็นเจ้าของร่วมอย่างแท้จริงเท่านั้น
บทสรุป
สมาร์ตพอยน์เตอร์ใน Rust เปลี่ยนการเป็นเจ้าของจากข้อจำกัดให้กลายเป็นชุดหน่วยประกอบที่นำมาต่อกันได้ การเลือกใช้ระหว่างพวกมันเป็นไปตามรูปร่างของปัญหาโดยตรง:
- ใช้
Box<T>เมื่อค่าหนึ่งต้องการเจ้าของบนฮีปเพียงรายเดียว เมื่อชนิดเวียนเกิดต้องการขนาดที่คงที่ หรือเมื่อฟังก์ชันคืนค่าเป็น trait object - ใช้
Rc<T>สำหรับเจ้าของหลายรายบนเธรดเดียว และเปลี่ยนไปใช้Arc<T>ทันทีที่การเป็นเจ้าของข้ามขอบเขตเธรด - เพิ่ม
RefCell<T>สำหรับการเปลี่ยนแปลงภายในในโค้ดเธรดเดียว และMutex<T>สำหรับสิ่งที่เทียบเท่าในงานพร้อมกัน โดยทำให้ตัวยามการยืมและการล็อกมีอายุสั้น - รวมพวกมันอย่างจงใจ
Rc<RefCell<T>>สำหรับสถานะที่ใช้ร่วมและแก้ไขได้บนเธรดเดียวArc<Mutex<T>>สำหรับข้ามเธรด - ระวังวงจรการอ้างอิงระหว่างพอยน์เตอร์ที่มีการนับ และตัดมันด้วย
Weak<T>เพื่อเลี่ยงการรั่ว - ตั้งค่าเริ่มต้นเป็นชนิดที่มีอำนาจน้อยที่สุด และปล่อยให้คอมไพเลอร์บอกว่าเมื่อใดจึงจำเป็นต้องมีความสามารถมากขึ้น
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

ทำความเข้าใจ Ownership และ Borrowing ใน Rust อย่างลึกซึ้ง สำหรับนักพัฒนาและผู้เตรียมสอบสัมภาษณ์
บทความเจาะลึกระบบ Ownership และ Borrowing ของ Rust ตั้งแต่พื้นฐานจนถึงขั้นสูง พร้อมตัวอย่างโค้ดจริงและแนวทางแก้ปัญหา Compiler Error ที่พบบ่อย เหมาะสำหรับการเตรียมสัมภาษณ์งาน

Rust Traits และ Generics ฉบับสมบูรณ์ 2026: Trait Upcasting, AsyncFn และรูปแบบขั้นสูงสำหรับสัมภาษณ์งาน
คู่มือเชิงลึกเกี่ยวกับ Rust traits และ generics พร้อมฟีเจอร์ใหม่จาก Rust 2024 Edition: trait upcasting, AsyncFn closures, RPITIT และรูปแบบขั้นสูงที่ใช้ในการสัมภาษณ์จริง

คำถามสัมภาษณ์ Rust: คู่มือฉบับสมบูรณ์ 2026
25 คำถามสัมภาษณ์ Rust ที่พบบ่อยที่สุด Ownership, borrowing, lifetime, trait, async และ concurrency พร้อมคำตอบละเอียดและตัวอย่างโค้ด