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

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.)

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 function

Trì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 variable khi bạn viết &greeting.
  • cannot return value referencing local variable khi 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 value khi 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).

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ề.

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.

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]
▶ Tự chạy thử

Đừ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]
▶ Tự chạy thử

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: OK
404: Not Found
418: Unknown
▶ Tự chạy thử

Cá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)
▶ Tự chạy thử

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.

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.

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