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

Lỗi Rust E0716: temporary value dropped while borrowed (giá trị tạm bị hủy khi đang mượn)

E0716 nghĩa là: bạn mượn từ một giá trị không có tên biến, rồi tiếp tục dùng lần mượn đó ở một dòng sau. Rust vứt những giá trị không tên như vậy ở cuối câu lệnh, nên lần mượn sẽ trỏ vào hư không. Trông thì đáng sợ, nhưng đây là một trong những lỗi mượn dễ sửa nhất: thường bạn chỉ cần tách một dòng thành hai. (Trang này thuộc giải thích các lỗi trình biên dịch Rust.)

error[E0716]: temporary value dropped while borrowed
--> src/main.rs:2:13
|
2 | let s = String::from("hi").as_str(); // s borrows a String that has no name
| ^^^^^^^^^^^^^^^^^^ - temporary value is freed at the end of this statement
| |
| creates a temporary value which is freed while still in use
3 | println!("{s}"); // ❌ that String is already gone
| - borrow later used here
|
help: consider using a `let` binding to create a longer lived value
|
2 ~ let binding = String::from("hi");
3 ~ let s = binding.as_str(); // s borrows a String that has no name
|

Trình biên dịch báo rằng một giá trị tạm bị giải phóng ở cuối câu lệnh trong khi vẫn đang được mượn, và gợi ý dùng let để tạo một giá trị sống lâu hơn.

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

fn main() {
let s = String::from("hi").as_str(); // s borrows a String that has no name
println!("{s}"); // ❌ that String is already gone
}

Các chú thích giữ tiếng Anh vì chúng xuất hiện trong thông báo lỗi: s mượn một String không có tên, và tới dòng 3 thì String đó đã biến mất.

Một giá trị tạm (temporary) là giá trị được tạo ra giữa một biểu thức nhưng không bao giờ được lưu vào biến. Ở dòng trên, String::from("hi") tạo ra một String thật, nhưng không ai sở hữu nó: không có let tên = ... nào cho nó cả. Rust drop (dọn dẹp) một giá trị tạm ở cuối câu lệnh, tức là ở dấu ;.

Đọc ba nhãn từ trái sang phải:

  1. ^^^^ dưới String::from("hi"): ở đây một String tạm được tạo ra.
  2. - ở cuối dòng: ở đây, tại dấu ;, String đó được giải phóng.
  3. - dưới s ở dòng 3: nhưng s (một &str trỏ vào String đó) vẫn được dùng ở đây.

Hãy nghĩ tới một người phục vụ ghi món bạn gọi lên một tờ giấy nhớ rồi vứt tờ giấy ngay khi bước đi. Nếu bạn bảo một người bạn “món đã gọi ghi trên tờ giấy đó”, bạn ấy sẽ chẳng tìm thấy gì. Bạn phải tự giữ tờ giấy, hoặc ghi món ra chỗ nào đó còn lại lâu dài.

Phần help: của thông báo thường chính là câu trả lời: tạo một biến (rustc gọi nó là binding) để giá trị có một chủ sở hữu sống lâu hơn.

Đây là cùng quy tắc với E0597: một tham chiếu không bao giờ được sống lâu hơn giá trị nó trỏ tới, nếu không nó sẽ thành một tham chiếu treo trỏ vào vùng nhớ đã giải phóng. Khác biệt duy nhất là trong E0597 giá trị có tên và chết ở một dấu }, còn trong E0716 giá trị không có tên và chết ở dấu ;.

Dùng một giá trị tạm bên trong cùng câu lệnh thì hoàn toàn ổn. println!("{}", String::from("hi").as_str()); biên dịch được, vì lần mượn kết thúc trước dấu ;. Lỗi chỉ xuất hiện khi lần mượn vẫn còn cần ở một dòng sau. Xem Khi nào một lần mượn kết thúc? và Lifetime.

Có một ngoại lệ hữu ích, gọi là kéo dài vòng đời giá trị tạm (temporary lifetime extension). Nếu bạn đặt & (hoặc &mut) ngay trước giá trị tạm trong một let, Rust sẽ lặng lẽ giữ giá trị tạm sống lâu bằng biến. Đó là lý do những dòng sau biên dịch được:

