Fix lỗi Rust error[E0382]: use of moved value sau khi chuyển quyền sở hữu

beginner🦀 Rust2026-03-25| Rust 1.40+, mọi nền tảng (Linux, macOS, Windows), rustc compiler

Error Message

error[E0382]: use of moved value: `variable_name`
#rust#ownership#move#borrow-checker#e0382

Chuyện Gì Vừa Xảy Ra

Bạn đang biên dịch một chương trình Rust và đột nhiên thấy lỗi này:

error[E0382]: use of moved value: `name`
  --> src/main.rs:6:20
   |
4  |     let name = String::from("Alice");
5  |     greet(name);
   |           ---- value moved here
6  |     println!("{}", name);
   |                    ^^^^ value used here after move

Mô hình ownership của Rust đang gây ra vấn đề ở đây. Khi truyền name vào greet(), quyền sở hữu đã được chuyển vào hàm đó. Sau khi điều đó xảy ra, name không còn tồn tại trong phạm vi của hàm gọi nữa — trình biên dịch sẽ không cho phép bạn truy cập một biến mà nó không còn sở hữu.

Đây không nhất thiết là lỗi logic. Trình biên dịch đang đảm bảo an toàn bộ nhớ, đó chính là mục đích của Rust. Cách sửa phù hợp phụ thuộc vào những gì bạn thực sự cần làm với giá trị đó sau lời gọi hàm.

Tái Hiện Lỗi

fn greet(name: String) {
    println!("Hello, {}!", name);
}

fn main() {
    let name = String::from("Alice");
    greet(name);           // quyền sở hữu chuyển vào greet()
    println!("{}", name);  // LỖI: value used after move
}

Chạy cargo build và bạn sẽ thấy toàn bộ lỗi E0382 với số dòng chính xác chỉ ra cả nơi giá trị được chuyển đi và nơi bạn cố gắng sử dụng lại.

Xác Định Trường Hợp Của Bạn

Chọn cách sửa phù hợp bằng cách trả lời câu hỏi: bạn thực sự cần gì từ giá trị đó sau khi đã truyền nó đi:

  • Chỉ cần đọc giá trị sau đó? → Dùng tham chiếu (&)
  • Cần một bản sao độc lập của dữ liệu? → Clone hoặc copy
  • Muốn hàm trả lại quyền sở hữu? → Trả về giá trị
  • Chuyển vào vòng lặp hoặc closure? → Clone trước khi capture, hoặc cơ cấu lại

Giải Pháp 1: Truyền Tham Chiếu Thay Vì Giá Trị

Chín trường hợp trong mười, đây là cách sửa đúng. Thay đổi hàm để mượn giá trị thay vì lấy quyền sở hữu.

fn greet(name: &str) {  // mượn string slice thay vì sở hữu String
    println!("Hello, {}!", name);
}

fn main() {
    let name = String::from("Alice");
    greet(&name);          // cho greet() mượn tạm
    println!("{}", name);  // vẫn hợp lệ — chúng ta vẫn sở hữu nó
}

Toán tử & tạo ra một tham chiếu. Hàm mượn giá trị tạm thời, và lần mượn đó được giải phóng khi hàm trả về. Bạn vẫn giữ quyền sở hữu xuyên suốt.

Nếu hàm của bạn hiện đang nhận String, chuyển sang &str hầu như luôn là lựa chọn đúng — trừ khi hàm cần lưu trữ hoặc mở rộng chuỗi đó bên trong.

Giải Pháp 2: Clone Giá Trị

Khi bạn thực sự cần hàm sở hữu dữ liệu bạn cần giữ nguyên bản gốc sau đó, hãy clone:

fn greet(name: String) {
    println!("Hello, {}!", name);
}

fn main() {
    let name = String::from("Alice");
    greet(name.clone());   // cho greet() bản sao của riêng nó
    println!("{}", name);  // bản gốc vẫn hợp lệ
}

Clone sẽ cấp phát một bản sao mới trên heap. Đây là giải pháp thô bạo — đúng, nhưng không miễn phí. Tránh dùng trong các vòng lặp nóng hoặc với cấu trúc dữ liệu lớn. Một Vec 1 MB được clone trong mỗi vòng lặp sẽ tích lũy rất nhanh.

Giải Pháp 3: Dùng Kiểu Copy

