Lỗi Rust E0382: borrow of moved value (dùng giá trị đã bị move)
E0382 nghĩa là bạn đã trao một giá trị đi (bạn đã move nó) rồi lại cố dùng nó lần nữa. Đây là lỗi phổ biến nhất mà người học gặp trong tuần đầu tiên với Rust, và gần như lúc nào cũng sửa được chỉ bằng một dòng. Trang này đưa ra cách sửa trước, rồi mới giải thích chuyện gì đang xảy ra. 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[E0382]: borrow of moved value: `name` --> src/main.rs:8:25 |6 | let name = String::from("Ferris"); | ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait7 | greet(name); // name moves into greet here | ---- value moved here8 | println!("Goodbye, {name}!"); // ❌ name is gone | ^^^^ value borrowed here after move |note: consider changing this parameter type in function `greet` to borrow instead if owning the value isn't necessary --> src/main.rs:1:16 |1 | fn greet(name: String) { | ----- ^^^^^^ this parameter takes ownership of the valuehelp: consider cloning the value if the performance cost is acceptable |7 | greet(name.clone()); // name moves into greet here | ++++++++Thông báo của trình biên dịch luôn bằng tiếng Anh: “borrow of moved value” nghĩa là “mượn một giá trị đã bị move”. Đâ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 greet(name: String) { println!("Hello, {name}!");}
fn main() { let name = String::from("Ferris"); greet(name); // name moves into greet here println!("Goodbye, {name}!"); // ❌ name is gone}Bạn cũng có thể gặp cách diễn đạt “use of moved value” (dùng giá trị đã bị move). Đó vẫn là cùng một lỗi. Rust nói “borrow” khi dòng phía sau chỉ đọc giá trị (như println!), và nói “use” khi dòng đó cố move nó thêm lần nữa (như let other = name;).
Ý nghĩa
Phần tiêu đề “Ý nghĩa”Hãy đọc ba nhãn từ trên xuống dưới. Chúng kể lại toàn bộ câu chuyện:
- Dòng 6:
namelà mộtString, vàStringkhông phảiCopy. Vì vậy truyền nó đi đâu đó sẽ move nó. - Dòng 7:
greet(name)move chuỗi vào trong hàm. Giờgreetsở hữu nó, và chuỗi bị hủy khigreetkết thúc. - Dòng 8: bạn cố đọc lại
name, nhưngmainkhông còn sở hữu nó nữa.
Hãy hình dung bạn đưa chiếc chìa khóa nhà duy nhất cho một người bạn. Đã đưa đi rồi thì bạn không thể dùng nó để mở cửa, dù bạn vẫn nhớ là mình từng có nó.
Vì sao Rust làm vậy
Phần tiêu đề “Vì sao Rust làm vậy”Mỗi giá trị trong Rust có đúng một chủ sở hữu. Khi chủ sở hữu biến mất, giá trị được dọn dẹp. Đây là cách Rust giải phóng bộ nhớ mà không cần bộ thu gom rác (garbage collector). Các quy tắc được giải thích ở trang Quyền sở hữu.
Nếu hai biến cùng sở hữu một String, thì cuối cùng cả hai sẽ cố giải phóng cùng một vùng nhớ. Đó là lỗi “double free” (giải phóng hai lần), một bug thật sự làm chương trình C và C++ bị sập. Rust ngăn chuyện đó bằng cách cho phép gán và lời gọi hàm move giá trị, nhờ vậy lúc nào cũng chỉ có một chủ sở hữu.
Các kiểu nhỏ, đơn giản như i32, bool và char là Copy: truyền chúng đi sẽ tạo một bản sao rẻ, và bạn sẽ không bao giờ gặp E0382 với chúng. Các kiểu sở hữu bộ nhớ trên heap, như String, Vec và phần lớn struct, thì bị move.
Cách sửa
Phần tiêu đề “Cách sửa”Cách sửa 1: Mượn bằng &
Phần tiêu đề “Cách sửa 1: Mượn bằng &”Chọn cách này khi hàm chỉ cần xem giá trị. Phần lớn trường hợp, đây là cách sửa đúng. Đổi tham số thành tham chiếu và truyền &name:
fn greet(name: &str) { println!("Xin chào, {name}!");}
fn main() { let name = String::from("Ferris"); greet(&name); // cho mượn, không trao đi println!("Tạm biệt, {name}!"); // ✅ vẫn là của mình}Kết quả:
Xin chào, Ferris!Tạm biệt, Ferris!&String cũng chạy được, nhưng &str là lựa chọn thường dùng vì nó nhận được cả chuỗi literal. Xem Mượn và Chuỗi. Nếu hàm cần thay đổi giá trị, hãy dùng &mut.
Cách sửa 2: Clone nó
Phần tiêu đề “Cách sửa 2: Clone nó”Chọn cách này khi đoạn code kia thật sự cần bản sao của riêng nó, ví dụ để lưu vào một struct hoặc một Vec, mà bạn vẫn cần bản gốc. .clone() tạo ra một bản sao đầy đủ và độc lập:
struct Player { name: String, score: u32,}
fn main() { let name = String::from("Ferris"); let player = Player { name: name.clone(), score: 0 }; println!("{} bắt đầu với {} điểm", player.name, player.score); println!("Tên gốc: {name}");}Kết quả:
Ferris bắt đầu với 0 điểmTên gốc: FerrisHãy thành thật với bản thân: clone một chuỗi ngắn hay một Vec nhỏ là rẻ, và hoàn toàn ổn, nhất là khi bạn đang học. Chỉ cần lo khi bạn clone dữ liệu lớn bên trong một vòng lặp chạy rất nhiều lần. Nếu mượn (cách sửa 1) dùng được, hãy ưu tiên cách đó.
Cách sửa 3: Derive Copy cho kiểu nhỏ
Phần tiêu đề “Cách sửa 3: Derive Copy cho kiểu nhỏ”Chọn cách này cho các struct nhỏ chỉ gồm những giá trị đơn giản, như tọa độ, màu sắc hoặc ID. Thêm #[derive(Clone, Copy)] và struct sẽ được sao chép thay vì bị move:
#[derive(Debug, Clone, Copy)]struct Point { x: i32, y: i32,}
fn show(p: Point) { println!("({}, {})", p.x, p.y);}
fn main() { let start = Point { x: 0, y: 3 }; show(start); // sao chép start show(start); // ✅ lại chạy được println!("{:?}", start);}Kết quả:
(0, 3)(0, 3)Point { x: 0, y: 3 }Copy chỉ dùng được khi mọi trường cũng là Copy. Một struct có trường String hoặc Vec không thể là Copy; với những struct đó, hãy dùng Clone và gọi .clone(). Xem thêm ở Các trait thông dụng.
Cách sửa 4: Trả lại quyền sở hữu
Phần tiêu đề “Cách sửa 4: Trả lại quyền sở hữu”Chọn cách này khi một hàm nhận một giá trị, thay đổi nó, và bạn muốn lấy kết quả. Hàm trả giá trị về, và bạn lưu nó lại:
fn add_signature(mut letter: String) -> String { letter.push_str("\n-- Ferris"); letter // trả nó về}
fn main() { let letter = String::from("Gửi bạn Rustacean thân mến,"); let letter = add_signature(letter); // nhận lại nó println!("{letter}");}Kết quả:
Gửi bạn Rustacean thân mến,-- FerrisCách này hợp với các hàm kiểu “builder”. Với một thay đổi tại chỗ đơn giản, fn add_signature(letter: &mut String) thường dễ đọc hơn.
Thường gặp ở đâu
Phần tiêu đề “Thường gặp ở đâu”Vòng lặp for trên một Vec. for task in tasks move cả vector vào vòng lặp, nên sau đó bạn không dùng được tasks nữa:
for task in tasks { println!("Doing: {task}");}println!("{} tasks done", tasks.len()); // ❌ `tasks` moved due to this implicit call to `.into_iter()`Cách sửa: lặp qua một tham chiếu mượn, for task in &tasks. Xem Vòng lặp for và Iterator.
Đưa một giá trị vào Vec hoặc struct. names.push(name) và Order { customer } đều move giá trị vào bên trong. Dùng name hoặc customer sau đó sẽ lỗi. Cách sửa: đọc lại nó từ “nhà mới” (names[0], order.customer), làm những việc khác trước khi move, hoặc push một bản .clone().
Move bên trong vòng lặp. Nếu log.push(message) nằm trong một vòng lặp, lượt đầu tiên move message đi và lượt thứ hai chẳng còn gì. Trình biên dịch báo value moved here, in previous iteration of loop (giá trị bị move ở đây, trong lượt lặp trước). Cách sửa: push message.clone(), hoặc tạo giá trị bên trong vòng lặp để mỗi lượt có một bản mới.
Luồng và closure move. thread::spawn(move || ...) move mọi biến được bắt (capture) vào luồng mới. Cách sửa: clone dữ liệu trước khi tạo luồng, hoặc chia sẻ nó bằng Arc. Xem Đồng thời và Trạng thái chia sẻ.
Lỗi liên quan
Phần tiêu đề “Lỗi liên quan”- E0502: bạn đã mượn một giá trị rồi lại cố thay đổi nó trong khi tham chiếu mượn vẫn còn được dùng.
- E0499: hai tham chiếu khả biến tới cùng một giá trị cùng lúc.
- E0106: thiếu chỉ định lifetime, hay gặp khi bạn trả về một tham chiếu để tránh move.
- E0308: kiểu không khớp, ví dụ truyền
Stringở chỗ cần&strsau khi chuyển sang mượn. - 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. Bạn gọi save(report) với report là một String, rồi in report ra. Vì sao bạn gặp E0382?
2. Một hàm chỉ cần đọc một Vec<String>. Thường thì cách sửa tốt nhất là gì?
3. Struct nào dùng được #[derive(Clone, Copy)]?
Điểm: 0 / 3
Đọc thêm
Phần tiêu đề “Đọc thêm”- E0382 — 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 thêm ví dụ.
- What Is Ownership? — The Rust Book (tiếng Anh): chương giải thích move,
ClonevàCopytừng bước một. - References and Borrowing — The Rust Book (tiếng Anh): cách
&cho phép bạn dùng một giá trị mà không move nó. - Ownership and moves — Rust by Example (tiếng Anh): các ví dụ ngắn chạy được về move và move một phần.
Copytrait — tài liệu thư viện chuẩn (tiếng Anh): kiểu nào làCopy, và khác biệt giữaCopyvớiClone.