Mục lục
- Mục Tiêu Bài Học
- Cú Pháp 'name — Tick + Lowercase Identifier
- Lifetime Trên Reference Type — &'a str, &'a mut T
- Lifetime Trong Generic Parameter List
- Multiple Lifetime — <'a, 'b>
- Annotation KHÔNG Tạo Lifetime Mới
- Lifetime Là Generic Parameter
- Lifetime Trong Struct, Enum, Impl
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Đọc và viết được cú pháp lifetime annotation: tick + lowercase identifier, ví dụ
'a,'b,'src,'log,'static. - Đặt lifetime đúng vị trí trên reference type:
&'a str,&'a mut T— lifetime đứng trước inner type, sau dấu&/&mut. - Declare lifetime trong generic parameter list của function:
fn foo<'a>(x: &'a str) -> &'a str— phải xuất hiện trong<>trước khi được dùng ở argument hoặc return. - Mở rộng sang nhiều lifetime:
fn parse<'a, 'b>(src: &'a str, log: &'b mut Logger), mỗi ref có thể bind một lifetime khác. - Hiểu rõ điều dễ nhầm nhất: annotation không tạo lifetime mới và không kéo dài tuổi thọ value — chỉ mô tả relationship để compiler kiểm chứng.
- Nhìn nhận lifetime như một generic parameter đồng hạng với type parameter
<T>, có thể bound (sẽ học ở B185), có thể default. - Đặt lifetime trên struct/enum/impl khi type chứa reference:
struct Parser<'a> { source: &'a str }đi cùngimpl<'a> Parser<'a>.
Bài này thuần về cú pháp. Semantic chi tiết — elision rules, multiple lifetime tương quan với output, 'static, subtyping, bound — sẽ đi sâu ở các bài 178 đến 185.
Cú Pháp 'name — Tick + Lowercase Identifier
Một lifetime annotation trong Rust luôn có dạng 'name — dấu nháy đơn (tick) đi liền với một identifier viết thường, không khoảng trắng:
'a // hợp lệ
'b // hợp lệ
'src // hợp lệ — descriptive name
'log // hợp lệ
'static // lifetime đặc biệt: sống suốt chương trình (B181)
'A // KHÔNG hợp lệ — phải viết thường
'1a // KHÔNG hợp lệ — phải bắt đầu bằng chữ
' a // KHÔNG hợp lệ — không có khoảng trắng giữa tick và tên
Convention cộng đồng: dùng tên ngắn 'a, 'b, 'c khi function chỉ có 1–2 reference và mối quan hệ rõ. Khi nhiều reference với vai trò khác nhau, dùng descriptive name như 'src, 'dst, 'arena, 'session để code đọc rõ nghĩa hơn. Rust 2024 edition và clippy đều khuyến nghị tên descriptive khi có từ ba lifetime trở lên.
Có một lifetime đặc biệt là 'static — built-in, không phải declare. Nghĩa: reference này valid suốt vòng đời chương trình. String literal "hello" có type &'static str. Bài 181 sẽ đi sâu, ở đây chỉ cần nhớ 'static không phải tên tự đặt — nó là từ khoá lifetime.
Lifetime Trên Reference Type — &'a str, &'a mut T
Lifetime chỉ có ý nghĩa khi gắn vào một reference. Cú pháp: lifetime đặt trước inner type, ngay sau dấu & hoặc &mut:
&str // reference không annotate — compiler infer
&'a str // shared reference với lifetime 'a
&'a mut T // mutable reference với lifetime 'a
&'static str // reference với lifetime 'static
// SAI
&str 'a // không đúng vị trí
&mut 'a T // không đúng — tick phải đứng giữa & và mut/type
&<'a> str // không có ngoặc nhọn ở vị trí này
Vị trí cố định này khiến parser của Rust phân biệt rõ type với type-with-lifetime. Lưu ý &mut phải đi liền — lifetime chèn vào giữa: viết là &'a mut T, không phải &mut 'a T. Cùng nguyên tắc cho slice: &'a [u8], &'a mut [u8]; cho trait object: &'a dyn Trait.
Hai annotation thường gặp nhất bạn sẽ viết hằng ngày là &'a str (string slice mượn) và &'a mut T (mutable borrow generic). Nhìn vào type bất kỳ trong code Rust, bất cứ khi nào thấy ký hiệu ', bạn đang nhìn vào một lifetime annotation.
Lifetime Trong Generic Parameter List
Khi viết function dùng lifetime, bạn không thể chỉ rải 'a vào argument — phải declare trước trong <> giống như generic type parameter:
// Declare 'a trong <>, rồi dùng cho cả x và return type.
fn foo<'a>(x: &'a str) -> &'a str {
x
}
// SAI: dùng 'a chưa declare.
// error[E0261]: use of undeclared lifetime name `'a`
fn bar(x: &'a str) -> &'a str { x }
Quy tắc đọc: mọi tên lifetime đứng trong type signature đều phải xuất hiện trong generic parameter list của item gần nhất. Với function, generic list nằm ngay sau tên hàm trước dấu ngoặc tròn. Với struct/enum/impl/trait — tương tự, đứng sau tên type.
Thứ tự bên trong <> theo convention: lifetime trước, type parameter sau, const parameter cuối:
fn merge<'a, T: Clone>(a: &'a [T], b: &'a [T]) -> Vec<T> {
let mut out = a.to_vec();
out.extend_from_slice(b);
out
}
Compiler không bắt buộc thứ tự, nhưng style guide chính thức và rustfmt đều enforce 'a, T. Đặt ngược (T, 'a) compile vẫn pass nhưng review sẽ bị flag.
Multiple Lifetime — <'a, 'b>
Một function có thể nhận nhiều reference với vòng đời không liên quan. Khi đó declare nhiều lifetime, mỗi reference bind một tên:
struct Logger;
impl Logger { fn log(&mut self, _msg: &str) {} }
struct Token<'a>(&'a str);
// 'src cho input source; 'log cho mutable borrow Logger.
// Output gắn với 'src vì Token mượn từ src.
fn parse<'src, 'log>(
src: &'src str,
log: &'log mut Logger,
) -> Token<'src> {
log.log("parsing");
Token(&src[..0])
}
Hai tham số ở đây độc lập về vòng đời: src có thể là string sống lâu, còn log chỉ mượn ngắn trong scope hiện tại. Nếu dùng chung một 'a cho cả hai, compiler sẽ ép cả hai cùng có lifetime ngắn nhất trong hai — quá hạn chế. Tách 'src và 'log cho mỗi ref tự do bind scope riêng. Bài 182 sẽ đi sâu vào multiple lifetime — ở đây bạn chỉ cần nắm cú pháp.
Nguyên tắc: declare nhiều lifetime khi và chỉ khi các reference thực sự độc lập. Nếu mọi reference cùng nguồn (hai slice cùng một Vec), một lifetime là đủ. Lifetime thừa không làm code an toàn hơn — chỉ thêm nhiễu.
Annotation KHÔNG Tạo Lifetime Mới
Đây là điểm sai lầm phổ biến nhất khi mới học lifetime. Người mới thường nghĩ viết 'a thì compiler sẽ "kéo dài" reference cho đủ — sai. Annotation chỉ là metadata mô tả relationship giữa các reference; nó không thay đổi vòng đời thực tế của bất kỳ value nào.
fn dangling<'a>() -> &'a str {
let s = String::from("hello");
// error[E0515]: cannot return reference to local variable `s`
&s
}
Dù bạn đặt 'a cho return, biến s vẫn drop khi hàm kết thúc. Compiler không biến nó thành 'static, không kéo dài scope — nó chỉ kiểm tra annotation có khớp với reality không. Reality: s chết tại }, không thể trả ref được — reject.
Phép so sánh: lifetime annotation như contract bạn ký với compiler. "Tôi cam đoan reference này sống ít nhất bằng 'a." Compiler nhận hợp đồng và đối chiếu với code: nếu code thực sự đáp ứng → pass; nếu không → reject. Compiler không phải runtime, không thể "fix" data lifetime bằng cách viết annotation đẹp hơn.
Hệ quả thực tiễn: khi gặp lỗi does not live long enough, đừng cố sửa bằng cách thêm 'static hay đổi tên lifetime — phải sửa kiến trúc để data thực sự sống đủ lâu (clone, move ownership, lưu vào struct outer scope...).
Lifetime Là Generic Parameter
Lifetime đứng trong cùng <> với type parameter không phải ngẫu nhiên — về bản chất, lifetime là một loại generic parameter. Chúng cùng họ với <T>: monomorphize, bound, default đều áp dụng được. Khác biệt duy nhất là kind: type parameter nhận type, lifetime parameter nhận một scope (vùng code).
// Generic full hình: 2 lifetime + 2 type + 1 const.
fn complex<'a, 'b, T: Clone, U, const N: usize>(
src: &'a [T; N],
extra: &'b U,
) -> Vec<T> {
let _ = extra;
src.to_vec()
}
Vì lifetime là generic parameter, nó có thể bound giống T: Display. Ví dụ 'a: 'b nghĩa "lifetime 'a sống ít nhất bằng 'b" (subtyping — B183), hoặc T: 'a nghĩa "type T không chứa reference sống ngắn hơn 'a" (lifetime bound trên generic — B185). Hai cấu trúc này dùng cùng cơ chế bound với syntax T: Trait, chỉ thay trait bằng lifetime.
Hệ quả khác: function call mang lifetime cũng monomorphize per call-site, compiler infer scope cụ thể tại mỗi nơi gọi. Không có runtime cost cho lifetime annotation — toàn bộ check xảy ra tại compile time, binary sinh ra không khác gì code không có lifetime.
Lifetime Trong Struct, Enum, Impl
Struct hoặc enum chứa reference field bắt buộc phải declare lifetime trên type, và lặp lại ở mọi impl block:
// Declare 'a trên struct, dùng cho field source.
struct Parser<'a> {
source: &'a str,
pos: usize,
}
// impl phải declare 'a sau impl, rồi dùng cho Parser<'a>.
impl<'a> Parser<'a> {
fn new(source: &'a str) -> Self {
Parser { source, pos: 0 }
}
fn rest(&self) -> &'a str {
&self.source[self.pos..]
}
}
enum Cow<'a, T: Clone> {
Borrowed(&'a T),
Owned(T),
}
Quy tắc đọc đơn giản: nếu struct/enum mượn data thay vì sở hữu, mỗi reference field yêu cầu một lifetime; lifetime phải declare ở type và lặp lại ở impl. Đừng quên dòng impl<'a> Parser<'a> — viết impl Parser không có <'a> sẽ lỗi missing lifetime specifier.
Khi nào nên đưa reference vào struct? Khi struct là view tạm thời của data sở hữu ở nơi khác (parser cầm slice của input, iterator cầm slice của container). Khi nào không nên? Khi struct cần sống lâu, di chuyển nhiều, hoặc lưu trong collection — lúc đó nên dùng owned data (String, Vec<T>) hoặc smart pointer (Arc<T>). Bài 179 sẽ đi sâu vào lifetime trong struct cùng trade-off này.
Còn một cú pháp tiện lợi là anonymous lifetime '_, dùng khi vị trí lifetime rõ ràng nhưng tên không cần thiết (ví dụ impl Parser<'_>). Sẽ học chi tiết ở B184; ở đây chỉ cần biết có cú pháp này.
Tổng Kết
- Lifetime annotation: tick + lowercase identifier (
'a,'b,'src,'log);'staticlà lifetime built-in. - Trên reference type, lifetime đứng trước inner type:
&'a str,&'a mut T,&'a dyn Trait. - Trong function/struct/enum/impl, lifetime phải declare trong generic parameter list
<>trước khi dùng — convention thứ tự: lifetime → type → const. - Nhiều reference độc lập: declare nhiều lifetime
<'a, 'b>; nếu cùng nguồn thì một lifetime đủ. - Annotation không tạo lifetime mới và không kéo dài tuổi thọ value — chỉ là contract để compiler kiểm chứng relationship.
- Lifetime là generic parameter cùng họ với
<T>; có thể bound ('a: 'b,T: 'a), default; không có runtime cost. - Struct/enum mượn data declare lifetime ở type và lặp lại ở mọi impl:
impl<'a> Parser<'a>.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Đánh giá tính hợp lệ cú pháp: (a)
&'A str, (b)&mut 'a T, (c)&'a mut T, (d)&'src [u8]. Cái nào hợp lệ, cái nào sai và sai chỗ nào? - Viết function
first_wordnhận&strvà trả slice của từ đầu tiên. Declare lifetime đúng cách và giải thích vì sao return slice gắn với lifetime của input. - Tại sao code này không compile, và sửa thế nào (không dùng
Clone)?fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } } - Viết struct
BookViewchứa hai fieldtitle: &strvàauthor: &strđều mượn từ cùng một source. Một lifetime đủ hay cần hai? Giải thích. - Cho function:
Hỏi: nếu gọifn pick<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str { x }pick(&a, &b)vớiasống dài,bsống ngắn, return reference có sống được đến hết scope củaakhông? Nếu đổi'a, 'bthành một'acho cả hai thì sao?
Đáp án
- (a) sai — lifetime phải lowercase. (b) sai — vị trí tick: phải
&'a mut T, không&mut 'a T. (c) đúng. (d) đúng — descriptive name lowercase. fn first_word<'a>(s: &'a str) -> &'a str { let b = s.as_bytes(); for (i, &c) in b.iter().enumerate() { if c == b' ' { return &s[..i]; } } s }. Return là slice bên trong input, không sở hữu data riêng — vòng đời phải gắn với'acủa input, vì khi input drop thì slice cũng dangling.- Lỗi
missing lifetime specifier: compiler không biết return bind vớixhayynên không elide được. Sửa:fn longest<'a>(x: &'a str, y: &'a str) -> &'a str— ép cả hai input cùng một lifetime'a, và return cũng'a. Compiler sẽ chọn lifetime là phần giao của hai input scope. - Một lifetime đủ nếu hai field thực sự mượn từ cùng source:
struct BookView<'a> { title: &'a str, author: &'a str }. Cần hai lifetime chỉ khi hai field mượn từ hai source độc lập và bạn muốn cho phép vòng đời khác nhau. - Có — return là
&'a str, gắn vớiasống dài; reference trả về sống được đến hết scope củaa, không bị ràng buộc bởib. Nếu hợp nhất thành một'acho cả hai, compiler ép'abằng phần giao — return sẽ bị giới hạn theo cái sống ngắn hơn (b), kém linh hoạt hơn dù code có vẻ "đơn giản".
Bài Tiếp Theo
Bài 178: Lifetime Trong Function Signature — đi sâu vào ý nghĩa của fn longest<'a>(x: &'a str, y: &'a str) -> &'a str: vì sao compiler không tự suy được, cách lifetime của return liên kết với lifetime của input, và những ví dụ ban đầu fail compile rồi pass sau khi thêm 'a — chuyển từ cú pháp sang semantic của lifetime trong function.
