blog.dopana

Back

Bài viết thứ 13 trong series Rust trung cấp. Trong khi các tham chiếu thông thường (&T) chỉ là những “cái nhìn mượn” vào bộ nhớ, Smart Pointers (Con trỏ thông minh) là những cấu trúc dữ liệu sở hữu dữ liệu mà chúng trỏ tới. Chúng hoạt động như tham chiếu nhưng mang theo siêu dữ liệu và khả năng bổ sung — phân bổ heap, sở hữu chia sẻ và khả năng thay đổi nội tại.

graph TD
    A["Smart Pointers"] --> B["Box<T><br/>Phân bổ heap<br/>Một chủ sở hữu"]
    A --> C["Rc<T><br/>Đếm tham chiếu<br/>Chia sẻ đơn luồng"]
    A --> D["RefCell<T><br/>Khả năng thay đổi nội tại<br/>Kiểm tra mượn thời gian chạy"]
    A --> E["Arc<T><br/>Đếm tham chiếu nguyên tử<br/>Chia sẻ an toàn đa luồng"]
    A --> F["Deref Trait<br/>Tùy chỉnh toán tử *"]

Điều Gì Làm Cho Một Con Trỏ Trở Nên “Thông Minh”?#

Một tham chiếu thông thường (&T) chỉ là một địa chỉ — nó mượn dữ liệu do người khác sở hữu. Một smart pointer, ngược lại, sở hữu dữ liệu mà nó trỏ tới. Khi smart pointer ra khỏi phạm vi, dữ liệu sẽ tự động được giải phóng và bộ nhớ được giải phóng.

Thành phần “phép màu” ở đây là trait Deref. Nó cho phép smart pointers hoạt động như các tham chiếu thông thường bằng cách tùy chỉnh hành vi khi bạn sử dụng toán tử *.

use std::ops::Deref;

struct MyBox<T>(T);

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
rust

Với Deref được cài đặt, bạn có thể sử dụng *my_box giống như *reference, và Rust sẽ tự động gọi deref() phía sau hậu trường.

Box: Đặt Dữ Liệu Lên Heap#

Box<T> là smart pointer đơn giản nhất. Nó phân bổ dữ liệu của mình trên heap và cung cấp một chủ sở hữu duy nhất. Khi Box bị giải phóng, bộ nhớ heap sẽ được giải phóng.

Tại Sao Nên Dùng Box?#

  1. Kiểu đệ quy: Các kiểu chứa chính chúng (như danh sách liên kết hoặc nút cây) cần sự gián tiếp để có kích thước xác định.
  2. Truyền dữ liệu lớn: Di chuyển dữ liệu lớn lên heap tránh các bản sao stack tốn kém.
  3. Đối tượng trait: Lưu trữ các giá trị của nhiều kiểu khác nhau sau một giao diện chung.

Kiểu Đệ Quy Với Box#

Nếu không có Box, một struct đệ quy như danh sách liên kết sẽ có kích thước vô hạn:

graph LR
    A["Cons(1, Box)"] --> B["Cons(2, Box)"]
    B --> C["Cons(3, Box)"]
    C --> D["Nil"]

Box Cho Đối Tượng Trait#

Box<dyn Trait> cho phép lưu trữ các kiểu khác nhau sau một giao diện chung:

Rc: Sở Hữu Chia Sẻ (Đơn Luồng)#

Đôi khi bạn cần nhiều chủ sở hữu cho cùng một dữ liệu. Rc<T> (Reference Counted - Đếm tham chiếu) cho phép điều này trong các kịch bản đơn luồng.

Cơ Chế Hoạt Động#

Mỗi lần bạn gọi Rc::clone(), số lượng tham chiếu tăng lên. Khi một bản sao bị giải phóng, số lượng giảm đi. Dữ liệu chỉ được giải phóng khi số lượng đạt 0.

use std::rc::Rc;

fn main() {
    let data = Rc::new(String::from("Chào, thế giới chia sẻ!"));
    println!("Số lượng tham chiếu: {}", Rc::strong_count(&data)); // 1

    {
        let data2 = Rc::clone(&data); // số lượng tăng lên
        println!("Số lượng tham chiếu: {}", Rc::strong_count(&data)); // 2
        println!("data2: {data2}");
    } // data2 bị giải phóng, số lượng giảm lại

    println!("Số lượng tham chiếu: {}", Rc::strong_count(&data)); // 1
    println!("data: {data}");
}
rust

Cấu Trúc Cây Với Rc#

Rc<T> rất phù hợp cho cấu trúc cây nơi các nút con cần tham chiếu đến nút cha:

[!NOTE] Rc<T> sử dụng đếm tham chiếu không nguyên tử, nhanh hơn nhưng không an toàn đa luồng. Đối với mã đa luồng, hãy sử dụng Arc<T> thay thế.

RefCell: Khả Năng Thay Đổi Nội Tại#

RefCell<T> cho phép khả năng thay đổi nội tại — khả năng thay đổi dữ liệu ngay cả khi bạn chỉ có một tham chiếu bất biến đến chính RefCell đó.

Bộ Kiểm Tra Mượn Tại Thời Gian Chạy#

