Ngày 27 của #100DaysOfRust: Giới thiệu về Smart Pointers trong Rust
Hôm nay, tôi đã bắt đầu khám phá smart pointers, một trong những chủ đề nâng cao nhưng thiết yếu trong Rust. Trong khi các con trỏ là một khái niệm phổ biến trong các ngôn ngữ lập trình, smart pointers trong Rust kết hợp ownership, borrowing và memory safety thành những công cụ mạnh mẽ.
🔹 Smart Pointers là gì?
Một con trỏ chỉ đơn giản là một biến chứa địa chỉ bộ nhớ của một số dữ liệu. Con trỏ quen thuộc nhất trong Rust là reference (&), cho phép mượn dữ liệu nhưng không sở hữu nó.
Smart pointers vượt xa điều này. Chúng là các cấu trúc dữ liệu hoạt động như các con trỏ nhưng đi kèm với metadata và khả năng bổ sung. Khác với references, smart pointers thường sở hữu dữ liệu mà chúng trỏ tới.
Một số ví dụ về smart pointers trong Rust:
StringvàVec<T>(đúng vậy, bạn đã sử dụng chúng rồi!)Box<T>– Cấp phát bộ nhớ trên heapRc<T>– Đếm tham chiếu (nhiều chủ sở hữu)RefCell<T>– Thực thi quy tắc mượn tại thời gian chạy
Hầu hết các smart pointers được triển khai bằng cách sử dụng structs và dựa trên hai traits quan trọng:
Deref– Giúp smart pointers hoạt động như các references.Drop– Tùy chỉnh hành vi khi giá trị ra khỏi phạm vi.
Trong bài viết này, tôi sẽ tập trung vào Box<T>, smart pointer đơn giản nhất.
📦 Sử dụng Box để trỏ tới dữ liệu trên Heap
Một box cho phép bạn lưu trữ dữ liệu trên heap thay vì trên stack. Trên stack, chỉ có một con trỏ nhỏ trỏ đến dữ liệu trên heap được lưu trữ.
Khi nào nên sử dụng Box:
- Khi kích thước của một loại không xác định tại thời điểm biên dịch.
- Khi chuyển nhượng quyền sở hữu của dữ liệu lớn mà không cần sao chép.
- Khi làm việc với trait objects (đa hình).
Ví dụ: Lưu trữ dữ liệu trên Heap
rust
fn main() {
let b = Box::new(5);
println!("b = {b}");
}
➡️ Điều này lưu trữ 5 trên heap, và box tự nó nằm trên stack.
Khi box ra khỏi phạm vi, cả con trỏ stack và bộ nhớ heap được dọn dẹp tự động.
🔁 Kích hoạt các loại đệ quy với Box
Một trong những cách sử dụng phổ biến nhất của Box<T> là để kích hoạt các loại đệ quy.
Hãy xem xét cấu trúc dữ liệu cổ điển cons list (một cấu trúc dữ liệu đệ quy từ Lisp).
Nỗ lực đầu tiên (Không biên dịch)
rust
enum List {
Cons(i32, List),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
➡️ Điều này không thành công vì Rust không thể xác định được lượng bộ nhớ mà loại đệ quy cần – nó có thể là vô hạn!
Sửa lỗi với Box
rust
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
➡️ Bằng cách bọc loại đệ quy trong Box, Rust biết kích thước (kích thước con trỏ cố định) và có thể biên dịch chương trình.
Tại sao điều này hoạt động
- Mỗi
Conschứa mộti32và một con trỏ (Box) tới phần tử tiếp theo. - Đệ quy kết thúc với
Nil. - Việc sử dụng bộ nhớ là có thể dự đoán vì Rust chỉ cần lưu trữ kích thước con trỏ cho box.
🧠 Những điểm chính
- Smart pointers sở hữu dữ liệu mà chúng trỏ tới, khác với references.
Box<T>di chuyển dữ liệu vào heap nhưng giữ kích thước con trỏ cố định trên stack.- Các loại đệ quy như linked lists yêu cầu
Box<T>để hoạt động. - Boxes là nhẹ: chúng cung cấp sự gián tiếp mà không có chi phí bổ sung.
- Traits
DerefvàDropgiúp smart pointers hoạt động một cách liền mạch.
Đây là bước đầu tiên của tôi vào smart pointers. Tiếp theo, tôi sẽ khám phá Deref, Drop và các smart pointers khác như Rc<T> và RefCell<T>, cùng với các mẫu nâng cao như interior mutability và reference cycles.
🚀 Hãy theo dõi tôi để cập nhật thêm về hành trình #100DaysOfRust của tôi!