Tại sao Rust không cần Garbage Collector nhưng vẫn an toàn bộ nhớ?
Nếu bạn hỏi bất kỳ lập trình viên Rust nào rằng điều gì làm Rust khác biệt nhất, câu trả lời gần như luôn là: Ownership. Đây là cơ chế cốt lõi giúp Rust loại bỏ toàn bộ lỗi liên quan đến bộ nhớ mà không cần bộ thu gom rác.
1. Ownership là nền tảng
Ở các ngôn ngữ như C hoặc C++, lập trình viên phải tự cấp phát và tự giải phóng bộ nhớ. Bỏ sót thì rò rỉ, giải phóng thừa thì crash. Ở các ngôn ngữ có Garbage Collector như Java hoặc Go, lập trình viên không cần lo free, nhưng đổi lại phải chấp nhận sự can thiệp của bộ thu gom rác và chi phí thời gian chạy.
Rust chọn một hướng khác: không dùng GC, không dùng runtime riêng, mà để trình biên dịch đảm bảo an toàn thông qua quy tắc Ownership.
Ba quy tắc của ownership tuy đơn giản nhưng đủ để loại bỏ phần lớn lỗi bộ nhớ:
- Mỗi giá trị có một owner.
- Tại một thời điểm chỉ có một owner.
- Khi owner ra khỏi scope, giá trị sẽ được drop tự động.
Điều này có nghĩa là bộ nhớ được quản lý hoàn toàn ở compile time, Rust xác định chính xác nơi cấp phát và nơi giải phóng, và không cho phép bạn viết code có nguy cơ gây lỗi. Vì vậy, Ownership là nền tảng của toàn bộ hệ thống an toàn bộ nhớ của Rust.
Một ví dụ rất nhỏ thể hiện triết lý này:
{
let s = String::from("hello");
} // s bị drop tại đây
Bạn không cần viết free, cũng không bao giờ có cơ hội quên viết free.
2. Move semantics
Một điều khiến người mới học Rust bất ngờ là việc gán biến không tạo bản sao:
let a = String::from("hello");
let b = a;
println!("{}", a); // lỗi
Ở đây Rust đã chuyển quyền sở hữu từ a sang b. Cách làm này giảm sao chép dữ liệu không cần thiết và tránh tình huống hai biến cùng sở hữu một vùng nhớ.
Move semantics nghe có vẻ khắt khe, nhưng nó mang lại hai lợi ích quan trọng:
- Giảm chi phí copy giúp Rust nhanh hơn.
- Tránh tình trạng double free và use after free.
Nếu khi lập trình bạn thực sự cần bản sao độc lập, bạn phải dùng .clone():
let a = String::from("hello");
let b = a.clone(); // b là bản sao của a
Rust không cấm sao chép, chỉ yêu cầu bạn chủ động chỉ rõ khi sao chép.
3. Borrowing
Ownership giúp Rust quản lý bộ nhớ, nhưng nếu chỉ có ownership thì mọi hàm đều phải lấy quyền sở hữu dữ liệu để xử lý, dẫn đến code rất cứng nhắc. Vì vậy Rust cho phép bạn mượn dữ liệu tạm thời bằng reference.
Borrowing có hai dạng:
Borrow bất biến
let s = String::from("hello");
let r = &s; // mượn bất biến
Bạn có thể tạo nhiều reference bất biến. Điều này phù hợp khi bạn chỉ đọc dữ liệu.
Borrow thay đổi
let mut s = String::from("hello");
let r = &mut s; // mượn thay đổi
r.push_str(" world");
Một nguyên tắc quan trọng được Rust đảm bảo:
- chỉ có một mutable reference tại một thời điểm
- hoặc nhiều immutable reference
- nhưng không tồn tại cả hai cùng lúc
Từ quy tắc này, Rust loại bỏ hoàn toàn data race ngay ở compile time. Bạn không cần chạy chương trình để phát hiện lỗi đa luồng; Rust đảm bảo bạn không thể tạo lỗi ngay từ bước biên dịch.
Borrowing vì thế trở thành công cụ giúp bạn vừa linh hoạt trong việc chia sẻ dữ liệu, vừa giữ tính an toàn tuyệt đối.
4. Lifetime
Lifetime thường bị xem là phần “khó” của Rust, nhưng bản chất của nó chỉ nhằm giải quyết một câu hỏi duy nhất:
Reference phải sống ít nhất bằng dữ liệu mà nó trỏ đến.
Ví dụ sau sẽ gây lỗi:
fn dangle() -> &String {
let s = String::from("hello");
&s
} // s bị drop tại đây
Reference trả về sẽ trỏ vào vùng nhớ không còn tồn tại. Rust phát hiện điều này ở compile time bằng cách kiểm tra lifetime.
Khi bạn chú thích lifetime, bạn đang giúp Rust hiểu mối quan hệ thời gian sống giữa các reference:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Lifetime không thay đổi cách chương trình chạy. Nó chỉ là thông tin để compiler kiểm tra tính hợp lệ của reference. Và tin vui là phần lớn thời gian Rust tự suy luận lifetime, bạn chỉ cần ghi rõ khi viết các hàm phức tạp hoặc struct chứa reference.
5. Tổng kết
Khi nhìn tổng thể, bạn sẽ thấy ba cơ chế này liên kết rất chặt chẽ:
- Ownership quản lý tài nguyên một cách rõ ràng và hiệu quả
- Move semantics giảm chi phí copy và ngăn lỗi double free
- Borrowing cho phép chia sẻ dữ liệu mà không mất an toàn
- Lifetime đảm bảo reference không trỏ vào vùng nhớ đã bị giải phóng
Điều quan trọng nhất là tất cả đều xảy ra ở compile time.
Rust bắt lỗi trước khi chương trình chạy, giúp bạn tránh:
- null pointer
- use after free
- data race
- double free
- memory leak
Mỗi cơ chế đều góp một phần vào mục tiêu tổng thể: hiệu năng như C, an toàn như Java, nhưng không cần Garbage Collector.
Phần 1: https://ant.ncc.plus/?p=20388
Reference:
– http://w3schools.com/rust/
