Lỗi Rust E0515: cannot return reference to local variable (trả về tham chiếu tới biến cục bộ)
E0515 nghĩa là: hàm của bạn cố trả về một tham chiếu (&) tới thứ mà chính nó tạo ra. Mọi thứ một hàm tạo ra đều bị dọn dẹp khi hàm trả về, nên tham chiếu đó sẽ trỏ vào hư không. Gần như mọi người mới học Rust đều gặp lỗi này, và cách sửa thường chỉ là bỏ một dấu & rồi trả về chính giá trị đó. (Trang này thuộc giải thích các lỗi trình biên dịch Rust.)
Thông báo lỗi
Phần tiêu đề “Thông báo lỗi”error[E0515]: cannot return reference to local variable `greeting` --> src/main.rs:3:5 |3 | &greeting // ❌ greeting is dropped when we return | ^^^^^^^^^ returns a reference to data owned by the current functionTrình biên dịch báo rằng hàm đang trả về một tham chiếu tới greeting, một biến cục bộ mà chính hàm sở hữu.
Đây là chương trình gây ra lỗi:
fn make_greeting(name: &str) -> &str { let greeting = format!("Hello, {name}!"); // a new String, owned by this function &greeting // ❌ greeting is dropped when we return}
fn main() { let text = make_greeting("Ferris"); println!("{text}");}Các chú thích giữ tiếng Anh vì dòng 3 xuất hiện nguyên văn trong thông báo lỗi. Chúng nói: greeting là một String mới do hàm này sở hữu, và nó bị drop khi hàm trả về.
Bạn sẽ thấy vài cách diễn đạt khác nhau của cùng lỗi này. Tất cả đều cùng một ý:
cannot return reference to local variablekhi bạn viết&greeting.cannot return value referencing local variablekhi bạn gọi một phương thức có mượn, nhưgreeting.as_str()hoặc&scores[2..].cannot return value referencing temporary valuekhi dữ liệu chưa bao giờ được lưu vào biến, nhưtext.to_uppercase().as_str().
Phiên bản nào cũng có nhãn “returns a value referencing data owned by the current function” (trả về một giá trị tham chiếu tới dữ liệu do hàm hiện tại sở hữu).
Ý nghĩa
Phần tiêu đề “Ý nghĩa”greeting là một biến cục bộ: nó được tạo bên trong make_greeting và do hàm đó sở hữu. Khi hàm trả về, mọi biến cục bộ đều bị drop. &str mà bạn trả về sẽ trỏ vào một String không còn tồn tại.
Giống như bạn dọn ra khỏi căn hộ rồi đưa chìa khóa cho bạn mình. Chìa khóa là thật, nhưng căn hộ giờ trống không, hoặc người khác đang ở. Bạn của bạn cần đồ đạc, chứ không phải chìa khóa.
Một cách hay dẫn tới lỗi này: lúc đầu bạn trả về greeting (một String) trong một hàm khai báo trả về &str, gặp E0308 (mismatched types), và trình biên dịch gợi ý “consider borrowing here: &greeting”. Gợi ý đó sửa được kiểu nhưng dẫn thẳng vào E0515. Cách sửa thật sự là đổi kiểu trả về.
Vì sao Rust làm vậy
Phần tiêu đề “Vì sao Rust làm vậy”Một tham chiếu không bao giờ được sống lâu hơn giá trị nó trỏ tới. Trả về tham chiếu tới một biến cục bộ sẽ tạo ra một tham chiếu treo (dangling reference), một con trỏ tới vùng nhớ đã giải phóng. Trong C và C++, code như vậy biên dịch được rồi crash (hoặc tệ hơn, âm thầm trả về dữ liệu rác). Rust bắt lỗi này ngay lúc biên dịch.
Một tham chiếu được trả về phải mượn từ thứ gì đó sống bên ngoài hàm: một tham số do nơi gọi truyền vào, một trường của self, hoặc một giá trị 'static như chuỗi literal. Biến cục bộ của hàm không đủ điều kiện. Đó là lý do chú thích lifetime không sửa được E0515: 'a chỉ mô tả dữ liệu sống bao lâu, nó không thể làm một biến cục bộ sống lâu hơn. Xem Quyền sở hữu và Mượn.
Cách sửa
Phần tiêu đề “Cách sửa”Cách sửa 1: Trả về giá trị sở hữu
Phần tiêu đề “Cách sửa 1: Trả về giá trị sở hữu”Chọn cách này khi hàm tạo ra dữ liệu. Đây là câu trả lời đúng trong phần lớn trường hợp. Trả về String thay vì &str (hoặc Vec<T> thay vì &[T]) và bỏ dấu &. Giá trị được move ra cho nơi gọi, và nơi gọi sẽ sở hữu nó:
fn make_greeting(name: &str) -> String { let greeting = format!("Xin chào, {name}!"); greeting // ✅ trao cả String cho nơi gọi}
fn top_scores() -> Vec<u32> { let mut scores = vec![42, 97, 63, 88]; scores.sort(); scores.split_off(2) // hai điểm cao nhất, dưới dạng một Vec mới}
fn main() { let text = make_greeting("Ferris"); println!("{text}"); println!("{:?}", top_scores());}Kết quả:
Xin chào, Ferris![88, 97]Đừng lo về chi phí: trả về một String hay Vec chỉ move một con trỏ nhỏ, độ dài và dung lượng. Bản thân nội dung văn bản không bị sao chép. Bạn cũng có thể viết luôn format!("Xin chào, {name}!") ở dòng cuối.
Cách sửa 2: Nhận dữ liệu qua tham số và trả về tham chiếu vào đó
Phần tiêu đề “Cách sửa 2: Nhận dữ liệu qua tham số và trả về tham chiếu vào đó”Chọn cách này khi nơi gọi đã sở hữu dữ liệu và bạn chỉ muốn trỏ vào một phần của nó. Nếu hàm làm việc trên một tham số được mượn, tham chiếu trả về sẽ mượn từ tham số đó, vốn sống bên ngoài hàm:
fn top_two(scores: &mut [u32]) -> &[u32] { scores.sort(); &scores[scores.len() - 2..] // ✅ trỏ vào dữ liệu của nơi gọi}
fn main() { let mut scores = vec![42, 97, 63, 88]; // do main sở hữu let best = top_two(&mut scores); println!("{best:?}");}Kết quả:
[88, 97]Với một tham số tham chiếu, Rust tự điền lifetime cho bạn. Với hai tham số trở lên, bạn có thể cần 'a (xem E0106). Cách này chỉ dùng được nếu bạn trả về một phần của đầu vào. Một giá trị hoàn toàn mới được tạo từ đầu vào (to_uppercase(), format!, trim().to_string()) lại là biến cục bộ, nên hãy quay về Cách sửa 1. Xem Slice.
Cách sửa 3: Trả về &'static str cho chuỗi literal
Phần tiêu đề “Cách sửa 3: Trả về &'static str cho chuỗi literal”Chọn cách này khi mọi kết quả có thể có đều là văn bản cố định viết sẵn trong code. Chuỗi literal được lưu trong chính chương trình và sống mãi mãi, nên trả về tham chiếu tới nó luôn an toàn:
fn status_text(code: u16) -> &'static str { match code { 200 => "OK", 404 => "Not Found", 500 => "Internal Server Error", _ => "Unknown", }}
fn main() { for code in [200, 404, 418] { println!("{code}: {}", status_text(code)); }}Kết quả:
200: OK404: Not Found418: UnknownCác thông điệp trạng thái HTTP được giữ nguyên tiếng Anh, như chúng xuất hiện trên web.
Đừng cố “sửa” kết quả của format! bằng cách viết &'static str trong chữ ký hàm. Bạn sẽ chỉ gặp lại E0515, vì String vẫn là biến cục bộ.
Cách sửa 4: Dùng Cow khi lúc thì mượn, lúc thì tạo mới
Phần tiêu đề “Cách sửa 4: Dùng Cow khi lúc thì mượn, lúc thì tạo mới”Chọn cách này khi hàm thường trả lại đầu vào y nguyên, nhưng đôi khi phải tạo một chuỗi mới. Cow (clone-on-write) chứa hoặc một &str được mượn, hoặc một String sở hữu, nên bạn chỉ cấp phát bộ nhớ khi thật cần:
use std::borrow::Cow;
fn safe_file_name(name: &str) -> Cow<'_, str> { if name.contains(' ') { Cow::Owned(name.replace(' ', "_")) // chỉ tạo String mới khi cần } else { Cow::Borrowed(name) // không sao chép gì cả }}
fn main() { for name in ["report.txt", "my notes.txt"] { let clean = safe_file_name(name); let kind = match clean { Cow::Borrowed(_) => "mượn", Cow::Owned(_) => "sở hữu", }; println!("{clean} ({kind})"); }}Kết quả:
report.txt (mượn)my_notes.txt (sở hữu)Cow là một cách tối ưu hóa. Nếu bạn không chắc mình cần nó, hãy trả về String (Cách sửa 1). Đọc thêm ở Weak và Cow.
Thường gặp ở đâu
Phần tiêu đề “Thường gặp ở đâu”Trả về s.as_str() của một String cục bộ. greeting.as_str() mượn greeting, nên nó lỗi y như &greeting, với câu chữ “cannot return value referencing local variable”. Cách sửa: trả về greeting và cho kiểu trả về là String.
Một .map() tạo ra chuỗi mới. Trông thì vô hại, nhưng mỗi lần to_lowercase() tạo một String tạm thời bị drop ngay sau .as_str():
fn tags(input: &str) -> Vec<&str> { input.split(',').map(|t| t.trim().to_lowercase().as_str()).collect() // ❌ cannot return value referencing temporary value}Cách sửa: trả về Vec<String> và bỏ .as_str(): .map(|t| t.trim().to_lowercase()).collect(). (Nếu không có to_lowercase(), t.trim() là một slice của input, nên Vec<&str> vẫn chạy được. Xem Iterator.)
Một phương thức định dạng một trường. fn full_name(&self) -> &str { &format!("{} {}", self.first, self.last) } cho ra “cannot return reference to temporary value”. Cách sửa: -> String. Trả về &self.first thì ổn, vì dữ liệu đó sống trong struct, không phải trong phương thức. Xem Struct.
Một hàm không có tham số tham chiếu nào. fn make_greeting() -> &String lỗi sớm hơn, với E0106, vì chẳng có gì để nó mượn. Cách sửa vẫn vậy: trả về String.
Lỗi liên quan
Phần tiêu đề “Lỗi liên quan”- E0106: missing lifetime specifier: thường xuất hiện trước; thêm
'asẽ không sửa được một tham chiếu tới biến cục bộ. - E0597: borrowed value does not live long enough: cùng vấn đề nhưng bên trong một hàm, với một khối hoặc vòng lặp thay vì lệnh trả về.
- E0308: mismatched types: trả về
Stringở chỗ đã khai báo&str. Hãy đổi kiểu trả về, đừng thêm&. - E0382: borrow of moved value: sau khi chuyển sang giá trị sở hữu, hãy cẩn thận đừng dùng một giá trị sau khi đã move nó.
E0716: temporary value dropped while borrowed: tham chiếu tới một giá trị tạm thời được dùng về sau trong cùng hàm.- Giải thích tất cả lỗi trình biên dịch
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Một hàm tạo thông điệp bằng format! rồi trả về &message. Cách sửa đúng thường là gì?
2. Hàm nào biên dịch được?
3. Khi nào Cow<str> là kiểu trả về tốt?
Điểm: 0 / 3
Đọc thêm
Phần tiêu đề “Đọc thêm”- E0515 — Rust error codes index (tiếng Anh): giải thích chính thức của lỗi này.
- Dangling References — The Rust Book (tiếng Anh): ví dụ
danglevà vì sao câu trả lời là trả về thẳngString. - Validating References with Lifetimes — The Rust Book (tiếng Anh): vì sao một tham chiếu được trả về phải gắn với một trong các tham số.
- The Slice Type — The Rust Book (tiếng Anh): trả về slice
&strmượn từ đầu vào, phiên bản an toàn của Cách sửa 2. Cow— tài liệu thư viện chuẩn (tiếng Anh): kiểu clone-on-write ở Cách sửa 4, kèm ví dụ.