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

Mượn (borrowing) trong Rust

Ở trang quyền sở hữu, bạn đã thấy một vấn đề: truyền một String vào hàm sẽ move nó, nên sau đó bạn không dùng được nữa. Cứ phải move giá trị qua lại (hoặc clone chúng) thì thật mệt. Mượn (borrowing) là lời giải của Rust.

Mượn nghĩa là cho phép một đoạn code dùng giá trị mà không lấy quyền sở hữu của nó, giống như cho bạn bè mượn một cuốn sách. Họ đọc xong thì trả lại, và sách vẫn là của bạn.

Bạn tạo một tham chiếu (reference) bằng cách đặt & trước một giá trị. Tham chiếu cho phép hàm xem dữ liệu mà không sở hữu nó:

fn print_length(s: &String) { // mượn s, không sở hữu nó
println!("Độ dài: {}", s.len());
}
fn main() {
let text = String::from("hello");
print_length(&text); // cho hàm mượn text
println!("Vẫn là của tôi: {text}"); // ✅ text vẫn hợp lệ ở đây
}

Kết quả:

Độ dài: 5
Vẫn là của tôi: hello
▶ Tự chạy thử

Chuỗi "hello" được giữ nguyên tiếng Anh vì len() đếm byte, mà chữ tiếng Việt có dấu chiếm nhiều byte hơn.

&text nghĩa là “cho hàm này mượn text”. Vì hàm chỉ mượn, quyền sở hữu vẫn thuộc về main, và bạn dùng tiếp text được. Hãy so sánh với trang quyền sở hữu, nơi cùng đoạn code đó nhưng thiếu & thì bị lỗi.

Tham chiếu khả biến: mượn để thay đổi

Phần tiêu đề “Tham chiếu khả biến: mượn để thay đổi”

Một tham chiếu & thông thường là chỉ đọc: bạn được nhìn nhưng không được động vào. Nếu hàm cần thay đổi giá trị đi mượn, hãy dùng tham chiếu khả biến (mutable reference) với &mut:

fn add_excitement(s: &mut String) {
s.push_str("!!!");
}
fn main() {
let mut text = String::from("xin chào");
add_excitement(&mut text); // cho mượn kiểu khả biến
println!("{text}"); // xin chào!!!
}

Kết quả:

xin chào!!!
▶ Tự chạy thử

Hãy để ý ba thứ phải khớp nhau: text được khai báo là mut, hàm yêu cầu &mut String, và lời gọi truyền vào &mut text. Cần đủ cả ba thì mới thay đổi được giá trị đi mượn.

Rust áp đặt hai quy tắc cho tham chiếu. Chúng tồn tại để ngăn data race (tranh chấp dữ liệu), cả một nhóm lỗi xảy ra khi hai đoạn code cùng động vào một dữ liệu tại cùng thời điểm và làm hỏng nó.

Tại mỗi thời điểm, bạn có thể có hoặc:

  • bao nhiêu tham chiếu chỉ đọc cũng được (&), hoặc
  • đúng một tham chiếu khả biến (&mut),

nhưng không bao giờ cả hai cùng lúc.

Nói đơn giản: nhiều người đọc hoặc một người viết, không bao giờ cả hai cùng lúc. Đoạn code này vi phạm quy tắc:

fn main() {
let mut text = String::from("hello");
let r1 = &text; // một lần mượn chỉ đọc
let r2 = &mut text; // ❌ không thể mượn khả biến khi r1 còn tồn tại
println!("{r1} {r2}");
}

Trình biên dịch chặn bạn lại, báo rằng không thể mượn text kiểu khả biến vì nó đang được mượn kiểu bất biến:

error[E0502]: cannot borrow `text` as mutable because it is also borrowed as immutable

Có thể bạn nghĩ một lần mượn kéo dài đến dấu } đóng phạm vi của nó. Không phải vậy. Lần mượn chỉ kéo dài đến chỗ cuối cùng tham chiếu được dùng. Sau đó, giá trị lại được tự do cho mượn tiếp, kể cả mượn khả biến.

Nghĩa là ví dụ hỏng ở trên có thể sửa chỉ bằng cách dùng xong r1 trước khi tạo r2:

fn main() {
let mut text = String::from("xin chào");
let r1 = &text;
println!("Đang đọc: {r1}"); // lần dùng cuối của r1, nên lần mượn kết thúc ở đây
let r2 = &mut text; // ✅ ổn: không còn ai đang đọc
r2.push_str(", thế giới");
println!("{text}");
}

Kết quả:

Đang đọc: xin chào
xin chào, thế giới
▶ Tự chạy thử

Quy tắc vẫn là “nhiều người đọc hoặc một người viết”. Chỉ là nó được kiểm tra trên đoạn code mà mỗi tham chiếu thực sự được dùng. Người đọc r1 đã xong việc khi người viết r2 xuất hiện, và r2 xong việc trước khi println! cuối cùng đọc lại text. Không có gì chồng lên nhau, nên Rust hài lòng.

Đây là cách phổ biến nhất để sửa lỗi mượn: đổi thứ tự. Đọc hết những gì cần đọc, rồi mới viết (hoặc ngược lại), thay vì trộn lẫn hai việc.

