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

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.

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` trait
7 | greet(name); // name moves into greet here
| ---- value moved here
8 | 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 value
help: 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;).

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:

  1. Dòng 6: name là một String, và String không phải Copy. Vì vậy truyền nó đi đâu đó sẽ move nó.
  2. Dòng 7: greet(name) move chuỗi vào trong hàm. Giờ greet sở hữu nó, và chuỗi bị hủy khi greet kết thúc.
  3. Dòng 8: bạn cố đọc lại name, nhưng main khô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ó.

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.

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

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

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ểm
Tên gốc: Ferris
▶ Tự chạy thử

Hã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 đó.

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

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.

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,
-- Ferris
▶ Tự chạy thử

Cá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.

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

  • 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 &str sau 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

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