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

Lỗi Rust E0106: missing lifetime specifier (thiếu lifetime)

Lỗi E0106 nghĩa là: bạn viết một tham chiếu (&) trong kiểu trả về của hàm hoặc trong một struct, và Rust không biết nó mượn từ đâu. Đây không phải lỗi trong logic của bạn. Trình biên dịch chỉ cần bạn nói thêm một từ, hoặc thay đổi kế hoạch một chút. Gần như ai cũng gặp lỗi này trong tuần đầu tiên, và cách sửa thường chỉ là một dòng. (Trang này thuộc loạt bài Giải thích các lỗi trình biên dịch Rust.)

error[E0106]: missing lifetime specifier
--> src/main.rs:1:32
|
1 | fn longer(a: &str, b: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
|
1 | fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
| ++++ ++ ++ ++

Thông báo của trình biên dịch luôn bằng tiếng Anh: “missing lifetime specifier” nghĩa là “thiếu chỉ định lifetime”, và phần help nói rằng chữ ký hàm không cho biết giá trị trả về mượn từ a hay từ b. Đây là chương trình gây ra lỗi đó:

fn longer(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
println!("{}", longer("apple", "fig"));
}

Tham chiếu là một lần mượn: nó trỏ tới dữ liệu do người khác sở hữu. Mỗi khi một hàm trả về tham chiếu, Rust cần biết nó trỏ vào dữ liệu của ai, để kiểm tra rằng dữ liệu đó còn sống trong lúc bên gọi dùng kết quả.

Hãy nhìn ba dấu bên dưới dòng 1. Trình biên dịch gạch dưới a và b bằng ----, và kiểu trả về bằng ^. Nó đang nói: “bạn trả về một &str đi mượn, và nó có thể đến từ a hoặc từ b. Là cái nào?” Nó sẽ không đoán.

Hãy nghĩ đến thẻ thư viện. Nếu một người bạn đưa bạn một cuốn sách và nói “sách mượn đấy”, bạn muốn biết mượn của ai, để biết khi nào phải trả. Lifetime (vòng đời) chính là cái nhãn đó trên một lần mượn.

Rust đảm bảo rằng một tham chiếu không bao giờ sống lâu hơn dữ liệu mà nó trỏ tới (không có “con trỏ lơ lửng”, dangling pointer). Để kiểm tra điều đó cho một lời gọi hàm, trình biên dịch chỉ nhìn vào chữ ký (signature) của hàm, không nhìn vào thân hàm. Vì vậy chính chữ ký phải nói rõ tham chiếu được trả về đến từ đâu.

Phần lớn thời gian bạn không bao giờ phải viết lifetime, vì Rust tự điền vào bằng ba quy tắc lược bỏ (elision rules) đơn giản:

  1. Mỗi tham số là tham chiếu có lifetime riêng của nó.
  2. Nếu có đúng một tham số là tham chiếu, tham chiếu trả về sẽ mượn từ tham số đó.
  3. Nếu là phương thức có &self hoặc &mut self, tham chiếu trả về mượn từ self.

E0106 xuất hiện khi không quy tắc nào cho ra câu trả lời: có hai tham số tham chiếu trở lên (và không có self), không có tham số tham chiếu nào, hoặc một tham chiếu được lưu trong struct. Bài Lifetime giải thích kỹ chuyện này, còn Mượn giải thích bản thân tham chiếu.

Chọn cách này khi hàm thật sự trả về một phần của một trong các đầu vào. 'a chỉ là một cái tên. Viết nó lên các đầu vào và đầu ra nghĩa là “kết quả sống lâu bằng các đầu vào này”:

// 'a: &str trả về mượn từ a hoặc b, nên nó sống lâu bằng cả hai.
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let first = String::from("apple");
let second = String::from("fig");
println!("Từ dài hơn: {}", longer(&first, &second));
}

Kết quả:

Từ dài hơn: apple
▶ Tự chạy thử

