Lỗi Rust E0499: cannot borrow as mutable more than once at a time (mượn khả biến hai lần)
Lỗi E0499 nghĩa là bạn cố có hai tham chiếu khả biến (&mut) tới cùng một giá trị vào cùng một lúc. Đây là một trong những lỗi borrow checker thường gặp nhất, và cách sửa thường rất nhỏ: nhiều khi bạn chỉ cần dời một dòng. (Bạn đang tìm lỗi khác? Xem tổng quan các lỗi trình biên dịch.)
Thông báo lỗi
Phần tiêu đề “Thông báo lỗi”error[E0499]: cannot borrow `balance` as mutable more than once at a time --> src/main.rs:5:20 |4 | let deposit = &mut balance; | ------------ first mutable borrow occurs here5 | let withdraw = &mut balance; | ^^^^^^^^^^^^ second mutable borrow occurs here6 |7 | *deposit += 50; | -------------- first borrow later used hereThông báo của trình biên dịch luôn bằng tiếng Anh. Nó nói: không thể mượn balance dạng khả biến nhiều hơn một lần cùng lúc. Đây là chương trình gây ra lỗi đó:
fn main() { let mut balance = 100;
let deposit = &mut balance; let withdraw = &mut balance;
*deposit += 50; *withdraw -= 30;
println!("Số dư: {balance}");}Ý nghĩa
Phần tiêu đề “Ý nghĩa”Trình biên dịch chỉ vào ba chỗ, và gộp lại chúng kể toàn bộ câu chuyện:
- Dòng 4:
deposittrở thành một lần mượn khả biến củabalance. - Dòng 5:
withdrawcố trở thành lần mượn khả biến thứ hai củabalance. - Dòng 7:
depositvẫn được dùng sau dòng 5. Vì vậy lần mượn đầu tiên vẫn còn sống khi lần thứ hai bắt đầu.
Nhãn thứ ba là nhãn quan trọng. Hai lần mượn &mut chỉ là vấn đề khi lifetime (vòng đời) của chúng chồng lên nhau. Nếu deposit đã dùng xong trước dòng 5, sẽ không có lỗi nào.
Hãy hình dung một tài liệu dùng chung chỉ có một chiếc chìa khóa “chỉnh sửa”. Ai giữ chìa khóa thì người đó được sửa tài liệu. Rust sẽ không làm thêm chìa: người thứ hai phải đợi đến khi người thứ nhất trả lại.
Vì sao Rust làm vậy
Phần tiêu đề “Vì sao Rust làm vậy”Quy tắc mượn của Rust là: tại mọi thời điểm bạn có thể có nhiều người đọc (&) hoặc một người ghi (&mut), không bao giờ nhiều hơn một người ghi. Một tham chiếu &mut là một lời hứa: “chừng nào tôi còn tồn tại, không ai khác được đụng vào giá trị này”.
Lời hứa đó ngăn chặn những bug thật. Nếu hai đoạn code có thể cùng lúc thay đổi một giá trị, đoạn này có thể thay đổi kích thước một Vec trong khi đoạn kia vẫn trỏ vào vùng nhớ cũ của nó. Ở các ngôn ngữ khác, đó là sập chương trình hoặc dữ liệu bị hỏng một cách âm thầm. Giữa các luồng, đó là data race (tranh chấp dữ liệu).
Nhờ quy tắc này, trình biên dịch có thể chắc chắn rằng trong lúc một &mut còn sống, nó là con đường duy nhất tới dữ liệu. Bạn có được sự an toàn đó mà không tốn chút chi phí nào lúc chạy. Cái giá phải trả là đôi khi bạn phải nói rõ hơn với trình biên dịch rằng hai lần mượn của bạn thật ra không đụng nhau.
Cách sửa
Phần tiêu đề “Cách sửa”Cách sửa 1: Dùng xong lần mượn thứ nhất rồi mới bắt đầu lần thứ hai
Phần tiêu đề “Cách sửa 1: Dùng xong lần mượn thứ nhất rồi mới bắt đầu lần thứ hai”Hãy thử cách này trước. Nó sửa được phần lớn lỗi E0499. Một lần mượn kéo dài đến lần dùng cuối cùng, chứ không phải đến cuối khối. Vì vậy hãy làm hết việc với tham chiếu thứ nhất, rồi mới tạo tham chiếu thứ hai:
fn main() { let mut balance = 100;
let deposit = &mut balance; *deposit += 50; // lần dùng cuối của `deposit`: lần mượn kết thúc ở đây
let withdraw = &mut balance; // ✅ giờ không còn ai đang mượn *withdraw -= 30;
println!("Số dư: {balance}");}Kết quả:
Số dư: 120Nhiều khi bạn chẳng cần thêm biến nào cả: balance += 50; rồi balance -= 30; cũng chạy tốt như vậy.
Cách sửa 2: Mượn những phần không chồng lên nhau
Phần tiêu đề “Cách sửa 2: Mượn những phần không chồng lên nhau”Đôi khi bạn thật sự cần hai &mut cùng lúc, nhưng chúng trỏ tới những phần khác nhau của dữ liệu. Trình biên dịch không phải lúc nào cũng nhận ra điều đó, nên bạn phải chỉ cho nó thấy.
Với hai phần tử của một Vec hoặc slice, tasks[0] và tasks[1] đều mượn toàn bộ tasks. Hãy dùng split_at_mut (tiếng Anh) để cắt nó thành hai nửa được đảm bảo không chồng lên nhau:
fn main() { let mut tasks = vec![String::from("write"), String::from("test")];
// Tách Vec thành hai nửa không chồng lên nhau let (left, right) = tasks.split_at_mut(1); let first = &mut left[0]; let second = &mut right[0];
std::mem::swap(first, second); println!("{tasks:?}");}Kết quả:
["test", "write"](Nếu chỉ để hoán đổi, tasks.swap(0, 1) còn đơn giản hơn. split_at_mut dành cho lúc bạn cần cả hai tham chiếu để làm việc thật.)
Với một struct, cùng lỗi này xuất hiện khi bạn gọi hai phương thức mà mỗi cái đều nhận &mut self, như p.name_mut() và p.score_mut(). Mỗi lời gọi mượn toàn bộ struct. Nếu bạn mượn trực tiếp các trường, trình biên dịch sẽ thấy chúng tách biệt:
struct Player { name: String, score: u32,}
fn main() { let mut p = Player { name: String::from("Ferris"), score: 0 };
// Mượn trực tiếp các trường, không qua phương thức let name = &mut p.name; let score = &mut p.score;
name.push_str(" (chú cua)"); *score += 10; println!("{} có {} điểm", p.name, p.score);}Kết quả:
Ferris (chú cua) có 10 điểmCách sửa 3: Dùng chỉ số thay vì giữ tham chiếu
Phần tiêu đề “Cách sửa 3: Dùng chỉ số thay vì giữ tham chiếu”Một trường hợp kinh điển: bạn lặp qua một Vec bằng iter_mut() và đồng thời cố push vào nó bên trong vòng lặp. Iterator giữ một &mut suốt cả vòng lặp, nên push là cái thứ hai:
fn main() { let mut queue = vec![1, 2, 3]; for job in queue.iter_mut() { *job *= 10; queue.push(*job); // ❌ E0499: `queue` đã bị iter_mut() mượn rồi }}Khi vòng lặp phải thay đổi chính collection, hãy lặp qua vị trí thay vào đó. Chỉ số chỉ là một con số, nên nó không mượn gì cả:
fn main() { let mut queue = vec![1, 2, 3];
// Lặp qua vị trí thay vì giữ một tham chiếu vào `queue` for i in 0..queue.len() { queue[i] *= 10; let job = queue[i]; queue.push(job); }
println!("{queue:?}");}Kết quả:
[10, 20, 30, 10, 20, 30]0..queue.len() được tính một lần, trước khi vòng lặp bắt đầu, nên các phần tử mới không bị duyệt lại. Mỗi lần mượn queue[i] chỉ kéo dài trong một dòng.
Cách sửa 4: RefCell (phương án cuối cùng)
Phần tiêu đề “Cách sửa 4: RefCell (phương án cuối cùng)”RefCell dời việc kiểm tra mượn từ lúc biên dịch sang lúc chạy. Bạn có thể gọi borrow_mut() mà không cần biến mut, miễn là không có hai lần mượn khả biến nào cùng sống vào một thời điểm:
use std::cell::RefCell;
fn main() { let balance = RefCell::new(100); // lưu ý: không cần `mut`
*balance.borrow_mut() += 50; // mượn, sửa, lần mượn kết thúc ở dấu `;` *balance.borrow_mut() -= 30;
println!("Số dư: {}", balance.borrow());}Kết quả:
Số dư: 120Hãy thành thật với bản thân: RefCell không xóa bỏ quy tắc, nó chỉ kiểm tra muộn hơn. Nếu bạn giữ hai “guard” borrow_mut() cùng sống một lúc, chương trình vẫn biên dịch được nhưng sẽ panic khi chạy:
use std::cell::RefCell;
fn main() { let balance = RefCell::new(100);
let mut deposit = balance.borrow_mut(); let mut withdraw = balance.borrow_mut(); // 💥 `deposit` vẫn còn sống
*deposit += 50; *withdraw -= 30;}thread 'main' (12345) panicked at src/main.rs:7:32:RefCell already borrowedThông báo panic (bằng tiếng Anh) nói rằng RefCell đã đang bị mượn rồi. Vì vậy chỉ dùng RefCell khi dữ liệu thật sự được chia sẻ (ví dụ bên trong Rc<RefCell<T>>) và các cách sửa 1–3 không phù hợp. Nó không phải là cách để bịt miệng trình biên dịch.
Thường gặp ở đâu
Phần tiêu đề “Thường gặp ở đâu”- Lưu
&muttrong một vòng lặp.handles.push(&mut counter)bên trong vòng lặpforsẽ lỗi với thông báo “mutably borrowed here in the previous iteration of the loop” (đã bị mượn khả biến ở lượt lặp trước). Mỗi lượt lặp thêm một&mutnữa tới cùng một giá trị. Cách sửa: lưu chỉ số hoặc giá trị sở hữu, hoặc thay đổi giá trị trực tiếp trong vòng lặp. - Hai lời gọi
get_muttrên mộtHashMap.let a = stock.get_mut("apples")rồilet p = stock.get_mut("pears")mượnstockhai lần. Cách sửa: dùng xongatrước khi gọiget_mutlần nữa, hoặc dùngstock.get_disjoint_mut(["apples", "pears"])để lấy cả hai cùng lúc. - Hai closure cùng thay đổi một biến.
let mut inc = || count += 1;vàlet mut inc2 = || count += 2;mỗi cái giữ một&mut countsuốt thời gian nó còn sống. Cách sửa: dùng một closure thôi, hoặc gọi closure thứ nhất lần cuối trước khi tạo closure thứ hai. Xem Closure.
Lỗi liên quan
Phần tiêu đề “Lỗi liên quan”- E0502: cannot borrow as mutable because it is also borrowed as immutable: cùng ý tưởng, nhưng một trong hai lần mượn là
&chỉ đọc. - E0382: borrow of moved value: bạn dùng một giá trị sau khi nó đã bị move đi nơi khác.
- E0106: missing lifetime specifier: Rust cần biết một tham chiếu được trả về sống được bao lâu.
- Tất cả lỗi trình biên dịch: danh sách đầy đủ các trang lỗi.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Khi nào Rust báo lỗi E0499?
2. Bạn tạo let a = &mut v;, dùng a lần cuối, rồi tạo let b = &mut v;. Chuyện gì xảy ra?
3. Công cụ nào cho bạn hai tham chiếu &mut tới hai phần tử khác nhau của cùng một slice?
Điểm: 0 / 3
Đọc thêm
Phần tiêu đề “Đọc thêm”- E0499 — Rust error codes index (tiếng Anh): phần giải thích chính thức của lỗi này, kèm một ví dụ tối giản.
- References and Borrowing — The Rust Book (tiếng Anh): chương giới thiệu quy tắc “mỗi lúc chỉ một
&mut”. - Splitting Borrows — The Rustonomicon (tiếng Anh): vì sao các trường của struct có thể được mượn riêng còn phần tử của slice thì không, và
split_at_muthoạt động thế nào. - RefCell and Interior Mutability — The Rust Book (tiếng Anh): khi nào dời việc kiểm tra mượn sang lúc chạy là lựa chọn đúng.