struct Label<'a> {
text: &'a str,
}
fn make_title() -> String {
String::from("Hộp thư đến")
}
fn main() {
let greeting = &String::from("chào"); // ✅ được kéo dài: & nằm ngay trong let
let queue = &mut vec![1, 2, 3]; // ✅ được kéo dài
queue.push(4);
let label = Label { text: &make_title() }; // ✅ được kéo dài: & nằm ngay trong struct literal
println!("{greeting} {queue:?} {}", label.text);
}

Kết quả:

chào [1, 2, 3, 4] Hộp thư đến
▶ Tự chạy thử

Việc kéo dài này không đi xuyên qua một lời gọi phương thức hay lời gọi hàm. String::from("hi").as_str(), make_title().as_str(), vec![1, 2].iter() và shout(&make_title()) đều mượn giá trị tạm bên trong một lời gọi, nên giá trị tạm vẫn chết ở dấu ; và bạn gặp E0716. Đừng cố nhớ hết mọi trường hợp: nếu thấy E0716, hãy đặt tên cho giá trị.

Cách sửa 1: Đặt tên cho giá trị tạm bằng let

Phần tiêu đề “Cách sửa 1: Đặt tên cho giá trị tạm bằng let”

Chọn cách này gần như mọi lúc. Đó là điều trình biên dịch gợi ý, và là câu trả lời đúng cho phần lớn lỗi E0716. Hãy lưu giá trị vào một biến riêng trước, rồi mượn từ biến đó ở dòng tiếp theo.

Một trường hợp rất hay gặp là tách một dòng nhập liệu. read_command() trả về một String mới, .trim() và .split(' ') mượn từ nó, và Vec<&str> giữ các lần mượn đó:

let parts: Vec<&str> = read_command().trim().split(' ').collect(); // ❌ E0716

Rust chỉ vào read_command() và nói nó bị giải phóng ở cuối câu lệnh. Hãy đặt tên cho nó:

fn read_command() -> String {
// Trong chương trình thật, hàm này sẽ đọc một dòng từ stdin.
String::from(" add milk eggs \n")
}
fn main() {
let line = read_command(); // giờ String sống đến hết main
let parts: Vec<&str> = line.trim().split(' ').collect();
println!("{parts:?}");
println!("lệnh: {}, số món: {}", parts[0], parts.len() - 1);
}

Kết quả:

["add", "milk", "eggs"]
lệnh: add, số món: 2
▶ Tự chạy thử

Dòng lệnh add milk eggs giữ tiếng Anh vì đó là một câu lệnh mẫu mà chương trình đọc vào.

Bạn cũng sẽ gặp đúng điều này với dữ liệu nhập thật: lines.next().unwrap().unwrap().trim().split(' ').collect() trên io::stdin().lines() gây E0716 vì cùng lý do. Hãy đặt let line = lines.next().unwrap().unwrap(); trên một dòng riêng trước. Xem Chuỗi.

Cách sửa 2: Giữ giá trị sở hữu thay vì tham chiếu

Phần tiêu đề “Cách sửa 2: Giữ giá trị sở hữu thay vì tham chiếu”

Chọn cách này khi lần mượn cần sống lâu hơn bất kỳ biến nào bạn có thể tạo, ví dụ trong một struct bạn truyền đi khắp nơi, hoặc trong một Vec bạn đổ dữ liệu vào bên trong vòng lặp. Một let bên trong vòng lặp sẽ bị drop ở cuối mỗi lượt, và chỉ biến E0716 thành E0597.

Đây là hai phiên bản bị lỗi. Một struct giữ &str từ một giá trị tạm, và một vòng lặp đẩy vào các tham chiếu tới chuỗi mới:

let label = Label { text: make_title().as_str() }; // ❌ E0716
let mut names: Vec<&str> = Vec::new();
for name in ["ferris", "corro"] {
names.push(name.to_uppercase().as_str()); // ❌ E0716
}

Cách sửa là để struct và Vec sở hữu các chuỗi của chúng. Dùng String thay vì &str, và bỏ .as_str():

