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

Lỗi Rust E0502: cannot borrow as mutable because it is also borrowed as immutable

E0502 nghĩa là bạn cố thay đổi một giá trị trong khi một chỗ khác trong code vẫn đang đọc nó qua một tham chiếu. Lỗi này xuất hiện liên tục khi làm việc với vector, và cách sửa thường chỉ là dời một dòng hoặc sao chép một giá trị. Trang này đưa ra cách sửa trước, rồi giải thích vì sao Rust nhất quyết đòi như vậy. Các lỗi khác trong loạt bài này nằm ở trang Lỗi trình biên dịch Rust.

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let first = &v[0]; // a reference into the Vec
| - immutable borrow occurs here
4 | v.push(4); // ❌ changes the Vec while `first` is alive
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("First: {first}"); // ...because `first` is used here
| ----- immutable 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 v dạng khả biến (mutable) vì v cũng đang được mượn dạng bất biến (immutable). Đây là chương trình gây ra lỗi đó (các chú thích được giữ bằng tiếng Anh vì chúng xuất hiện nguyên văn trong thông báo lỗi):

fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // a reference into the Vec
v.push(4); // ❌ changes the Vec while `first` is alive
println!("First: {first}"); // ...because `first` is used here
}

Trình biên dịch chỉ vào ba dòng, và bạn cần cả ba để hiểu lỗi:

  1. Dòng 3: &v[0] tạo một tham chiếu chỉ đọc vào trong v. Đó là lần mượn bất biến (immutable borrow).
  2. Dòng 4: v.push(4) cần thay đổi v, nên nó lấy một lần mượn khả biến (mutable borrow).
  3. Dòng 5: first lại được dùng. Chính dòng này giữ cho lần mượn đầu tiên còn sống qua dòng 4.

Nếu không có dòng 5, code sẽ biên dịch được. Vấn đề không nằm ở việc có hai lần mượn, mà ở chỗ chúng chồng lên nhau.

Quy tắc là: nhiều người đọc hoặc một người ghi, không bao giờ cả hai cùng lúc. Bạn đã học quy tắc này ở trang Mượn.

Với Vec có một lý do rất cụ thể. Vec giữ các phần tử trong một khối bộ nhớ liền nhau. Khi bạn push mà khối đó đã đầy, Vec chuyển mọi thứ sang một khối mới lớn hơn và giải phóng khối cũ. Mọi tham chiếu vào khối cũ (như first) giờ trỏ tới vùng nhớ đã giải phóng. Trong C++, đây là lỗi “iterator invalidation” nổi tiếng, có thể làm chương trình sập hoặc lặng lẽ đọc phải dữ liệu rác.

Giống như bạn ghi lại địa chỉ nhà một người bạn, rồi họ chuyển sang nhà khác lớn hơn. Tờ giấy của bạn trông vẫn ổn, nhưng nó dẫn tới một căn nhà trống. Rust từ chối biên dịch code, thay vì để bạn đến gõ cửa căn nhà trống đó.

Hãy thử cách này trước. Từ Rust 2018, một lần mượn kết thúc ở lần dùng cuối cùng, chứ không phải ở dấu ngoặc nhọn đóng. Điều này gọi là non-lexical lifetimes. Vì vậy chỉ cần dùng xong tham chiếu trước khi thay đổi vector:

fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
println!("Phần tử đầu: {first}"); // lần dùng cuối của `first`: lần mượn kết thúc ở đây
v.push(4); // ✅ không còn ai đang đọc nữa
println!("Bây giờ: {:?}", v);
}

Kết quả:

Phần tử đầu: 1
Bây giờ: [1, 2, 3, 4]
▶ Tự chạy thử

Chọn cách này khi bạn cần giá trị cũ sau khi đã thay đổi. Hãy lấy một bản sao thay vì tham chiếu. Với số, chỉ cần bỏ dấu &. Với String, hãy dùng .clone():

fn main() {
let mut queue = vec![String::from("deploy"), String::from("test")];
let next = queue[0].clone(); // bản sao của riêng mình, không phải tham chiếu
queue.push(String::from("release"));
println!("Tiếp theo: {next}");
println!("Hàng đợi: {:?}", queue);
}

Kết quả:

Tiếp theo: deploy
Hàng đợi: ["deploy", "test", "release"]
▶ Tự chạy thử

Với let first = v[0]; trên một Vec<i32>, bạn được bản sao miễn phí, vì i32 là Copy.

Cách sửa 3: Dùng iter_mut hoặc retain để sửa phần tử tại chỗ

Phần tiêu đề “Cách sửa 3: Dùng iter_mut hoặc retain để sửa phần tử tại chỗ”

