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.)
Thông báo lỗi
Phần tiêu đề “Thông báo lỗi”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"));}Ý nghĩa
Phần tiêu đề “Ý nghĩa”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.
Vì sao Rust làm vậy
Phần tiêu đề “Vì sao Rust làm vậy”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:
- Mỗi tham số là tham chiếu có lifetime riêng của nó.
- Nếu có đúng một tham số là tham chiếu, tham chiếu trả về sẽ mượn từ tham số đó.
- Nếu là phương thức có
&selfhoặ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.
Cách sửa
Phần tiêu đề “Cách sửa”Cách sửa 1: Thêm một lifetime có tên ('a)
Phần tiêu đề “Cách sửa 1: Thêm một lifetime có tên ('a)”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: appleHai 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!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 parameterVớ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)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 IshmaelCách sửa 4: Dùng 'static cho chuỗi literal
Phần tiêu đề “Cách sửa 4: Dùng 'static cho chuỗi literal”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 fromNế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à OK404 nghĩa là Not Found(“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).
Thường gặp ở đâu
Phần tiêu đề “Thường gặp ở đâu”- 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ớimapbằ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ườngString(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.
Lỗi liên quan
Phần tiêu đề “Lỗi liên quan”- E0382: borrow of moved value: một lỗi khác về quyền sở hữu, khi dùng một giá trị sau khi đã trao nó đi.
- E0502: mutable borrow while immutably borrowed: borrow checker làm việc sau khi lifetime của bạn đã đâu vào đấy.
- E0499: two mutable borrows: hai
&muttới cùng một dữ liệu cùng lúc. - E0308: mismatched types: ví dụ, trả về
Stringtừ một hàm ghi là&str. E0515: cannot return reference to local variable: bạn thử cách sửa 1 với dữ liệu được tạo bên trong hàm. Hãy trả về mộtStringsở hữu thay vào đó (cách sửa 2).- 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. 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
Đọc thêm
Phần tiêu đề “Đọc thêm”- E0106 — Rust error codes index (tiếng Anh): phần giải thích chính thức của lỗi này.
- Validating References with Lifetimes — The Rust Book (tiếng Anh): toàn bộ câu chuyện về lifetime, gồm cả ví dụ
longestvà struct chứa tham chiếu. - Lifetime elision — The Rust Reference (tiếng Anh): chính xác các quy tắc Rust dùng để tự điền lifetime cho bạn.
- Elision — Rust by Example (tiếng Anh): các ví dụ ngắn về khi nào bạn có thể bỏ lifetime đi.
- Lifetimes — Rust by Example (tiếng Anh): các ví dụ chạy được về lifetime tường minh trong hàm, phương thức và struct.