# Con trỏ thông minh trong Rust: Box, Rc, Arc và RefCell năm 2026 > Con trỏ thông minh Box, Rc, Arc và RefCell trong Rust được giải thích qua các ví dụ biên dịch được năm 2026, bảng quyết định và những câu hỏi phỏng vấn thường gặp. - Published: 2026-07-05 - Updated: 2026-07-07 - Author: SharpSkill - Tags: rust, smart-pointers, memory-management, interview, rust-2024-edition - Reading time: 11 min --- Con trỏ thông minh trong Rust là những công cụ mở ra các mẫu sở hữu mà chỉ riêng trình kiểm tra mượn không thể diễn đạt: cấp phát trên heap, sở hữu chung, và đột biến sau một tham chiếu dùng chung. Trong khi một tham chiếu thông thường (`&T`) chỉ mượn một giá trị, một con trỏ thông minh sở hữu dữ liệu của nó và bổ sung thêm hành vi lên trên. Hướng dẫn này phân tích bốn kiểu mà mọi lập trình viên Rust đều gặp trong sản xuất lẫn trong phỏng vấn: `Box`, `Rc`, `Arc` và `RefCell`, với các ví dụ biên dịch được nhắm tới Rust phiên bản 2024. > **Tóm tắt trong một dòng** > > Dùng `Box` để cấp phát heap với một chủ sở hữu duy nhất, `Rc` để sở hữu chung trên một luồng, `Arc` để sở hữu chung qua nhiều luồng, và `RefCell` để đột biến một giá trị thông qua một tham chiếu dùng chung. Cặp `Rc>` và `Arc>` bao quát trạng thái dùng chung có thể thay đổi. ## Con trỏ thông minh là gì trong Rust Một con trỏ thông minh là một struct hành xử như một con trỏ nhưng mang thêm siêu dữ liệu hoặc khả năng. Hầu hết triển khai trait `Deref`, nên `*pointer` và các lời gọi phương thức hoạt động như thể con trỏ là một tham chiếu thông thường, cùng trait `Drop`, nên việc dọn dẹp chạy tự động khi giá trị rời khỏi phạm vi. Thư viện chuẩn cung cấp bốn kiểu được đề cập ở đây, và việc hiểu chúng phụ thuộc vào nắm vững [quyền sở hữu và mượn](/blog/rust/ownership-borrowing-rust-complete-guide), vốn quyết định ai giải phóng mỗi vùng cấp phát và khi nào. Điểm khác biệt then chốt so với một tham chiếu là quyền sở hữu. `&T` không bao giờ sở hữu dữ liệu mà nó trỏ tới, nên nó không thể sống lâu hơn giá trị. Một con trỏ thông minh sở hữu dữ liệu, kiểm soát vòng đời của nó, và giải phóng nó một cách tất định. [Chương về con trỏ thông minh trong Rust Book](https://doc.rust-lang.org/book/ch15-00-smart-pointers.html) xem chúng như một nhóm chính vì chúng cùng chia sẻ hình dạng sở-hữu-cộng-hành-vi này. ## `Box`: cấp phát heap cho kiểu đệ quy và kiểu có kích thước xác định `Box` là con trỏ thông minh đơn giản nhất. Nó lưu một giá trị trên heap và giữ một con trỏ tới nó trên stack, với một chủ sở hữu duy nhất và không có chi phí runtime nào ngoài chính việc cấp phát. Nhiệm vụ phổ biến nhất của nó là cho các kiểu đệ quy một kích thước đã biết: một kiểu chứa chính nó một cách trực tiếp sẽ lớn vô hạn, nhưng một `Box` chỉ là một con trỏ, nên kích thước của nó cố định bất kể nó trỏ tới cái gì. Ví dụ bên dưới định nghĩa một cây nhị phân. Mỗi `Node` giữ hai nút con, và không có sự gián tiếp thì trình biên dịch không thể tính kích thước của `Tree`. Bọc mỗi nút con trong một `Box` phá vỡ đệ quy ở cấp độ kiểu. ```rust // tree.rs #[derive(Debug)] enum Tree { Leaf(i32), // Box puts each child on the heap, so Node has a fixed size (two pointers). Node(Box, Box), } 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` cũng quan trọng khi việc di chuyển một giá trị lớn sẽ tốn kém, hoặc khi trả về một trait object như `Box`. Trong mọi trường hợp, quy tắc là như nhau: một chủ sở hữu, được giải phóng tự động khi `Box` bị drop. ## `Rc`: sở hữu chung trong mã đơn luồng Đôi khi một giá trị cần nhiều chủ sở hữu, và không cái nào rõ ràng là cái cuối cùng dùng nó. `Rc` (đếm tham chiếu) giải quyết điều này bằng cách giữ một số đếm mạnh về số lượng chủ sở hữu đang tồn tại. `Rc::clone` tăng số đếm đó mà không sao chép dữ liệu bên dưới, và mỗi lần drop sẽ giảm nó. Khi số đếm chạm không, giá trị được giải phóng. Hãy xét một đối tượng cấu hình mà nhiều worker cùng đọc. Mỗi worker nên giữ cấu hình chừng nào còn cần, và vùng cấp phát chỉ nên biến mất khi worker cuối cùng không còn. ```rust // shared_config.rs 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 } ``` Gọi `Rc::clone` một cách tường minh (thay vì `config.clone()`) là cách viết đúng thành ngữ: nó báo cho người đọc rằng thao tác này rẻ và chỉ chạm vào một bộ đếm. Điều cần lưu ý là `Rc` chỉ trao ra các tham chiếu dùng chung, bất biến. Nó không thể đột biến giá trị mà nó chia sẻ, và không an toàn để gửi qua các luồng. [Tài liệu `Rc`](https://doc.rust-lang.org/std/rc/struct.Rc.html) nêu rõ cả hai ràng buộc. ## `RefCell` và tính khả biến nội tại trong Rust Rust thông thường buộc rằng một giá trị hoặc được chia sẻ qua nhiều tham chiếu bất biến, hoặc bị đột biến qua đúng một tham chiếu khả biến, và nó kiểm tra điều này tại thời điểm biên dịch. `RefCell` cung cấp tính khả biến nội tại: nó cho phép mã đột biến một giá trị qua một tham chiếu dùng chung bằng cách dời chính phép kiểm tra đó sang thời điểm chạy. `borrow()` trả về một guard đọc dùng chung; `borrow_mut()` trả về một guard ghi độc quyền. Các quy tắc giống hệt nhau, nhưng vi phạm sẽ gây panic thay vì thất bại khi biên dịch. Kết hợp với `Rc`, điều này tạo ra mẫu dùng-chung-khả-biến đơn luồng kinh điển, `Rc>`: nhiều chủ sở hữu đều có thể cập nhật cùng một giá trị. ```rust // counter.rs use std::rc::Rc; use std::cell::RefCell; // Rc gives shared ownership; RefCell allows mutation through a shared reference. type SharedCounter = Rc>; 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 dời việc kiểm tra mượn sang lúc chạy** > > Gọi `borrow_mut()` trong khi một guard `borrow()` hoặc `borrow_mut()` khác vẫn còn sống sẽ gây panic với `already borrowed: BorrowMutError`. Các bảo đảm an toàn vẫn giữ vững, nhưng một lỗi logic mà trình biên dịch lẽ ra đã bắt được lại biến thành một sự cố lúc chạy. Hãy giữ các guard tồn tại ngắn ngủi và tránh giữ một phép mượn xuyên qua một lời gọi hàm có thể tái nhập cùng một `RefCell`. Tính khả biến nội tại cũng là nơi các chu trình tham chiếu ẩn nấp. Hai giá trị `Rc>` trỏ vào nhau giữ cho số đếm mạnh của nhau luôn trên không mãi mãi, nên không cái nào từng được giải phóng. Cách khắc phục là `Weak`, một handle không sở hữu và không ảnh hưởng tới số đếm mạnh; phần trình bày kinh điển nằm ở [Learn Rust With Entirely Too Many Linked Lists](https://rust-unofficial.github.io/too-many-lists/). ## `Arc`: đếm tham chiếu an toàn luồng cho tính đồng thời `Arc` (đếm tham chiếu nguyên tử) là người anh em đa luồng của `Rc`. API công khai gần như giống hệt, nhưng bộ đếm dùng các thao tác nguyên tử, nên việc clone và drop một `Arc` từ nhiều luồng cùng lúc vẫn đúng đắn. Chính tính nguyên tử đó là lý do `Arc` chỉ được chọn khi việc chia sẻ vượt qua ranh giới luồng: phép tăng nguyên tử chậm hơn một cách đo lường được so với phép tăng thường mà `Rc` dùng. Vì `Arc` vẫn trao ra các tham chiếu bất biến, việc đột biến qua các luồng cần một nguyên thủy đồng bộ hóa. `Mutex` là đối tác thường gặp, cho ra mẫu `Arc>` phản chiếu `Rc>` cho mã đồng thời. Ví dụ sau tạo ra bốn luồng, mỗi luồng tăng một tổng dùng chung một nghìn lần. ```rust // parallel_sum.rs 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 } ``` Trình biên dịch tự thực thi ranh giới này: `Rc` không phải là `Send`, nên việc cố di chuyển một cái vào `thread::spawn` sẽ thất bại khi biên dịch, đẩy hướng về `Arc`. Với tay lấy `Arc` bên trong một luồng duy nhất chỉ khiến phải trả cho các thao tác nguyên tử không bao giờ dùng tới. [Tài liệu `Arc`](https://doc.rust-lang.org/std/sync/struct.Arc.html) trình bày chi tiết các bảo đảm về thứ tự bộ nhớ, và `Arc>` là nền tảng cho phần lớn trạng thái dùng chung trong [Rust bất đồng bộ với Tokio](/blog/rust/rust-async-await-tokio-futures-concurrency). ## Box vs Rc vs Arc vs RefCell: khi nào dùng cái nào Bốn kiểu này kết hợp dọc theo hai trục: một giá trị có bao nhiêu chủ sở hữu, và liệu nó có thể bị đột biến qua một handle dùng chung hay không. Bảng dưới đây tóm tắt các đánh đổi. | Kiểu | Quyền sở hữu | Đột biến qua handle dùng chung | An toàn luồng | Chi phí | |------|-----------|----------------------------|-------------|----------| | `Box` | Duy nhất | Không | Có, nếu `T: Send` | Chỉ cấp phát heap | | `Rc` | Chung | Không | Không | Đếm không nguyên tử | | `Arc` | Chung | Không | Có | Đếm nguyên tử | | `RefCell` | Duy nhất | Có (kiểm tra lúc chạy) | Không | Cờ mượn lúc chạy | Cây quyết định thực dụng thì ngắn gọn. Cần một chủ sở hữu trên heap: `Box`. Cần nhiều chủ sở hữu trên một luồng: `Rc`. Cần nhiều chủ sở hữu qua nhiều luồng: `Arc`. Cần đột biến một giá trị dùng chung: bọc kiểu bên trong bằng `RefCell` (đơn luồng) hoặc `Mutex` (đa luồng). Hướng dẫn Effective Rust về [các kiểu tham chiếu và con trỏ](https://www.lurklurk.org/effective-rust/) đi tới cùng cách phân tầng. Hãy bắt đầu với lựa chọn ít quyền năng nhất và chỉ thêm khả năng khi trình biên dịch buộc phải làm vậy. ## Câu hỏi phỏng vấn về con trỏ thông minh Rust Con trỏ thông minh là một chủ đề phỏng vấn đáng tin cậy vì nó kiểm tra xem một ứng viên có hiểu về quyền sở hữu hay chỉ nắm cú pháp. Các câu hỏi dưới đây xuất hiện thường xuyên, và nhiều câu khác được tập hợp trong [mô-đun phỏng vấn con trỏ thông minh Rust](/technologies/rust/interview-questions/smart-pointers). **Sự khác biệt giữa `Rc` và `Arc` là gì?** Cả hai đều cung cấp quyền sở hữu chung thông qua đếm tham chiếu. `Rc` dùng một bộ đếm không nguyên tử và bị giới hạn trong một luồng; `Arc` dùng các thao tác nguyên tử và có thể được chia sẻ qua nhiều luồng với một chi phí runtime nhỏ. Trình biên dịch thực thi sự phân chia đó: `Rc` không phải `Send` cũng không phải `Sync`, nên nó không thể vượt qua ranh giới luồng. **Vì sao kết hợp `Rc` với `RefCell`?** `Rc` trao quyền sở hữu chung nhưng chỉ với quyền truy cập bất biến. `RefCell` bổ sung tính khả biến nội tại, cho phép các chủ sở hữu đột biến giá trị qua một tham chiếu dùng chung với kiểm tra mượn lúc chạy. Kết hợp lại, `Rc>` là khối xây dựng dùng-chung-khả-biến đơn luồng đúng thành ngữ. **Đếm tham chiếu có thể rò rỉ bộ nhớ ở Rust như thế nào?** Hai giá trị `Rc` (hoặc `Arc`) tham chiếu lẫn nhau tạo thành một chu trình mà số đếm mạnh không bao giờ chạm không, nên vùng cấp phát không bao giờ được giải phóng. Phá vỡ chu trình bằng `Weak`, vốn giữ một tham chiếu không sở hữu, khôi phục việc dọn dẹp đúng đắn. **Khi nào `Box` được ưa dùng hơn `Rc`?** Bất cứ khi nào một chủ sở hữu duy nhất là đủ. `Box` không có chi phí đếm tham chiếu, nên nó là lựa chọn mặc định cho cấp phát heap, kiểu đệ quy và trait object. Chỉ với tay lấy `Rc` khi thực sự cần quyền sở hữu chung đích thực. ## Kết luận Con trỏ thông minh trong Rust biến quyền sở hữu từ một ràng buộc thành một tập hợp các khối xây dựng có thể kết hợp. Việc chọn giữa chúng bám sát trực tiếp vào hình dạng của bài toán: - Dùng `Box` khi một giá trị cần một chủ sở hữu heap duy nhất, một kiểu đệ quy cần một kích thước cố định, hoặc một hàm trả về một trait object. - Dùng `Rc` cho nhiều chủ sở hữu trên một luồng, và chuyển sang `Arc` ngay khi quyền sở hữu vượt qua ranh giới luồng. - Thêm `RefCell` cho tính khả biến nội tại trong mã đơn luồng, và `Mutex` cho phiên bản đồng thời tương đương, luôn giữ các guard mượn và khóa tồn tại ngắn ngủi. - Kết hợp một cách có chủ đích: `Rc>` cho trạng thái khả biến dùng chung trên một luồng, `Arc>` qua nhiều luồng. - Cảnh giác với các chu trình tham chiếu giữa các con trỏ có đếm và phá vỡ chúng bằng `Weak` để tránh rò rỉ. - Mặc định chọn kiểu ít quyền năng nhất và để trình biên dịch cho biết khi nào cần thêm khả năng. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/rust/rust-smart-pointers-box-rc-arc-refcell