Các quy tắc này được kiểm tra hoàn toàn lúc biên dịch, nên không tốn chút chi phí nào khi chương trình chạy. Nhờ vậy Rust có thể hứa “không có data race” mà vẫn nhanh như C: sự an toàn đã được chứng minh trước khi chương trình khởi động.

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. Nếu trình biên dịch phàn nàn, hãy đọc lỗi: nó thường chỉ ra chính xác chỗ các lần mượn chồng lên nhau.

Hãy viết hàm total_letters mượn một Vec<String> và trả về tổng số chữ cái của tất cả các từ. Sau khi gọi hàm, main vẫn phải in được vector.

Gợi ý: kiểu của tham số là &Vec<String>, và bạn gọi nó với &words. Duyệt vector đi mượn bằng for word in list rồi cộng dồn word.len().

Xem lời giải
fn total_letters(list: &Vec<String>) -> usize {
let mut total = 0;
for word in list {
total += word.len();
}
total
}
fn main() {
let words = vec![String::from("borrow"), String::from("check")];
let count = total_letters(&words); // cho mượn vector
println!("{count} chữ cái");
println!("{:?} vẫn thuộc về main", words);
}

Kết quả:

11 chữ cái
["borrow", "check"] vẫn thuộc về main
▶ Tự chạy thử

Các từ được giữ tiếng Anh vì len() đếm byte: với chữ có dấu, kết quả sẽ không còn bằng số chữ cái.

Hàm chỉ cần nhìn các từ, nên một lần mượn chỉ đọc & là đủ. Nếu bạn viết list: Vec<String>, vector sẽ bị move vào hàm và println! cuối cùng sẽ không biên dịch được.

Hãy viết hai hàm: add_item thêm một món vào danh sách đi chợ, và print_list in ra từng món. Thêm ba món rồi in danh sách.

Gợi ý: một hàm thay đổi danh sách, một hàm chỉ đọc. Hàm nào cần &mut Vec<String>, hàm nào chỉ cần &Vec<String>? Để biến &str thành String, dùng String::from(item).

Xem lời giải
fn add_item(list: &mut Vec<String>, item: &str) {
list.push(String::from(item));
}
fn print_list(list: &Vec<String>) {
for item in list {
println!("- {item}");
}
}
fn main() {
let mut list = Vec::new();
add_item(&mut list, "sữa");
add_item(&mut list, "bánh mì");
add_item(&mut list, "táo");
print_list(&list);
}

Kết quả:

- sữa
- bánh mì
- táo
▶ Tự chạy thử

add_item thay đổi danh sách, nên nó nhận một lần mượn &mut, và list trong main phải là mut. print_list chỉ đọc, nên & thường là đủ. Mỗi lần mượn &mut list kết thúc khi lời gọi trả về, vì thế lời gọi tiếp theo lại mượn được.

Chương trình này không biên dịch được. Hãy sửa để nó in ra Tổng: 10 điểm, giữ nguyên cả first lẫn second.

fn main() {
let mut message = String::from("Tổng:");
let first = &mut message;
let second = &mut message;
first.push_str(" 10");
second.push_str(" điểm");
println!("{message}");
}
error[E0499]: cannot borrow `message` as mutable more than once at a time
--> src/main.rs:4:18
|
3 | let first = &mut message;
| ------------ first mutable borrow occurs here
4 | let second = &mut message;
| ^^^^^^^^^^^^ second mutable borrow occurs here
5 | first.push_str(" 10");
| ----- first borrow later used here

Rustc báo rằng message không thể bị mượn khả biến hai lần cùng lúc: lần mượn đầu vẫn còn được dùng ở dòng 5.

Gợi ý: mỗi lúc chỉ một người viết. Xem lại phần Khi nào một lần mượn kết thúc? ở trên: bạn có thể dùng xong first trước khi tạo second không?

Xem lời giải
fn main() {
let mut message = String::from("Tổng:");
let first = &mut message;
first.push_str(" 10"); // lần dùng cuối của first: lần mượn kết thúc ở đây
let second = &mut message;
second.push_str(" điểm");
println!("{message}");
}

Kết quả:

Tổng: 10 điểm
▶ Tự chạy thử

Trong bản gốc, first vẫn còn được dùng (ở dòng 5) khi second được tạo ở dòng 4. Đó là hai người viết cùng lúc. Đưa first.push_str lên trên nghĩa là first đã xong việc trước khi second tồn tại, nên lúc nào cũng chỉ có một &mut.

  • Mượn (&) cho phép code dùng một giá trị mà không lấy quyền sở hữu: bạn sẽ nhận lại nó.
  • Tham chiếu & thông thường là chỉ đọc; dùng &mut để thay đổi giá trị đi mượn.
  • Để thay đổi qua tham chiếu, biến, tham số và lời gọi đều cần mut/&mut.
  • Quy tắc: nhiều người đọc hoặc một người viết, không bao giờ cả hai cùng lúc.
  • Một lần mượn kết thúc ở lần dùng cuối của tham chiếu, nên sắp xếp lại code thường sửa được lỗi mượn.
  • Tất cả được kiểm tra lúc biên dịch, nên không tốn gì lúc chạy.

Kiểm tra nhanh

1. Đặt & trước một giá trị khi truyền vào hàm có tác dụng gì?

2. Theo các quy tắc mượn, tổ hợp nào được phép tồn tại cùng lúc?

3. Bạn cần gì để thay đổi một giá trị thông qua tham chiếu?

4. Vì sao Rust áp đặt được các quy tắc mượn mà không tốn hiệu năng lúc chạy?

Điểm: 0 / 4