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

Lỗi Rust E0597: borrowed value does not live long enough (sống không đủ lâu)

E0597 nghĩa là: bạn giữ một tham chiếu (&x) lâu hơn chính giá trị x. Giá trị bị dọn dẹp ở cuối khối (block) của nó, nhưng có thứ gì đó bên ngoài khối vẫn muốn dùng tham chiếu. Đây là lỗi rất phổ biến, nhất là với khối lồng nhau, vòng lặp và struct, và cách sửa thường chỉ là dời một dòng code hoặc lưu một giá trị sở hữu (owned) thay vì một tham chiếu. (Trang này thuộc giải thích các lỗi trình biên dịch Rust.)

error[E0597]: `x` does not live long enough
--> src/main.rs:5:13
|
4 | let x = 5;
| - binding `x` declared here
5 | r = &x; // r borrows x
| ^^ borrowed value does not live long enough
6 | } // x is dropped here
| - `x` dropped here while still borrowed
7 | println!("r: {r}"); // ❌ r would point at nothing
| - borrow later used here

Trình biên dịch nói rằng x “không sống đủ lâu”: x bị hủy ở cuối khối trong khi r vẫn còn mượn nó và còn được dùng ở dòng 7.

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

fn main() {
let r;
{
let x = 5;
r = &x; // r borrows x
} // x is dropped here
println!("r: {r}"); // ❌ r would point at nothing
}

Các chú thích trong code giữ tiếng Anh vì chúng xuất hiện nguyên văn trong thông báo lỗi: r mượn x, x bị hủy ở cuối khối, và r sẽ trỏ vào hư không.

Bốn nhãn kể lại câu chuyện theo thứ tự:

  1. Dòng 4: x được tạo bên trong khối lồng { ... }.
  2. Dòng 5: r mượn x. Đến đây vẫn ổn.
  3. Dòng 6: khối kết thúc, nên x bị drop (dọn dẹp). Lần mượn vẫn còn sống.
  4. Dòng 7: r được dùng, nhưng giá trị nó trỏ tới đã biến mất.

Hãy tưởng tượng bạn ghi lại số phòng khách sạn của một người bạn. Số đó hữu ích khi bạn ấy còn ở đó. Khi bạn ấy trả phòng, con số vẫn còn trên tờ giấy của bạn, nhưng giờ nó dẫn tới một căn phòng trống, hoặc phòng của người lạ. Rust không cho bạn giữ một tờ giấy dẫn tới căn phòng trống.

Bạn cũng sẽ thấy nhãn ngắn “borrowed value does not live long enough” (giá trị được mượn không sống đủ lâu). Đó vẫn là lỗi này; dòng tiêu đề chỉ ghi thêm tên biến.

Một tham chiếu không bao giờ được sống lâu hơn giá trị nó trỏ tới. Nếu có, bạn sẽ có một tham chiếu treo (dangling reference): một con trỏ tới vùng nhớ đã được giải phóng và giờ có thể chứa thứ khác. Trong C và C++, đó là nguồn gốc kinh điển của crash và lỗ hổng bảo mật (“use after free”).

Mọi giá trị trong Rust bị drop khi chủ sở hữu của nó ra khỏi phạm vi (scope), thường là ở dấu } đóng khối nơi nó được khai báo. Borrow checker so sánh hai khoảng: giá trị sống bao lâu và tham chiếu được dùng bao lâu. Nếu tham chiếu được dùng sau dấu } của giá trị, bạn gặp E0597. Xem Phạm vi, Mượn và Lifetime.

Để ý điều trình biên dịch không phàn nàn: việc mượn x ở dòng 5. Vấn đề chỉ nằm ở lần dùng sau đó ở dòng 7. Bỏ dòng cuối đi là chương trình biên dịch được. Đó là gợi ý hữu ích cho các cách sửa bên dưới: hoặc làm giá trị sống lâu hơn, hoặc làm cho việc dùng tham chiếu kết thúc sớm hơn.

Cách sửa 1: Đưa giá trị ra phạm vi bên ngoài

Phần tiêu đề “Cách sửa 1: Đưa giá trị ra phạm vi bên ngoài”

Chọn cách này khi giá trị thật sự cần tồn tại lâu bằng tham chiếu. Đây là cách sửa phổ biến nhất: khai báo giá trị trước (bên ngoài) khối, để nó sống ít nhất lâu bằng tham chiếu.

fn main() {
let scores = vec![72, 95, 88]; // giờ sống đến hết main
let best;
{
best = scores.iter().max().unwrap();
}
println!("Điểm cao nhất: {best}"); // ✅ scores vẫn còn sống
}

Kết quả:

Điểm cao nhất: 95
▶ Tự chạy thử

Quy tắc dễ nhớ: chủ sở hữu phải được khai báo trước bên mượn và sống lâu hơn nó. Nếu bạn thấy một giá trị được tạo bên trong một khối hoặc vòng lặp, còn tham chiếu tới nó lại được dùng bên ngoài, hãy đưa giá trị ra ngoài.

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

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

Chọn cách này khi bạn gom các giá trị được tạo bên trong vòng lặp. Mỗi lượt lặp tạo một String mới và drop nó ở cuối lượt, nên một Vec<&str> bên ngoài vòng lặp không thể giữ tham chiếu tới chúng:

