Bỏ qua để đến nội dung

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.

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.

Toàn bộ hệ thống chỉ có vậy. Mọi thứ khác đều suy ra từ đây:

  1. Mỗi giá trị có đúng một chủ sở hữu (một biến “sở hữu” giá trị đó).
  2. Tại mỗi thời điểm chỉ có một chủ sở hữu.
  3. 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
}
▶ Tự chạy thử

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.

Đế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
}
▶ Tự chạy thử

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 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
}
▶ Tự chạy thử

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ệ.

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.

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.

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` trait
3 | let mut edited = original;
| -------- value moved here
4 | edited.push_str(" v2");
5 | println!("{original} -> {edited}");
| ^^^^^^^^ value borrowed here after move

Trì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 v2
▶ Tự chạy thử

let 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.

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! -- Ferris
▶ Tự chạy thử

Quyề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.

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` trait
4 | names.push(name);
| ---- value moved here
5 | println!("{name} joined the list");
| ^^^^ value borrowed here after move

Thô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"]
▶ Tự chạy thử

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.

  • 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

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