Chọn cách này khi bạn viết một vòng lặp như for p in prices.iter() rồi cố gọi prices.remove(i) hoặc sửa phần tử bên trong. Việc đó cũng gây ra E0502. Thư viện chuẩn có sẵn các phương thức dành riêng cho việc này:

fn main() {
let mut prices = vec![120, 45, 300, 80];
// sửa từng phần tử tại chỗ
for p in prices.iter_mut() {
*p = *p * 9 / 10; // giảm 10%
}
// bỏ những phần tử không cần
prices.retain(|&p| p >= 50);
println!("{:?}", prices);
}

Kết quả:

[108, 270, 72]
▶ Tự chạy thử

iter_mut đưa bạn một &mut cho mỗi phần tử, nên lúc nào cũng chỉ có một người ghi. retain lo việc xóa giúp bạn, an toàn, chỉ trong một lượt duyệt. Xem Vector và Iterator.

Cách sửa 4: Gom các thay đổi lại, rồi áp dụng sau

Phần tiêu đề “Cách sửa 4: Gom các thay đổi lại, rồi áp dụng sau”

Chọn cách này khi bạn cần thêm phần tử dựa trên những gì đã đọc. Trước tiên đọc và gom lại, sau đó ghi ở một bước riêng:

fn main() {
let mut scores = vec![10, 25, 7, 40];
let bonuses: Vec<i32> = scores.iter().filter(|&&s| s > 20).map(|s| s * 2).collect();
scores.extend(bonuses); // lần mượn để đọc đã xong, nên ta ghi được
println!("{:?}", scores);
}

Kết quả:

[10, 25, 7, 40, 50, 80]
▶ Tự chạy thử

Vec phụ tốn thêm một chút bộ nhớ, nhưng code rõ ràng và hiển nhiên là đúng. Thường thì đó là một sự đánh đổi đáng giá.

Hãy để cách này làm phương án cuối cùng, khi các cách khác không phù hợp. Chỉ số chỉ là một con số, không phải một lần mượn, nên nó không chặn việc thay đổi:

fn main() {
let mut scores = vec![10, 25, 7];
for i in 0..scores.len() {
if scores[i] > 20 {
scores.push(scores[i] * 2);
}
}
println!("{:?}", scores);
}

Kết quả:

[10, 25, 7, 50]
▶ Tự chạy thử

0..scores.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 được duyệt tới. Hãy cẩn thận khi xóa phần tử theo chỉ số: vị trí sẽ bị dịch đi, và bạn có thể bỏ sót phần tử hoặc gây panic. Để xóa, retain (cách sửa 3) an toàn hơn.

Push vào một Vec trong khi đang lặp qua nó. for s in &scores { scores.push(..) } mượn scores suốt cả vòng lặp. Cách sửa: gom trước rồi extend sau (cách sửa 4), hoặc lặp theo chỉ số (cách sửa 5).

Gọi một phương thức &mut self trong khi đang lặp qua một trường. Chuyện này rất hay gặp bên trong các khối impl:

fn audit(&mut self) {
for item in &self.items {
self.record(item); // ❌ cannot borrow `*self` as mutable because it is also borrowed as immutable
}
}

self.record mượn khả biến toàn bộ self, kể cả self.items. Cách sửa: tác động trực tiếp lên trường kia (self.log.push(item.clone())), vì Rust cho phép mượn hai trường khác nhau cùng lúc. Hoặc clone các phần tử trước. Xem Struct.

Giữ một giá trị lấy từ HashMap trong khi insert. let apples = stock.get("apples").unwrap(); rồi stock.insert(..) rồi dùng apples sẽ gây ra E0502. Cách sửa: sao chép giá trị ra bằng let apples = *stock.get("apples").unwrap(); hoặc .copied(). Xem HashMap.

  • E0499: hai lần mượn khả biến cùng lúc. Cùng một ý tưởng, nhưng là hai người ghi thay vì một người đọc và một người ghi.
  • E0382: bạn đã move một giá trị rồi lại dùng nó. Hay xuất hiện khi bạn sửa E0502 bằng cách move thay vì mượn.
  • E0106: thiếu chỉ định lifetime trên một hàm trả về tham chiế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

1. Vì sao let first = &v[0]; v.push(4); println!("{first}"); không biên dịch được?

2. Bạn muốn xóa mọi phần tử nhỏ hơn 50 khỏi một Vec<i32>. Cách sửa E0502 gọn gàng nhất là gì?

3. Trong Rust hiện đại, một lần mượn bất biến kết thúc khi nào?

Điểm: 0 / 3