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.
Tham chiếu: mượn bằng &
Phần tiêu đề “Tham chiếu: mượn bằng &”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: 5Vẫn là của tôi: helloChuỗ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!!!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.
Các quy tắc mượn
Phần tiêu đề “Các quy tắc 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 immutableKhi nào một lần mượn kết thúc?
Phần tiêu đề “Khi nào một lần mượn kết thúc?”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àoxin chào, thế giớiQuy 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.
Vì sao điều này quan trọng
Phần tiêu đề “Vì sao điều này quan trọng”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.
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. 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.
1. Đếm mà không lấy
Phần tiêu đề “1. Đếm mà không lấy”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ề mainCá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.
2. Danh sách đi chợ
Phần tiêu đề “2. Danh sách đi chợ”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áoadd_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.
3. Hai người viết cùng lúc
Phần tiêu đề “3. Hai người viết cùng lú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 here4 | let second = &mut message; | ^^^^^^^^^^^^ second mutable borrow occurs here5 | first.push_str(" 10"); | ----- first borrow later used hereRustc 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ểmTrong 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.
Tóm tắt
Phần tiêu đề “Tóm tắt”- 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
Phần tiêu đề “Kiểm tra nhanh”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
Đọc thêm
Phần tiêu đề “Đọc thêm”- References and Borrowing — The Rust Book (tiếng Anh): chương chính thức, có cả phần tham chiếu treo (dangling reference) và vì sao Rust không cho tạo chúng.
- Borrowing — Rust by Example (tiếng Anh): ví dụ chạy được về mượn bằng
&, kèm các trang tiếp theo về mượn khả biến và aliasing. - Non-lexical lifetimes — The Edition Guide (tiếng Anh): thay đổi khiến lần mượn kết thúc ở lần dùng cuối thay vì ở dấu ngoặc đóng.
- The Slice Type — The Rust Book (tiếng Anh): mượn chỉ một phần của chuỗi hoặc mảng, bước tiếp theo tự nhiên sau tham chiếu.
reference— tài liệu thư viện chuẩn (tiếng Anh):&Tvà&mut Tlà gì, và chúng triển khai những trait nào.