Lỗi Gặp Phải
Hai tham chiếu có thể thay đổi cùng trỏ đến một biến, cả hai đều tồn tại cùng lúc. Trình biên dịch Rust phát hiện ngay lập tức và từ chối build:
error[E0499]: cannot borrow `vec` as mutable more than once at a time
--> src/main.rs:5:18
|
4 | let first = &mut vec;
| -------- first mutable borrow occurs here
5 | let second = &mut vec;
| ^^^^^^^^ second mutable borrow occurs here
6 | println!("{}", first[0]);
| ----- first borrow later used here
Đây là borrow checker đang làm đúng nhiệm vụ của nó. Quy tắc rất nghiêm ngặt: chỉ một tham chiếu có thể thay đổi tại một thời điểm, không có ngoại lệ.
Nguyên Nhân
Rust ngăn chặn data race ngay tại thời điểm biên dịch. Hai tham chiếu có thể thay đổi cùng trỏ vào một dữ liệu có nghĩa là một tham chiếu có thể sửa đổi hoặc làm vô hiệu dữ liệu trong khi tham chiếu kia đang đọc — một lỗi kinh điển trong C/C++ mà Rust khiến nó không thể xảy ra về mặt cấu trúc.
Điểm dễ nhầm lẫn: "cùng lúc" không có nghĩa như bạn nghĩ. Một mutable borrow tồn tại cho đến lần sử dụng cuối cùng của nó, không phải cho đến khi bạn gán biến tiếp theo. Điều này khiến nhiều lập trình viên đến từ ngôn ngữ khác bị vấp ngã.
Các Bước Sửa Lỗi
Bước 1: Xác định các borrow đang chồng chéo nhau
Chạy rustc --explain E0499 để xem giải thích đầy đủ từ trình biên dịch. Thông báo lỗi cũng chỉ ra chính xác nơi mỗi borrow bắt đầu và nơi nó vẫn đang được sử dụng. Mục tiêu của bạn: đảm bảo mutable borrow đầu tiên kết thúc hoàn toàn trước khi borrow thứ hai bắt đầu.
Bước 2: Để borrow đầu tiên ra khỏi scope
Cách sửa đơn giản nhất — sử dụng tham chiếu đầu tiên hoàn toàn xong, rồi mới tạo tham chiếu thứ hai.
fn main() {
let mut vec = vec![1, 2, 3];
// SAI: cả hai borrow cùng tồn tại một lúc
// let first = &mut vec;
// let second = &mut vec; // lỗi!
// ĐÚNG: borrow đầu tiên kết thúc trước khi borrow thứ hai bắt đầu
{
let first = &mut vec;
first.push(4);
} // borrow đầu tiên kết thúc ở đây
let second = &mut vec;
second.push(5);
println!("{:?}", vec); // [1, 2, 3, 4, 5]
}
Bước 3: Dùng chỉ số thay vì nhiều tham chiếu (với collection)
Cố gắng tham chiếu có thể thay đổi vào hai phần tử của cùng một Vec cùng lúc là một trong những nguyên nhân phổ biến nhất gây ra E0499. Truy cập theo chỉ số giúp tránh hoàn toàn vấn đề này:
fn main() {
let mut data = vec![10, 20, 30];
// SAI: hai tham chiếu có thể thay đổi vào cùng một Vec
// let a = &mut data[0];
// let b = &mut data[1]; // lỗi!
// ĐÚNG: dùng chỉ số trực tiếp
data[0] += data[1]; // đọc [1] và ghi [0] trong một câu lệnh
println!("{:?}", data); // [30, 20, 30]
// Hoặc: tách slice ra
let (left, right) = data.split_at_mut(1);
left[0] = right[0] + right[1];
println!("{:?}", data); // [50, 20, 30]
}
Bước 4: Dùng split_at_mut để thay đổi các phần slice không chồng chéo
Cần hai vùng có thể thay đổi vào các phần khác nhau của cùng một collection? split_at_mut chia slice thành hai nửa có thể thay đổi không chồng chéo nhau. Borrow checker chấp nhận điều này vì hai nửa được chứng minh là không thể alias nhau:
fn swap_first_last(data: &mut Vec<i32>) {
let len = data.len();
if len < 2 {
return;
}
let (head, tail) = data.split_at_mut(len - 1);
std::mem::swap(&mut head[0], &mut tail[0]);
}
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
swap_first_last(&mut v);
println!("{:?}", v); // [5, 2, 3, 4, 1]
}
Bước 5: Tách logic thành các lượt xử lý riêng biệt
Đôi khi chính thiết kế mới là vấn đề. Đọc và ghi vào cùng một cấu trúc đồng thời vốn dĩ đã mâu thuẫn — hãy tách thành hai lượt riêng biệt:
fn main() {
let mut scores: Vec<i32> = vec![3, 7, 2, 9, 1];
// Mẫu SAI: cố tìm max và cập nhật trong một lượt
// với nhiều mutable borrow
// ĐÚNG: hai lượt xử lý riêng biệt
let max = *scores.iter().max().unwrap(); // lượt đọc (immutable borrow)
for s in scores.iter_mut() { // lượt ghi (mutable borrow)
*s = if *s == max { 100 } else { *s };
}
println!("{:?}", scores); // [3, 7, 2, 100, 1]
}
Bước 6: Dùng RefCell cho interior mutability (khi thực sự cần)
Đôi khi borrow checker quá bảo thủ — cấu trúc dạng đồ thị và một số mẫu đệ quy là ví dụ điển hình. RefCell<T> là lối thoát: nó trì hoãn việc kiểm tra borrow sang thời điểm chạy thay vì biên dịch.
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
{
let mut borrow = data.borrow_mut();
borrow.push(4);
} // mutable borrow được giải phóng ở đây
println!("{:?}", data.borrow()); // [1, 2, 3, 4]
}
Cảnh báo: RefCell sẽ panic khi chạy nếu bạn thực sự vi phạm quy tắc borrow. Hãy coi đây là phương án cuối cùng — tái cấu trúc code hầu như luôn là giải pháp gọn gàng hơn.
Kiểm Tra Sau Khi Sửa
Build project và xác nhận E0499 đã biến mất:
cargo build
Sau đó chạy test để đảm bảo không có gì bị hỏng:
cargo test
Cũng nên chạy cargo clippy — nó phát hiện các mẫu code dễ gây ra lỗi này trước khi bạn kịp gặp trình biên dịch:
cargo clippy
Tóm Tắt Nhanh: Dùng Cách Sửa Nào
- Các borrow thực ra không chồng chéo? Thêm khối
{ }để giới hạn scope của borrow đầu tiên. - Hai phần tử của một Vec? Dùng
split_at_mut()hoặc phép tính chỉ số. - Mẫu đọc rồi ghi? Tách thành hai lượt xử lý riêng biệt.
- Trạng thái chia sẻ phức tạp? Dùng
RefCell<T>(single-threaded) hoặcMutex<T>(multi-threaded). - Cấu trúc đồ thị / cây? Cân nhắc các crate arena như
slotmaphoặcgenerational-arena.
Những Lỗi Thường Gặp
- Giả định borrow kết thúc tại dòng gán biến. Không phải vậy. Một borrow tồn tại cho đến lần sử dụng cuối cùng của nó. NLL (Non-Lexical Lifetimes), được giới thiệu trong Rust 2018, tự động xử lý nhiều trường hợp rõ ràng — nhưng các mẫu phức tạp vẫn có thể khiến bạn bất ngờ.
- Dùng
RefCellngay từ đầu. Đây là phương án cuối cùng, không phải giải pháp tiện lợi. Tái cấu trúc code hầu như luôn là lựa chọn đúng đắn hơn. - Gọi phương thức
&mut selftrong khi đang giữ tham chiếu đến một trường. Lời gọi phương thức đó sẽ mượn toàn bộ struct theo dạng có thể thay đổi. Bất kỳ tham chiếu nào đang tồn tại đến bất kỳ trường nào của struct đó đều sẽ xung đột — dù phương thức đó không bao giờ động đến trường cụ thể đó.

