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

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

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 here
5 | let withdraw = &mut balance;
| ^^^^^^^^^^^^ second mutable borrow occurs here
6 |
7 | *deposit += 50;
| -------------- first borrow later used here

Thô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}");
}

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:

  1. Dòng 4: deposit trở thành một lần mượn khả biến của balance.
  2. Dòng 5: withdraw cố trở thành lần mượn khả biến thứ hai của balance.
  3. Dòng 7: deposit vẫ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.

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

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

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

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

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

Hã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 borrowed

Thô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.

  • Lưu &mut trong một vòng lặp. handles.push(&mut counter) bên trong vòng lặp for sẽ 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 &mut nữ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_mut trên một HashMap. let a = stock.get_mut("apples") rồi let p = stock.get_mut("pears") mượn stock hai lần. Cách sửa: dùng xong a trước khi gọi get_mut lần nữa, hoặc dùng stock.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 count suố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.

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