Các kiểu nguyên thủy như i32, f64, bool, và char triển khai trait Copy. Chúng không bị move — chúng được copy tự động, không cần clone tường minh.

fn double(x: i32) -> i32 {
    x * 2
}

fn main() {
    let n = 5;
    let result = double(n);
    println!("{} doubled is {}", n, result);  // ổn, i32 là Copy
}

Các struct tự định nghĩa cũng có thể có hành vi này, miễn là mọi field đều là Copy:

#[derive(Copy, Clone)]
struct Point {
    x: f64,
    y: f64,
}

fn translate(p: Point, dx: f64) -> Point {
    Point { x: p.x + dx, y: p.y }
}

fn main() {
    let p = Point { x: 1.0, y: 2.0 };
    let q = translate(p, 5.0);
    println!("original x: {}", p.x);  // ổn — Point là Copy
}

Lưu ý: bạn không thể derive Copy nếu bất kỳ field nào chứa String, Vec, hoặc kiểu được cấp phát trên heap khác.

Giải Pháp 4: Move Vào Vòng Lặp Hoặc Closure

Closure là nơi E0382 gây bất ngờ nhất. Từ khóa move capture quyền sở hữu — và sau khi bị capture, bản gốc biến mất:

// Vấn đề: closure lấy quyền sở hữu
let msg = String::from("hello");
let print_msg = move || println!("{}", msg);
print_msg();
println!("{}", msg);  // LỖI: đã bị chuyển vào closure

// Sửa: clone trước khi closure capture
let msg = String::from("hello");
let msg_clone = msg.clone();
let print_msg = move || println!("{}", msg_clone);
print_msg();
println!("{}", msg);  // bản gốc vẫn ở đây

Trong các vòng lặp for thông thường, vấn đề này ít xảy ra hơn. Macro println! mượn ngầm định, vì vậy việc lặp qua &items trong khi tham chiếu msg hoạt động mà không cần clone.

Giải Pháp 5: Trả Lại Quyền Sở Hữu

Đôi khi một hàm cần quyền sở hữu tạm thời và bạn muốn lấy lại sau khi xong:

fn process(data: String) -> String {
    println!("Processing: {}", data);
    data  // trả quyền sở hữu lại cho người gọi
}

fn main() {
    let s = String::from("some data");
    let s = process(s);  // gán lại với giá trị được trả về
    println!("Got back: {}", s);
}

Mượn hầu như luôn gọn gàng hơn, nhưng pattern này phát huy tác dụng khi hàm biến đổi hoặc mở rộng giá trị — hãy nghĩ đến builder và string formatter.

Kiểm Tra Bản Sửa

Build lại và kiểm tra output:

cargo build

Không có lỗi E0382 nghĩa là vấn đề ownership đã được giải quyết. Đi thêm một bước để chắc chắn bản sửa có ý nghĩa ngữ nghĩa:

cargo test
cargo clippy   # phát hiện các pattern có thể được đơn giản hóa thêm

Nếu Clippy gắn cờ chữ ký hàm của bạn và gợi ý &str thay vì String, đó là dấu hiệu bạn đang clone ở nơi mà một tham chiếu là đủ.

Hướng Dẫn Quyết Định Nhanh

  • Hàm chỉ đọc dữ liệu → đổi tham số thành &T hoặc &str
  • Hàm lưu trữ dữ liệu (ví dụ: trong field của struct) → việc chuyển quyền sở hữu là có chủ đích, giữ nguyên
  • Cần cả bản gốc lẫn bản sao đã chỉnh sửa.clone() trước khi truyền
  • Dữ liệu là kiểu nguyên thủy hoặc struct chỉ trên stack → derive hoặc dựa vào Copy
  • Dùng trong move closure → clone giá trị trước khi closure capture

Điều Cần Nhớ

E0382 không phải là trình biên dịch đang cầu kỳ — nó đang phát hiện một lỗi thực sự trước khi code được triển khai. Pattern tương tự trong C++ biên dịch tốt và gây ra use-after-free tại runtime. Rust phát hiện nó tại thời điểm biên dịch, với số dòng và thông báo rõ ràng.

Bắt đầu với tham chiếu. Đổi fn foo(s: String) thành fn foo(s: &str) trước — linh hoạt hơn, bỏ qua việc cấp phát, và giải quyết lỗi trong hầu hết các trường hợp. Chỉ dùng .clone() sau khi đã loại trừ khả năng dùng tham chiếu.

Related Error Notes