สมาร์ตพอยน์เตอร์ใน Rust อธิบายครบ: Box, Rc, Arc และ RefCell ปี 2026

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

แผนภาพการจัดการหน่วยความจำของสมาร์ตพอยน์เตอร์ Rust Box, Rc, Arc และ RefCell

สมาร์ตพอยน์เตอร์ใน 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 จะตัดการเวียนเกิดออกที่ระดับชนิด

tree.rsrust
#[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 แต่ละครั้งจะลดมันลง เมื่อจำนวนนับแตะศูนย์ ค่านั้นจะถูกคืน

ลองพิจารณาออบเจ็กต์การตั้งค่าที่เวิร์กเกอร์หลายตัวอ่านร่วมกัน เวิร์กเกอร์แต่ละตัวควรถือการตั้งค่าไว้นานเท่าที่จำเป็น และหน่วยความจำที่จองไว้ควรหายไปก็ต่อเมื่อเวิร์กเกอร์ตัวสุดท้ายหมดไปแล้ว

shared_config.rsrust
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>> เจ้าของหลายรายที่ทุกรายสามารถอัปเดตค่าเดียวกันได้

counter.rsrust
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
}
RefCell เลื่อนการตรวจสอบการยืมไปที่รันไทม์

การเรียก 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>> สำหรับโค้ดที่ทำงานพร้อมกัน ตัวอย่างต่อไปนี้สร้างสี่เธรดที่แต่ละเธรดเพิ่มค่าผลรวมที่ใช้ร่วมกันหนึ่งพันครั้ง

parallel_sum.rsrust
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> เพื่อเลี่ยงการรั่ว
  • ตั้งค่าเริ่มต้นเป็นชนิดที่มีอำนาจน้อยที่สุด และปล่อยให้คอมไพเลอร์บอกว่าเมื่อใดจึงจำเป็นต้องมีความสามารถมากขึ้น

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#rust
#smart-pointers
#memory-management
#interview
#rust-2024-edition

แชร์

บทความที่เกี่ยวข้อง

แผนภาพแสดงระบบ Ownership และ Borrowing ของภาษา Rust

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

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

คู่มือขั้นสูง Rust traits และ generics พร้อมตัวอย่างโค้ดและโลโก้ปู Rust

Rust Traits และ Generics ฉบับสมบูรณ์ 2026: Trait Upcasting, AsyncFn และรูปแบบขั้นสูงสำหรับสัมภาษณ์งาน

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

คำถามสัมภาษณ์ Rust - คู่มือฉบับสมบูรณ์

คำถามสัมภาษณ์ Rust: คู่มือฉบับสมบูรณ์ 2026

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