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

Lỗi Rust E0507: cannot move out of a value behind a shared reference (move khỏi giá trị mượn)

E0507 nghĩa là bạn cố lấy một giá trị (move nó) ra khỏi thứ mà bạn chỉ đang mượn. Bạn được nhìn một giá trị đi mượn, nhưng không được mang nó đi, vì như thế chủ sở hữu thật sẽ còn lại một lỗ hổng. Đây là lỗi rất hay gặp, nhất là với Vec<String>, trường của struct, và Option. Cách sửa thường là mượn thay vì move, hoặc .clone(). Trang này cho bạn xem các cách sửa trước, rồi mới giải thích. Các lỗi khác trong loạt bài này nằm ở lỗi trình biên dịch Rust.

error[E0507]: cannot move out of `self.name` which is behind a shared reference
--> src/main.rs:7:9
|
7 | self.name // ❌ tries to give away a String that `self` only borrows
| ^^^^^^^^^ move occurs because `self.name` has type `String`, which does not implement the `Copy` trait
|
help: consider cloning the value if the performance cost is acceptable
|
7 | self.name.clone() // ❌ tries to give away a String that `self` only borrows
| ++++++++

Trình biên dịch báo rằng không thể move self.name ra ngoài vì nó nằm sau một tham chiếu dùng chung (shared reference), và gợi ý clone giá trị nếu chấp nhận được chi phí.

Đây là chương trình gây ra lỗi:

struct User {
name: String,
}
impl User {
fn name(&self) -> String {
self.name // ❌ tries to give away a String that `self` only borrows
}
}
fn main() {
let u = User { name: String::from("Ferris") };
println!("{}", u.name());
}

Chú thích ở dòng 7 giữ tiếng Anh vì nó xuất hiện trong thông báo lỗi; nó nói rằng dòng này cố trao đi một String mà self chỉ đang mượn.

Cùng mã lỗi này có nhiều cách diễn đạt. Tất cả đều có nghĩa “bạn chỉ mượn thứ này, nên không thể move nó”:

  • cannot move out of index of `Vec<String>` với let first = names[0];
  • cannot move out of `*r` which is behind a shared reference với let owned = *r; khi r: &String
  • cannot move out of `nick` as enum variant `Some` which is behind a shared reference với match *nick { Some(n) => n, ... }
  • cannot move out of `report`, a captured variable in an `Fn` closure
  • cannot move out of `self.log` which is behind a mutable reference (đúng vậy, ngay cả &mut cũng không cho bạn cứ thế lấy đi)
  • cannot move out of an `Rc` với let s: String = *shared;

Hãy nhìn dòng 7. Phương thức trả về String, một giá trị sở hữu (owned). Vậy self.name sẽ move chuỗi ra khỏi struct và trao nó cho nơi gọi. Nhưng phương thức chỉ có &self, một lần mượn dùng chung (chỉ đọc) của user. Nó không sở hữu user, nên không thể trao tên của user đi.

Phần move occurs because ... does not implement the Copy trait cũng quan trọng. Nếu trường đó là u32, Rust sẽ lặng lẽ sao chép nó và chẳng có lỗi nào. Một String sở hữu vùng nhớ trên heap, nên nó chỉ có thể được move, không thể được sao chép.

Hãy nghĩ tới việc mượn kệ sách của một người bạn trong một buổi chiều. Bạn đọc cuốn nào trên đó cũng được. Nhưng bạn không được mang sách về nhà: kệ sách không phải của bạn, và bạn của bạn sẽ quay về thấy một chỗ trống.

Mỗi giá trị có đúng một chủ sở hữu, và chủ sở hữu dọn dẹp nó khi biến mất (xem Quyền sở hữu). Nếu bạn move được self.name ra khỏi một User đi mượn, user vẫn tưởng mình sở hữu chuỗi đó. Sau này user bị drop, và nó sẽ giải phóng chuỗi lần thứ hai, hoặc ai đó sẽ đọc một trường không còn ở đó nữa. Đó là “use after free” hay “double free”, hai trong số những lỗi tệ nhất trong C và C++.

Vec cũng vậy. names[0] vẫn do vector sở hữu. Nếu let first = names[0]; move nó ra ngoài, vector sẽ có một lỗ hổng ở vị trí 0, và nó sẽ cố giải phóng ô đó thêm lần nữa khi bị drop. Vì vậy truy cập theo chỉ số chỉ cho bạn quyền truy cập, không bao giờ cho quyền sở hữu.

Quy tắc trong một câu: qua một tham chiếu, bạn được đọc và (với &mut) được thay đổi, nhưng không được lấy đi. Nếu muốn quyền sở hữu, bạn phải sở hữu container, hoặc để lại thứ gì đó vào chỗ của giá trị.

Chọn cách này khi bạn chỉ cần đọc giá trị. Đây là cách sửa đúng trong phần lớn trường hợp. Trả về &str thay vì String, viết &v[0] thay vì v[0], và mượn nội dung của Option bằng as_deref() hoặc as_ref():

