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.
Thông báo lỗi
Phần tiêu đề “Thông báo lỗi”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 here4 | v.push(4); // ❌ changes the Vec while `first` is alive | ^^^^^^^^^ mutable borrow occurs here5 | println!("First: {first}"); // ...because `first` is used here | ----- immutable 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 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}Ý nghĩa
Phần tiêu đề “Ý nghĩa”Trình biên dịch chỉ vào ba dòng, và bạn cần cả ba để hiểu lỗi:
- Dòng 3:
&v[0]tạo một tham chiếu chỉ đọc vào trongv. Đó là lần mượn bất biến (immutable borrow). - Dòng 4:
v.push(4)cần thay đổiv, nên nó lấy một lần mượn khả biến (mutable borrow). - Dòng 5:
firstlạ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.
Vì sao Rust làm vậy
Phần tiêu đề “Vì sao Rust làm vậy”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 đó.
Cách sửa
Phần tiêu đề “Cách sửa”Cách sửa 1: Đọc xong rồi hãy ghi
Phần tiêu đề “Cách sửa 1: Đọc xong rồi hãy ghi”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: 1Bây giờ: [1, 2, 3, 4]Cách sửa 2: Sao chép giá trị ra
Phần tiêu đề “Cách sửa 2: Sao chép giá trị ra”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: deployHàng đợi: ["deploy", "test", "release"]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]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]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á.
Cách sửa 5: Dùng chỉ số
Phần tiêu đề “Cách sửa 5: Dùng chỉ số”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]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.
Thường gặp ở đâu
Phần tiêu đề “Thường gặp ở đâu”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.
Lỗi liên quan
Phần tiêu đề “Lỗi liên quan”- 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
Phần tiêu đề “Kiểm tra nhanh”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
Đọc thêm
Phần tiêu đề “Đọc thêm”- E0502 — Rust error codes index (tiếng Anh): phần giải thích chính thức của lỗi này.
- Storing Lists of Values with Vectors — The Rust Book (tiếng Anh): có đúng ví dụ
let first = &v[0]; v.push(..)này và lý do nó thất bại. - References and Borrowing — The Rust Book (tiếng Anh): quy tắc “nhiều người đọc hoặc một người ghi” đầy đủ.
- Non-lexical lifetimes — The Edition Guide (tiếng Anh): vì sao giờ đây một lần mượn kết thúc ở lần dùng cuối thay vì ở dấu ngoặc nhọn đóng.
Vec— tài liệu thư viện chuẩn (tiếng Anh):retain,iter_mut,extendvà các phương thức khác giúp bạn thay đổi vector một cách an toàn.