Quyền sở hữu trong Rust
Quyền sở hữu (ownership) là ý tưởng làm nên chất Rust của Rust. Nhờ nó, ngôn ngữ đảm bảo an toàn bộ nhớ mà không cần bộ thu gom rác (garbage collector) và cũng không bắt bạn tự giải phóng bộ nhớ. Lúc đầu nghe có vẻ lạ, nhưng ý tưởng cốt lõi rất đơn giản. Chúng ta sẽ đi từng bước một.
Vấn đề mà quyền sở hữu giải quyết
Phần tiêu đề “Vấn đề mà quyền sở hữu giải quyết”Khi chạy, chương trình lưu dữ liệu trong bộ nhớ. Đến một lúc nào đó, phần bộ nhớ ấy phải được dọn dẹp, nếu không chương trình sẽ rò rỉ bộ nhớ và cuối cùng bị sập. Các ngôn ngữ thường xử lý chuyện này theo hai cách:
- Thủ công (C/C++): bạn tự giải phóng bộ nhớ. Quên giải phóng thì rò rỉ. Giải phóng quá sớm thì gặp lỗi sập chương trình và lỗ hổng bảo mật.
- Bộ thu gom rác (Java, Go, Python): một tiến trình chạy ngầm dọn dẹp giúp bạn. Tiện lợi, nhưng phải trả giá bằng hiệu năng và tính dễ đoán.
Rust chọn con đường thứ ba: nó quyết định ngay lúc biên dịch chính xác khi nào mỗi giá trị được dọn dẹp, dựa trên một bộ quy tắc về quyền sở hữu. Không cần tự giải phóng, không cần bộ thu gom rác.
Ba quy tắc của quyền sở hữu
Phần tiêu đề “Ba quy tắc của quyền sở hữu”Toàn bộ hệ thống chỉ có vậy. Mọi thứ khác đều suy ra từ đây:
- Mỗi giá trị có đúng một chủ sở hữu (một biến “sở hữu” giá trị đó).
- Tại mỗi thời điểm chỉ có một chủ sở hữu.
- Khi chủ sở hữu ra khỏi phạm vi (scope), giá trị được tự động dọn dẹp.
Hãy xem từng quy tắc hoạt động thế nào.
Quy tắc 3: giá trị bị drop ở cuối phạm vi
Phần tiêu đề “Quy tắc 3: giá trị bị drop ở cuối phạm vi”Phạm vi (scope) là vùng nằm giữa { và }. Khi phạm vi của một biến kết thúc, Rust tự động giải phóng giá trị của nó, bạn không bao giờ phải viết free():
fn main() { { let message = String::from("xin chào"); // message hợp lệ ở đây println!("{message}"); } // <- hết phạm vi, `message` được tự động dọn dẹp
// println!("{message}"); // ❌ lỗi: message không còn tồn tại ở đây}Bạn không phải giải phóng gì cả: Rust đã làm việc đó ngay khi message ra khỏi phạm vi.
Quy tắc 1 và 2: “move”
Phần tiêu đề “Quy tắc 1 và 2: “move””Đến đây mới thú vị. Hãy xem chuyện gì xảy ra khi bạn gán một biến cho biến khác:
fn main() { let s1 = String::from("xin chào"); let s2 = s1; // quyền sở hữu MOVE (di chuyển) từ s1 sang s2
println!("{s2}"); // ✅ chạy được // println!("{s1}"); // ❌ lỗi: s1 đã bị move}Khi bạn viết let s2 = s1, Rust không sao chép chuỗi mà move (di chuyển) quyền sở hữu từ s1 sang s2. Vì quy tắc 2 nói chỉ có một chủ sở hữu, s1 giờ không còn hợp lệ. Nếu cố dùng nó, bạn sẽ nhận được lỗi sau (trình biên dịch báo rằng bạn đang mượn một giá trị đã bị move):
error[E0382]: borrow of moved value: `s1`Nếu tôi muốn có hai bản sao thì sao?
Phần tiêu đề “Nếu tôi muốn có hai bản sao thì sao?”Nếu bạn thật sự cần một bản sao thứ hai, độc lập, hãy gọi .clone():
fn main() { let s1 = String::from("xin chào"); let s2 = s1.clone(); // tạo một bản sao đầy đủ
println!("s1 = {s1}, s2 = {s2}"); // ✅ giờ cả hai đều dùng được}clone() được viết tường minh là có chủ ý: sao chép có thể tốn kém, nên Rust bắt bạn phải yêu cầu rõ ràng.
Kiểu đơn giản được sao chép, không bị move
Phần tiêu đề “Kiểu đơn giản được sao chép, không bị move”Có thể bạn thắc mắc vì sao đoạn này lại không gây lỗi:
fn main() { let x = 5; let y = x; println!("x = {x}, y = {y}"); // ✅ cả hai đều dùng được}Những giá trị nhỏ, kích thước cố định như số nguyên, số thực, boolean và char sao chép rất rẻ, nên Rust chỉ đơn giản sao chép chúng thay vì move. Với người mới, bạn có thể nhớ quy tắc nhanh sau:
- Giá trị đơn giản (số,
bool,char) → được sao chép. Biến gốc vẫn hợp lệ. - Dữ liệu có chủ sở hữu (như
String) → bị move. Biến gốc không còn hợp lệ.
Quyền sở hữu và hàm
Phần tiêu đề “Quyền sở hữu và hàm”Truyền một giá trị vào hàm cũng move nó, giống hệt như khi gán cho một biến:
fn take_ownership(s: String) { println!("Giờ tôi sở hữu: {s}");} // s bị drop ở đây
fn main() { let text = String::from("xin chào"); take_ownership(text); // println!("{text}"); // ❌ lỗi: text đã bị move vào hàm}Đây chính là lý do chủ đề tiếp theo, mượn (borrowing), ra đời. Mượn cho phép một hàm dùng giá trị mà không lấy quyền sở hữu, nhờ vậy bạn vẫn dùng tiếp được giá trị đó sau khi gọi hàm.
Luyện tập
Phần tiêu đề “Luyện tập”Hãy tự làm từng bài trước, trên Rust Playground hoặc trong một dự án trên máy. Gặp lỗi biên dịch trong lúc làm là chuyện bình thường: hãy đọc kỹ, chúng thường chỉ thẳng vào chỗ bị move.
1. Giữ lại bản gốc
Phần tiêu đề “1. Giữ lại bản gốc”Chương trình này không biên dịch được. Hãy sửa để nó in ra draft -> draft v2, mà không xoá biến nào.
fn main() { let original = String::from("draft"); let mut edited = original; edited.push_str(" v2"); println!("{original} -> {edited}");}error[E0382]: borrow of moved value: `original` --> src/main.rs:5:16 |2 | let original = String::from("draft"); | -------- move occurs because `original` has type `String`, which does not implement the `Copy` trait3 | let mut edited = original; | -------- value moved here4 | edited.push_str(" v2");5 | println!("{original} -> {edited}"); | ^^^^^^^^ value borrowed here after moveTrình biên dịch cho biết original đã bị move sang edited ở dòng 3, nên dòng 5 không dùng được nó nữa.
Gợi ý: bạn cần hai chuỗi độc lập, để sửa chuỗi này không ảnh hưởng đến chuỗi kia.
Xem lời giải
fn main() { let original = String::from("draft"); let mut edited = original.clone(); // một bản sao đầy đủ, riêng biệt edited.push_str(" v2"); println!("{original} -> {edited}");}Kết quả:
draft -> draft v2let mut edited = original; move chuỗi đi, nên original không còn hợp lệ. .clone() tạo ra một chuỗi thứ hai với vùng nhớ riêng. Giờ push_str chỉ thay đổi edited, còn original vẫn là draft.
2. Trả lại cho người gọi
Phần tiêu đề “2. Trả lại cho người gọi”Hãy viết hàm add_signature lấy quyền sở hữu một String, thêm " -- Ferris" vào cuối rồi trả chuỗi lại. Gọi hàm từ main và in kết quả.
Gợi ý: một hàm có thể trả về chính giá trị nó nhận vào. Viết -> String trong chữ ký hàm, và nhận tham số dưới dạng mut text: String để được phép thay đổi nó.
Xem lời giải
fn add_signature(mut text: String) -> String { text.push_str(" -- Ferris"); text // quyền sở hữu move ngược ra cho người gọi}
fn main() { let letter = String::from("Hẹn sớm gặp lại!"); let letter = add_signature(letter); println!("{letter}");}Kết quả:
Hẹn sớm gặp lại! -- FerrisQuyền sở hữu move vào hàm khi gọi, và move ra lại cùng giá trị trả về. Chúng ta hứng nó bằng một biến letter mới (che khuất biến cũ). Cách này chạy được, nhưng cứ truyền giá trị vào rồi lại lấy ra mãi thì khá mệt. Đó chính là vấn đề mà mượn giải quyết.
3. Ai đang sở hữu nó?
Phần tiêu đề “3. Ai đang sở hữu nó?”Chương trình này không biên dịch được. Hãy sửa để nó in ra Ana joined the list rồi đến ["Ana"].
fn main() { let mut names = Vec::new(); let name = String::from("Ana"); names.push(name); println!("{name} joined the list"); println!("{:?}", names);}error[E0382]: borrow of moved value: `name` --> src/main.rs:5:16 |3 | let name = String::from("Ana"); | ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait4 | names.push(name); | ---- value moved here5 | println!("{name} joined the list"); | ^^^^ value borrowed here after moveThông báo lỗi giữ nguyên tiếng Anh như rustc in ra; vì dòng 5 xuất hiện trong đó nên chuỗi được in cũng để tiếng Anh.
Gợi ý: push lấy quyền sở hữu, giống hệt một lời gọi hàm. Sau dòng 4, ai sở hữu chuỗi? Bạn có thể dùng name trước khi nó bị move, hoặc hỏi chủ sở hữu mới.
Xem lời giải
fn main() { let mut names = Vec::new(); let name = String::from("Ana"); println!("{name} joined the list"); // dùng nó khi ta còn sở hữu names.push(name); // giờ vector sở hữu nó println!("{:?}", names);}Kết quả:
Ana joined the list["Ana"]Sau names.push(name), vector là chủ sở hữu và name không dùng được nữa. In trước là một cách sửa. Một cách khác là giữ nguyên thứ tự và in names[0], vì vector vẫn đang giữ chuỗi. Dùng clone cũng được, nhưng ở đây không cần sao chép.
Tóm tắt
Phần tiêu đề “Tóm tắt”- Quyền sở hữu giúp Rust quản lý bộ nhớ an toàn mà không cần bộ thu gom rác.
- Ba quy tắc: mỗi giá trị có một chủ sở hữu, mỗi lúc chỉ một chủ sở hữu, giá trị bị drop khi chủ sở hữu ra khỏi phạm vi.
- Gán dữ liệu có chủ sở hữu (như
String) sẽ move nó: biến gốc không còn hợp lệ. - Dùng
.clone()để có một bản sao độc lập; các kiểu đơn giản (số,bool,char) được sao chép tự động. - Truyền giá trị vào hàm cũng move nó, và điều đó dẫn chúng ta đến mượn (borrowing).
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Trong Rust, một giá trị có thể có bao nhiêu chủ sở hữu cùng lúc?
2. Bạn gọi phương thức nào để tạo một bản sao độc lập của String thay vì move nó?
3. Kiểu nào sau đây được tự động sao chép thay vì bị move khi gán cho biến khác?
4. Chuyện gì xảy ra khi bạn truyền một String vào hàm nhận nó theo giá trị (không phải tham chiếu)?
5. Theo trang này, khi nào Rust tự động dọn dẹp (drop) một giá trị?
Điểm: 0 / 5
Đọc thêm
Phần tiêu đề “Đọc thêm”- What Is Ownership? — The Rust Book (tiếng Anh): chương chính thức, có hình minh hoạ một
Stringtrông thế nào trong bộ nhớ và vì sao move không sao chép nó. - Ownership and moves — Rust by Example (tiếng Anh): các ví dụ ngắn, chạy được về move, kể cả move vào hàm.
- RAII — Rust by Example (tiếng Anh): giá trị được dọn dẹp thế nào khi ra khỏi phạm vi, và trait
Dropliên quan ra sao. Copy— tài liệu thư viện chuẩn (tiếng Anh): kiểu nào được sao chép thay vì move, và khác biệt giữaCopyvớiClone.- Rust track — Exercism (tiếng Anh): bài tập miễn phí có phản hồi, khi bạn muốn luyện nhiều hơn những gì trang này có.