Trong khi Rc<T> theo dõi số lượng chủ sở hữu, RefCell<T> theo dõi số lượng tham chiếu tại thời gian chạy. Các quy tắc giống hệt với việc mượn tại thời gian biên dịch, nhưng vi phạm sẽ gây lỗi thời gian chạy (panic) thay vì lỗi biên dịch.

use std::cell::RefCell;

fn main() {
    let cell = RefCell::new(5);

    {
        let mut borrow = cell.borrow_mut(); // mượn thay đổi
        *borrow += 1;
        println!("Trong khối: {borrow}");
    } // borrow bị giải phóng ở đây

    println!("Sau khối: {}", cell.borrow()); // mượn bất biến
}
rust

Kết Hợp Rc + RefCell#

Kết hợp Rc<T>RefCell<T> cho phép sở hữu chia sẻ kèm khả năng thay đổi:

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let data = Rc::new(RefCell::new(vec![1, 2, 3]));

    let data2 = Rc::clone(&data);
    data2.borrow_mut().push(4);

    println!("data: {:?}", data.borrow()); // [1, 2, 3, 4]
}
rust

[!WARNING] RefCell<T> sẽ gây panic thời gian chạy nếu bạn vi phạm quy tắc mượn (ví dụ: hai lần mượn thay đổi đồng thời). Điều này đổi lấy an toàn thời gian biên dịch bằng tính linh hoạt — hãy sử dụng một cách thận trọng.

Arc: Sở Hữu Chia Sẻ An Toàn Đa Luồng#

Arc<T> (Atomically Reference Counted - Đếm tham chiếu nguyên tử) là phiên bản đa luồng của Rc<T>. Nó sử dụng các phép toán nguyên tử để chia sẻ dữ liệu an toàn giữa các luồng.

graph TD
    A["Arc<Vec>"] --> B["Thread 1<br/>Arc::clone"]
    A --> C["Thread 2<br/>Arc::clone"]
    A --> D["Thread 3<br/>Arc::clone"]
    A --> E["Thread 4<br/>Arc::clone"]

[!TIP] Arc<T> có một khoảng phí nhỏ do các phép toán nguyên tử. Nếu bạn không cần an toàn đa luồng, Rc<T> sẽ nhanh hơn.

Khi Nào Nên Dùng Smart Pointer Nào?#

Smart PointerTrường Hợp Sử DụngAn Toàn Đa Luồng?
Box<T>Phân bổ heap, kiểu đệ quy, đối tượng trait✅ Có (một chủ sở hữu)
Rc<T>Nhiều chủ sở hữu, đơn luồng❌ Không
RefCell<T>Khả năng thay đổi nội tại, kiểm tra mượn thời gian chạy❌ Không
Arc<T>Nhiều chủ sở hữu, đa luồng✅ Có
Rc<RefCell<T>>Chia sẻ + thay đổi, đơn luồng❌ Không
Arc<Mutex<T>>Chia sẻ + thay đổi, đa luồng✅ Có

Ví Dụ Trực Quan (ELI5): Hệ Thống Sách Thư Viện#

Hãy tưởng tượng smart pointers như những cách quản lý sách thư viện khác nhau:

  • Box<T> giống như việc mượn một cuốn sách — bạn là người sở hữu duy nhất. Khi trả sách (drop), thư viện sẽ thu hồi lại.
  • Rc<T> giống như một cuốn sách mà nhiều nhóm học có thể cùng mượn. Thư viện theo dõi có bao nhiêu nhóm đang mượn. Chỉ khi nhóm cuối cùng trả sách, thư viện mới thu hồi.
  • RefCell<T> giống như một cuốn sách có bìa “chỉ đọc”. Bạn có thể nhìn nhưng để viết chú thích, bạn cần sự cho phép đặc biệt từ thủ thư (bộ kiểm tra thời gian chạy) — và chỉ có một người được viết vào lúc một.
  • Arc<T> giống như Rc<T> nhưng cho hệ thống thư viện trường đại học nhiều chi nhánh, nơi nhiều cơ sở cùng chia sẻ một cuốn sách. Bộ đếm nguyên tử đảm bảo không chi nhánh nào mất track cuốn sách.

Tổng Kết#

  • Smart Pointers sở hữu dữ liệu và cài đặt Deref để hoạt động như tham chiếu.
  • Box<T> cung cấp phân bổ heap và là thiết yếu cho kiểu đệ quy và đối tượng trait.
  • Rc<T> cho phép sở hữu nhiều người trong mã đơn luồng thông qua đếm tham chiếu.
  • RefCell<T> cung cấp khả năng thay đổi nội tại với kiểm tra mượn thời gian chạy.
  • Arc<T> là phiên bản an toàn đa luồng của Rc<T> cho mã đồng thời.
  • Kết hợp các smart pointer này (ví dụ: Rc<RefCell<T>> hoặc Arc<Mutex<T>>) mang lại những mẫu thiết kế mạnh mẽ cho trạng thái chia sẻ và thay đổi.

Trong bài viết tiếp theo, chúng ta sẽ khám phá Functional Rust — closures và iterators — và cách các abstractions không chi phí thời gian chạy của Rust cho phép bạn viết mã trừu tượng, có biểu đạt cao mà không hy sinh hiệu suất.

Tài liệu tham khảo#