fn main() {
let mut names: Vec<&str> = Vec::new();
for i in 1..=3 {
let name = format!("player{i}");
names.push(&name); // ❌ `name` does not live long enough
}
println!("{names:?}");
}

Cách sửa là để Vec sở hữu các chuỗi. Đổi Vec<&str> thành Vec<String> và đẩy chính String vào (thao tác này move nó vào vector, nên nó sống lâu bằng vector):

fn main() {
let mut names: Vec<String> = Vec::new();
for i in 1..=3 {
let name = format!("player{i}");
names.push(name); // move String vào Vec
}
println!("{names:?}");
}

Kết quả:

["player1", "player2", "player3"]
▶ Tự chạy thử

Ý tưởng này cũng áp dụng cho struct và HashMap: nếu dữ liệu được tạo trong một phạm vi nhỏ hơn container, hãy lưu String (hoặc Vec<T>) thay vì &str (hoặc &[T]). Với người mới, trường sở hữu (owned field) gần như luôn là lựa chọn đúng. Xem Chuỗi và Vector.

Chọn cách này khi bạn vẫn cần dùng tiếp bản gốc, hoặc bạn chỉ có một tham chiếu mượn tới nó. Một struct chứa tham chiếu (Label<'a> { text: &'a str }) không thể sống lâu hơn chuỗi nó trỏ tới. Hãy cho struct một trường String sở hữu và lưu một bản sao:

struct Label {
text: String, // sở hữu, không phải &str
}
fn main() {
let label;
{
let title = String::from("Hộp thư đến");
label = Label { text: title.clone() };
println!("Bên trong khối: {title}");
}
println!("Nhãn: {}", label.text); // ✅ label sở hữu bản sao của riêng nó
}

Kết quả:

Bên trong khối: Hộp thư đến
Nhãn: Hộp thư đến
▶ Tự chạy thử

Nếu sau đó bạn không cần title nữa, bỏ clone và move nó luôn: Label { text: title }. Clone một chuỗi ngắn rất rẻ và hoàn toàn ổn. Nếu bạn chỉ có &str, dùng .to_string() để có một bản sao sở hữu.

Cách sửa 4: Sắp xếp lại để lần mượn kết thúc kịp lúc

Phần tiêu đề “Cách sửa 4: Sắp xếp lại để lần mượn kết thúc kịp lúc”

Chọn cách này khi giá trị vốn chỉ cần sống ngắn. Thay vì làm giá trị sống lâu hơn, hãy dùng tham chiếu khi giá trị vẫn còn sống. Trường hợp này hay gặp với các hàm trả về tham chiếu, như ví dụ longest kinh điển:

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let first = String::from("Ferris the crab");
{
let second = String::from("Corro");
let winner = longest(&first, &second);
println!("Tên dài nhất: {winner}"); // ✅ dùng trước khi second bị drop
}
}

Kết quả:

Tên dài nhất: Ferris the crab
▶ Tự chạy thử

Hai cái tên được giữ tiếng Anh vì longest so sánh độ dài theo byte (.len()), mà chữ tiếng Việt có dấu chiếm nhiều byte hơn.

Nếu winner được khai báo bên ngoài khối và in ra sau khối, bạn sẽ gặp E0597 cho second, dù first tình cờ dài hơn. Chữ ký hàm nói kết quả có thể mượn từ bất kỳ đối số nào, nên cả hai phải sống lâu hơn kết quả. Nếu bạn thật sự cần kết quả về sau, hãy lưu nó dưới dạng sở hữu: let winner = longest(&first, &second).to_string();.

Một closure được lưu bên ngoài khối. Closure dùng name sẽ mượn nó. Nếu closure sống lâu hơn name, bạn gặp E0597 kèm một nhãn phụ, value captured here (giá trị được bắt ở đây):

let say_hello;
{
let name = String::from("Ferris");
say_hello = || println!("Hello, {name}!"); // ❌ `name` does not live long enough
}
say_hello();

Cách sửa: viết move || ... để closure lấy quyền sở hữu name. Xem Closure.

Luồng (thread). Mượn một biến cục bộ bên trong thread::spawn(|| ...) cho ra một lỗi khác, E0373 (“closure may outlive the current function”), vì luồng có thể chạy lâu hơn main. Cách sửa: thread::spawn(move || ...), chia sẻ bằng Arc, hoặc dùng std::thread::scope. Xem Đồng thời.

Tham chiếu tới một giá trị tạm thời. let label = Label { text: String::from("Inbox").as_str() }; gần giống E0597 nhưng hiện ra dưới dạng E0716, “temporary value dropped while borrowed”: String không bao giờ được lưu vào biến, nên nó bị drop ở cuối câu lệnh. Cách sửa: đưa nó vào một biến trước (let title = String::from("Inbox");), hoặc cho trường đó là String.

Trả về tham chiếu từ một hàm. Nếu giá trị được tạo bên trong hàm, bạn sẽ gặp E0515 thay vì lỗi này. Hãy trả về giá trị sở hữu.

Kiểm tra nhanh

1. Trong let r; { let x = 5; r = &x; } rồi một println! in r, điều gì thật sự gây ra E0597?

2. Mỗi lượt lặp bạn tạo một String và đẩy &name vào một Vec<&str>. Cách sửa thường dùng là gì?

3. Closure của một luồng mượn một Vec cục bộ. Bạn gặp lỗi nào?

Điểm: 0 / 3