Hai từ “apple” và “fig” được giữ bằng tiếng Anh vì hàm 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 kết quả chỉ bao giờ đến từ một tham số, hãy chỉ đặt 'a lên tham số đó: fn lookup<'a>(map: &'a HashMap<String, String>, key: &str) -> Option<&'a str>. Gợi ý của trình biên dịch đặt 'a lên mọi thứ, cách đó chạy được nhưng chặt hơn mức cần thiết.

Cách sửa 2: Trả về một String có chủ sở hữu

Phần tiêu đề “Cách sửa 2: Trả về một String có chủ sở hữu”

Chọn cách này khi hàm tạo ra văn bản mới, hoặc khi bạn không muốn bên gọi bị ràng buộc vào một lần mượn. Một String sở hữu thuộc về bên gọi, nên không còn gì phải theo dõi:

fn greeting(name: &str, lang: &str) -> String {
if lang == "vi" {
format!("Xin chào, {name}!")
} else {
format!("Hello, {name}!")
}
}
fn main() {
println!("{}", greeting("Ana", "vi"));
println!("{}", greeting("Sam", "en"));
}

Kết quả:

Xin chào, Ana!
Hello, Sam!
▶ Tự chạy thử

Cách này tốn một lần cấp phát bộ nhớ nhỏ. Với phần lớn chương trình, điều đó hoàn toàn ổn. Đừng ngại.

Cách sửa 3: Lưu dữ liệu có chủ sở hữu trong struct

Phần tiêu đề “Cách sửa 3: Lưu dữ liệu có chủ sở hữu trong struct”

Một trường của struct như title: &str cũng gây ra lỗi này:

error[E0106]: missing lifetime specifier
--> src/main.rs:2:12
|
2 | title: &str,
| ^ expected named lifetime parameter

Với struct, cách sửa tốt nhất gần như luôn là sở hữu dữ liệu: dùng String thay cho &str, và Vec<T> thay cho &[T]. Khi đó struct có thể sống lâu bao nhiêu tùy bạn:

#[derive(Debug)]
struct Book {
title: String, // sở hữu, không phải &str
pages: u32,
}
fn main() {
let book = Book { title: String::from("Dune"), pages: 412 };
println!("{} ({} trang)", book.title, book.pages);
}

Kết quả:

Dune (412 trang)
▶ Tự chạy thử

Chỉ cho struct một lifetime khi nó là một góc nhìn ngắn hạn vào dữ liệu nằm ở nơi khác, như một parser đang đọc một đoạn văn bản lớn. Khi đó bạn viết <'a> lên struct và lên trường:

struct Excerpt<'a> {
text: &'a str, // mượn từ cuốn tiểu thuyết bên dưới
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first = novel.split('.').next().unwrap();
let excerpt = Excerpt { text: first };
println!("Trích đoạn: {}", excerpt.text);
}

Kết quả:

Trích đoạn: Call me Ishmael
▶ Tự chạy thử

Khi một hàm không có tham số tham chiếu nào, lỗi sẽ nói “there is no value for it to be borrowed from” (không có giá trị nào để mượn từ đó):

error[E0106]: missing lifetime specifier
--> src/main.rs:1:22
|
1 | fn default_name() -> &str {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

Nếu bạn chỉ trả về các chuỗi literal như "OK", hãy viết &'static str. Literal được “nướng” sẵn vào chương trình, nên chúng sống suốt thời gian chương trình chạy:

fn status_text(code: u16) -> &'static str {
match code {
200 => "OK",
404 => "Not Found",
_ => "Unknown",
}
}
fn main() {
println!("200 nghĩa là {}", status_text(200));
println!("404 nghĩa là {}", status_text(404));
}

Kết quả:

200 nghĩa là OK
404 nghĩa là Not Found
▶ Tự chạy thử

(“OK” và “Not Found” là tên chuẩn của mã trạng thái HTTP, nên được giữ nguyên.) Chỉ dùng 'static cho literal và hằng số thật sự. Nếu văn bản được tạo lúc chạy (bằng format!, hoặc đọc từ file), 'static không giúp được gì: hãy trả về String (cách sửa 2).

  • Hàm trợ giúp có hai đầu vào, như fn lookup(map: &HashMap<String, String>, key: &str) -> Option<&str>. Cách sửa: chỉ gắn đầu ra với map bằng 'a (cách sửa 1).
  • Struct chứa văn bản, như một User { name: &str } được tạo từ dữ liệu đã phân tích. Cách sửa: dùng trường String (cách sửa 3).
  • Hàm trả về thông điệp cố định, như fn error_label() -> &str. Cách sửa: &'static str (cách sửa 4).

Phương thức thì là tin vui: fn name(&self) -> &str không cần lifetime nào cả, nhờ quy tắc 3.

Kiểm tra nhanh

1. Vì sao fn first_word(s: &str) -> &str biên dịch được mà không cần lifetime nào?

2. Struct Config { path: &str } của bạn gây ra E0106. Thường thì cách sửa tốt nhất là gì?

3. Một hàm tạo thông điệp bằng format! rồi trả về. Nó nên có kiểu trả về nào?

Điểm: 0 / 3