struct Label {
text: String, // sở hữu, nên không cần lifetime
}
fn make_title() -> String {
String::from("Hộp thư đến")
}
fn main() {
let label = Label { text: make_title() }; // String được move vào Label
println!("Nhãn: {}", label.text);
let mut names: Vec<String> = Vec::new();
for name in ["ferris", "corro"] {
names.push(name.to_uppercase()); // Vec sở hữu từng String mới
}
println!("{names:?}");
}

Kết quả:

Nhãn: Hộp thư đến
["FERRIS", "CORRO"]
▶ Tự chạy thử

Với người mới, trường sở hữu (String, Vec<T>) gần như luôn là lựa chọn dễ nhất. Một struct có trường &str cần lifetime và không bao giờ sống lâu hơn dữ liệu nó mượn. Xem Struct chứa tham chiếu và Struct.

Cách sửa 3: Sắp xếp lại chuỗi lời gọi

Phần tiêu đề “Cách sửa 3: Sắp xếp lại chuỗi lời gọi”

Chọn cách này khi giá trị tạm là điểm bắt đầu của một chuỗi iterator. Những dòng này tạo một Vec bằng vec![...], mượn nó bằng .iter(), và giữ iterator (hoặc kết quả của nó) để dùng sau:

let doubled = &mut vec![1, 2, 3].iter().map(|n| n * 10); // ❌ E0716
let best = vec![72, 95, 88].iter().max(); // ❌ E0716

Dấu &mut phía trước không giúp được gì: việc kéo dài vòng đời giữ iterator sống, nhưng không giữ cái Vec mà .iter() mượn. Có hai lối thoát:

  • Dùng into_iter() để iterator sở hữu Vec (rustc cũng gợi ý cách này). Khi đó bạn cũng không cần &mut nữa, chỉ cần let mut.
  • Kết thúc chuỗi bằng một kết quả sở hữu, như .copied(), .cloned(), .sum() hoặc .collect::<Vec<_>>() gồm các số thuần, để không còn gì mượn giá trị tạm sau dấu ;.
fn main() {
// Iterator sở hữu Vec, nên không có gì mượn một giá trị tạm
let mut doubled = vec![1, 2, 3].into_iter().map(|n| n * 10);
println!("{:?}", doubled.next());
println!("{:?}", doubled.next());
// .copied() biến Option<&i32> thành Option<i32>, một giá trị không mượn gì
let best = vec![72, 95, 88].iter().max().copied();
println!("Cao nhất: {best:?}");
}

Kết quả:

Some(10)
Some(20)
Cao nhất: Some(95)
▶ Tự chạy thử

Nếu sau này bạn còn cần Vec, hãy quay về Cách sửa 1: let scores = vec![72, 95, 88]; rồi scores.iter().max(). Xem Iterator.

Khóa một Mutex giữa chuỗi lời gọi. let first = names.lock().unwrap().first(); gây E0716, vì guard của khóa là một giá trị tạm. Khi nó bị drop, khóa được nhả ra, nên first không thể tiếp tục trỏ vào bên trong. Cách sửa: let guard = names.lock().unwrap(); rồi guard.first(), hoặc sao chép dữ liệu ra ngay một lần bằng .first().cloned(). Xem Trạng thái dùng chung.

Truyền &giá_trị_tạm vào một hàm trả về tham chiếu. Với fn shout(s: &str) -> &str, dòng let t = shout(&make_title()); gây E0716: &str trả về mượn String tạm, và việc kéo dài vòng đời không đi xuyên qua lời gọi hàm. Cách sửa: let title = make_title(); rồi shout(&title).

Trả nó về từ một hàm. Nếu bạn viết make_title().as_str() ở dòng cuối của một hàm trả về tham chiếu (như -> &'static str), bạn sẽ gặp E0515 (“cannot return value referencing temporary value”) thay vì lỗi này. Cách sửa: trả về String.

Kiểm tra nhanh

1. Trong Rust, “giá trị tạm” (temporary) là gì?

2. let s = String::from("hi").as_str(); rồi println!("{s}"); gây E0716. Cách sửa thường dùng là gì?

3. Dòng nào biên dịch được, nhờ việc kéo dài vòng đời giá trị tạm?

Điểm: 0 / 3