struct User {
name: String,
nickname: Option<String>,
}
impl User {
fn name(&self) -> &str {
&self.name
}
fn display_name(&self) -> &str {
match self.nickname.as_deref() {
Some(nick) => nick,
None => &self.name,
}
}
}
fn describe(nick: &Option<String>) -> &str {
match nick {
Some(n) => n,
None => "ẩn danh",
}
}
fn main() {
let users = vec![
User { name: String::from("Ferris"), nickname: Some(String::from("cua")) },
User { name: String::from("Corro"), nickname: None },
];
let first = &users[0];
println!("Người dùng đầu tiên: {}", first.name());
for user in &users {
println!("Hiển thị là: {}", user.display_name());
}
println!("Biệt danh: {}", describe(&users[1].nickname));
}

Kết quả:

Người dùng đầu tiên: Ferris
Hiển thị là: cua
Hiển thị là: Corro
Biệt danh: ẩn danh
▶ Tự chạy thử

Hãy nhìn describe: nó so khớp trên nick (tham chiếu), không phải *nick. Khi đó n là một &String, một lần mượn, và không có gì bị move. Viết match *nick { Some(n) => ... } là đòi một String sở hữu trong n, và đó là E0507. Gợi ý của trình biên dịch cũng nói vậy: consider removing the dereference here (hãy bỏ phép giải tham chiếu ở đây).

as_deref() biến một &Option<String> thành Option<&str>; as_ref() cho ra Option<&String>. Hãy dùng chúng trước .unwrap(), .map() hay match, để bạn làm việc với một lần mượn thay vì giá trị sở hữu. *r khi r: &String cũng vậy: thường bạn chẳng cần dấu *, cứ dùng r là được. Xem Option và Mượn.

Chọn cách này khi bạn thật sự cần bản sao của riêng mình, ví dụ để lưu ở chỗ khác trong khi bản gốc vẫn ở yên chỗ cũ. Trình biên dịch gợi ý cách này trong gần như mọi thông báo E0507:

struct User {
name: String,
}
impl User {
fn name(&self) -> String {
self.name.clone()
}
}
fn main() {
let names = vec![String::from("Ferris"), String::from("Corro")];
let first = names[0].clone();
println!("{first} đứng đầu, danh sách vẫn còn {} tên", names.len());
let u = User { name: String::from("Ferris") };
let mut badge = u.name();
badge.push_str(" (quản trị)");
println!("{badge} / {}", u.name);
}

Kết quả:

Ferris đứng đầu, danh sách vẫn còn 2 tên
Ferris (quản trị) / Ferris
▶ Tự chạy thử

Clone một cái tên hay một danh sách ngắn rất rẻ, và hoàn toàn ổn, nhất là khi bạn đang học. Nó chỉ thành vấn đề với dữ liệu lớn trong một vòng lặp chạy liên tục. Nếu mượn (Cách sửa 1) là đủ, hãy ưu tiên cách đó.

Cách sửa 3: Lấy ra và để lại thứ khác

Phần tiêu đề “Cách sửa 3: Lấy ra và để lại thứ khác”

Chọn cách này khi bạn có quyền &mut và muốn thật sự move giá trị ra ngoài, ví dụ để bàn giao một nhật ký đã xong hoặc bước tiếp theo. Bạn không được để lại lỗ hổng, nhưng bạn có thể đổi vào một thứ thay thế:

  • std::mem::take(&mut x) lấy giá trị và để lại giá trị mặc định của nó ("" cho String, rỗng cho Vec).
  • option.take() lấy thứ bên trong một Option và để lại None.
  • std::mem::replace(&mut x, new) đặt new vào và trả lại cho bạn giá trị cũ.
use std::mem;
#[derive(Debug)]
enum Status {
Queued,
Running,
}
struct Job {
log: String,
next_step: Option<String>,
status: Status,
}
impl Job {
fn take_log(&mut self) -> String {
mem::take(&mut self.log)
}
fn start_next(&mut self) -> Option<String> {
self.next_step.take()
}
fn set_status(&mut self, new: Status) -> Status {
mem::replace(&mut self.status, new)
}
}
fn main() {
let mut job = Job {
log: String::from("build thành công"),
next_step: Some(String::from("triển khai")),
status: Status::Queued,
};
let old = job.set_status(Status::Running);
println!("Trạng thái: {old:?} -> {:?}", job.status);
println!("Bước tiếp theo: {:?}", job.start_next());
println!("Bước tiếp theo lần nữa: {:?}", job.start_next());
println!("Nhật ký: {:?}", job.take_log());
println!("Nhật ký bây giờ: {:?}", job.log);
}

Kết quả:

Trạng thái: Queued -> Running
Bước tiếp theo: Some("triển khai")
Bước tiếp theo lần nữa: None
Nhật ký: "build thành công"
Nhật ký bây giờ: ""
▶ Tự chạy thử

Struct không bao giờ bị bỏ lại với một lỗ hổng: sau take_log, nhật ký là chuỗi rỗng; sau start_next, bước tiếp theo là None. Đây là cách sửa chuẩn cho “cannot move out of self.x which is behind a mutable reference”. mem::take cần kiểu đó cài đặt Default; với các kiểu khác, hãy dùng mem::replace.

Cách sửa 4: Sở hữu collection, rồi move ra khỏi nó

Phần tiêu đề “Cách sửa 4: Sở hữu collection, rồi move ra khỏi nó”

Chọn cách này khi bạn đã dùng xong collection hoặc struct và muốn lấy nội dung của nó. Nếu bạn sở hữu Vec, nó có thể trao các phần tử cho bạn: remove(i), swap_remove(i), pop(), hoặc into_iter() cho tất cả. Một phương thức nhận self (không phải &self) sở hữu struct và được phép move các trường của nó ra ngoài:

struct User {
name: String,
}
impl User {
fn into_name(self) -> String {
self.name
}
}
fn main() {
let mut queue = vec![String::from("email"), String::from("sao lưu"), String::from("báo cáo")];
let first = queue.remove(0);
println!("Đang làm {first}, còn lại: {queue:?}");
let users = vec![User { name: String::from("Ferris") }, User { name: String::from("Corro") }];
let names: Vec<String> = users.into_iter().map(|u| u.into_name()).collect();
println!("{names:?}");
}

Kết quả:

Đang làm email, còn lại: ["sao lưu", "báo cáo"]
["Ferris", "Corro"]
▶ Tự chạy thử

into_iter() tiêu thụ vector, nên bạn không thể dùng users sau đó (đó sẽ là E0382). Tiền tố into_ trong into_name là một thói quen đặt tên của Rust: nó cho người đọc biết “hàm này dùng hết giá trị”. remove(0) dịch mọi phần tử khác xuống một bậc; nếu thứ tự không quan trọng, swap_remove nhanh hơn. Xem Xóa phần tử.

Gom các trường trong vòng lặp for qua &users. for u in &users { names.push(u.name); } lỗi với cannot move out of `u.name` which is behind a shared reference. Cách sửa: names.push(u.name.clone()), hoặc lặp bằng for u in users nếu bạn không cần users sau đó nữa, hoặc gom các tham chiếu mượn: users.iter().map(|u| u.name.as_str()).collect::<Vec<&str>>().

Gọi .unwrap() trên một trường Option qua tham chiếu. config.title.unwrap() với config: &Config sẽ lỗi, vì unwrap nhận Option theo giá trị. Trình biên dịch gợi ý cách sửa: config.title.as_ref().unwrap() (cho ra &String), hoặc config.title.as_deref() để có Option<&str>.

Move một biến đã bắt bên trong closure Fn. Một closure có thể được gọi nhiều lần thì không thể trao đi thứ nó đã bắt: cannot move out of `report`, a captured variable in an `Fn` closure. Cách sửa: mượn bên trong closure (&report), clone bên trong nó, hoặc nếu nó chỉ chạy một lần, đổi ràng buộc thành FnOnce. Xem Fn, FnMut và FnOnce.

Giải tham chiếu một Rc. let s: String = *shared; cho ra cannot move out of an `Rc`, vì các chủ sở hữu khác có thể vẫn đang dùng giá trị đó. Cách sửa: mượn nó (&*shared hoặc chỉ &shared), clone nó, hoặc dùng Rc::unwrap_or_clone(shared). Box thì khác: bạn sở hữu nó, nên let s = *boxed; chạy được. Xem Con trỏ thông minh.

  • E0382: dùng một giá trị đã bị move. Bạn được phép move, nhưng lại dùng giá trị đó lần nữa sau đó.
  • E0594: không thể gán qua một tham chiếu &. Cùng quy tắc “bạn chỉ mượn thứ này”, nhưng cho việc ghi thay vì move.
  • E0596: cannot borrow as mutable, ví dụ khi dùng mem::take(&mut self.x) trong một phương thức chỉ có &self.
  • E0308: mismatched types, expected String, found &String, thường xuất hiện ngay sau khi chuyển sang mượn. Hãy trả về &str, hoặc gọi .clone().
  • E0515: cannot return a reference to a local variable, khi một cách sửa dựa trên mượn lại trỏ vào dữ liệu sắp bị drop.
  • 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. let first = names[0]; với names: Vec<String> gây E0507. Nếu bạn chỉ in first, cách sửa tốt nhất thường là gì?

2. Phương thức fn next(&mut self) -> Option<Task> cần trao đi self.pending (một Option<Task>). Cách nào biên dịch được?

3. Vì sao let n = nums[0]; ổn với nums: Vec<i32>, nhưng không ổn với Vec<String>?

Điểm: 0 / 3