Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu chính xác cú pháp
'a: 'bđọc là gì và mô tả quan hệ subtype như thế nào giữa hai lifetime. - Biết viết bound
<'a, 'b: 'a>trên function / struct / impl giống cách viếtT: Trait. - Hiểu variance covariant trên
&'a T: lifetime dài coerce được sang lifetime ngắn — nền tảng để compiler "nới" lifetime khi gọi function. - Biết hai use case điển hình: struct có hai field reference với lifetime khác nhau cần outlive lẫn nhau, và closure trả về reference liên kết với input.
- Đọc được error
E0491("reference has a longer lifetime than the data it references") và biết khi nào cần thêm bound để fix. - Biết khi nào compiler tự infer được, khi nào phải explicit — tránh thêm bound thừa làm API rối.
'a: 'b Là Gì
Cú pháp 'a: 'b đọc là "'a outlives 'b" — region 'a sống ít nhất bằng region 'b (có thể bằng hoặc dài hơn, không bao giờ ngắn hơn). Tương đương phát biểu: mọi thời điểm chương trình mà 'b còn alive thì 'a cũng phải còn alive.
Từ quan hệ outlives này sinh ra lifetime subtype: nếu 'a: 'b thì 'a là subtype của 'b. Hệ quả thực tế: ở nơi cần một reference với lifetime 'b, bạn có thể dùng reference với lifetime 'a — vì 'a "to" hơn nên đáp ứng được mọi yêu cầu của 'b.
Trực giác về kích thước region rất quan trọng để không nhầm. 'static là region lớn nhất (sống toàn chương trình) nên 'static: 'a đúng với mọi 'a; ngược lại 'a: 'static chỉ đúng khi 'a cũng 'static. Region nhỏ là region của một block ngắn; nó bị "chứa" trong region lớn hơn bao quanh.
Tên gọi subtype đến từ lý thuyết kiểu: cũng giống Dog là subtype của Animal trong OOP (mọi Animal nhận được Dog), ở Rust mọi &'b T nhận được &'a T khi 'a: 'b. Sự khác biệt là Rust chỉ áp dụng subtype cho lifetime, không cho type — type system của Rust là nominal, không có struct subtype theo nghĩa OOP.
Cú Pháp Bound
Outlives bound viết y hệt trait bound nhưng vế phải là lifetime thay vì trait. Đặt trong cặp angle bracket khai báo generic parameter của fn, struct, impl, hoặc trong where clause.
// Bound trên function: 'b outlive 'a
fn pick<'a, 'b: 'a>(short: &'a str, long: &'b str) -> &'a str {
if short.len() > long.len() { short } else { long }
// long: &'b str coerce thành &'a str được vì 'b: 'a
}
// Bound trên struct
struct Pair<'a, 'b: 'a> {
short: &'a str,
long: &'b str,
}
// Bound trong where clause cho rõ ràng
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where
'b: 'a,
{
if x.len() > y.len() { x } else { y }
}
Đối chiếu với trait bound quen thuộc: T: Display nói "T phải implement Display"; 'b: 'a nói "lifetime 'b phải outlive 'a". Cùng vị trí cú pháp, cùng cơ chế kiểm tra ở compile time. Có thể nối nhiều bound bằng +: 'b: 'a + 'static nghĩa là 'b outlive cả 'a lẫn 'static — thực tế bound thứ hai khiến 'b bắt buộc 'static.
Variance Trên Reference
Type &'a T là covariant theo 'a: nếu 'a: 'b thì &'a T là subtype của &'b T. Hệ quả thực hành: reference có lifetime dài luôn coerce được thành reference có lifetime ngắn, không bao giờ ngược lại.
fn takes_short<'b>(_r: &'b str) {}
fn demo<'a, 'b>(long_ref: &'a str)
where
'a: 'b,
{
// OK: 'a outlive 'b nên &'a str coerce thành &'b str
takes_short::<'b>(long_ref);
}
Trực giác: hàm yêu cầu ref "ít nhất sống đến 'b"; bạn đưa cho nó ref sống đến 'a (dài hơn) thì dư dả, hợp lệ. Ngược lại đưa ref ngắn cho chỗ cần ref dài là cấm — sẽ tạo dangling khi ref ngắn chết trước.
Variance không phải lúc nào cũng covariant: &'a mut T covariant theo 'a nhưng invariant theo T; fn(&'a T) contravariant theo 'a. Trong bài này chỉ cần nhớ luật chính cho &'a T: dài → ngắn được, ngắn → dài cấm.
Use Case: Struct Field Outlive
Khi struct có hai field reference với lifetime khác nhau và một field giữ pointer trỏ vào dữ liệu của field kia, cần bound để compiler biết field nguồn phải sống lâu hơn field đích.
// 'b outlive 'a: data x trỏ tới phải sống lâu hơn slice y
struct Foo<'a, 'b: 'a> {
x: &'b str, // owner data, sống lâu
y: &'a str, // slice / view của x, sống ngắn hơn
}
fn make<'a, 'b: 'a>(src: &'b str) -> Foo<'a, 'b> {
let head: &'a str = &src[..3];
Foo { x: src, y: head }
}
Nếu bỏ bound 'b: 'a, compiler không biết quan hệ giữa 'a và 'b nên không dám cho phép struct tồn tại — về lý thuyết y có thể tham chiếu vào data của x, và nếu x chết trước y thì y dangling. Bound đảm bảo invariant đó không xảy ra.
Thực tế từ Rust 2018 trở đi, compiler đã tự thêm bound này cho nhiều trường hợp đơn giản (struct chỉ chứa &'a T với inferred outlives). Bạn vẫn nên viết explicit khi struct phức tạp để API tự document và để lỗi compile ở dòng định nghĩa thay vì ở chỗ sử dụng xa lắc.
Use Case: Closure Return Reference
Closure trả về reference rất hay gặp khi viết iterator adapter, builder, parser. Lifetime của ref output phải liên kết được với lifetime của ref input — đôi khi cần outlives bound để compiler chấp nhận.
fn make_finder<'a, 'b: 'a>(needle: &'a str)
-> impl Fn(&'b str) -> Option<&'b str> + 'a
{
move |haystack| {
haystack.find(needle).map(|i| &haystack[i..i + needle.len()])
}
}
Closure capture needle: &'a str và trả về slice của haystack: &'b str. Bound 'b: 'a đảm bảo haystack đủ sống để output ref vẫn dùng được trong region 'a của needle. Không có bound, compiler không tìm được lifetime chung phù hợp cho cả hai input và sẽ từ chối closure.
Trong nhiều case đơn giản hơn (chỉ một lifetime, không cross-reference giữa các input), elision rule tự ghép — không cần outlives explicit. Chỉ khi đụng "closure capture ref + return ref khác" hoặc struct chứa nhiều ref liên đới thì bound mới phát huy tác dụng.
Compile Error E0491 Khi Vi Phạm
Khi bạn khai báo struct/type mà compiler không thể chứng minh được ref bên trong sống đủ lâu so với type bao ngoài, rustc phát ra error[E0491]: in type ..., reference has a longer lifetime than the data it references.
// Thiếu bound 'b: 'a — y giữ ref vào region của x nhưng compiler
// không biết 'b có outlive 'a hay không.
struct Bad<'a, 'b> {
x: &'a &'b str,
}
// error[E0491]: in type `&'a &'b str`, reference has a longer
// lifetime than the data it references
// note: the pointer is valid for the lifetime 'a as defined ...
// note: but the referenced data is only valid for the lifetime 'b ...
Cách đọc error: rustc đang nói "ref 'a tới một thứ chứa ref 'b; muốn an toàn thì 'b phải outlive 'a". Fix bằng thêm 'b: 'a ở khai báo generic:
struct Good<'a, 'b: 'a> {
x: &'a &'b str,
}
Error này còn xuất hiện trong impl block, trait object có lifetime, và HRTB (higher-ranked trait bound). Nguyên tắc xử lý không đổi: xác định ref nào nằm bên trong ref nào, thêm outlives để ref ngoài không trỏ vào dữ liệu chết sớm hơn.
Trade-off Giữa Explicit Bound Và Inference
Subtyping cho phép thiết kế API flexible: caller pass ref lifetime dài hơn mức cần thiết vẫn được chấp nhận, không phải khớp chính xác. Đồng thời compiler infer outlives trong rất nhiều trường hợp — đặc biệt với struct đơn giản và function elision.
- Ưu tiên inference: viết struct, fn với lifetime tối thiểu. Nếu compile pass thì không thêm bound. Ít noise, dễ đọc.
- Thêm explicit khi: compiler báo
E0491hoặcborrowed value does not live long enoughtại chỗ sử dụng xa định nghĩa; khi API public cần document rõ quan hệ outlives cho người dùng; khi struct có hai ref liên đới mà inference không lo nổi. - Tránh over-constrain: thêm bound chặt hơn cần thiết khiến caller bị từ chối trong các case lẽ ra hợp lệ. Ví dụ ép
'b: 'statickhi thực tế chỉ cần'b: 'asẽ chỉ chấp nhận literal hoặcBox::leak. - Đặt trong where clause khi có 2+ bound: dễ đọc hơn nhồi tất cả vào trong angle bracket. Đặc biệt với generic
<T, U, 'a, 'b>đủ thứ trait + outlives.
Quy tắc thực hành: viết code không bound trước, để compiler báo lỗi rồi mới thêm chính xác cái cần. Tránh sao chép bound "phòng thủ" từ codebase khác vì có thể không hợp ngữ cảnh và làm tăng cognitive load.
Tổng Kết
'a: 'bđọc là "'a outlives 'b" — region'asống ít nhất bằng'b;'atrở thành subtype của'b.- Cú pháp bound
<'a, 'b: 'a>hoặc trongwhere 'b: 'a— tương tự cách viết trait boundT: Trait. - Reference
&'a Tcovariant theo'a: ref lifetime dài coerce được sang ref lifetime ngắn, không bao giờ ngược lại. - Use case 1: struct chứa hai field ref mà một field giữ data của field kia — cần
'b: 'ađể tránh dangling. - Use case 2: closure capture ref và return ref khác — outlives bound liên kết hai lifetime ở input và output.
- Error
E0491"reference has a longer lifetime than the data it references" báo khi struct/type thiếu outlives bound — fix bằng thêm'b: 'aở generic. - Compiler infer được nhiều case; chỉ explicit khi cần thiết. Tránh over-constrain (ví dụ ép
'statickhông cần).
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Đọc bound
'a: 'bbằng tiếng Việt. Lifetime nào sống lâu hơn? Cho ví dụ'staticcó quan hệ thế nào với mọi'a? - Function
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a strkhông có bound. Có compile được không? Vì sao? Cần thêm bound nào để compile? - Giải thích vì sao
&'a Tcovariant theo'agiúp closure pass ref'staticvào hàm yêu cầu&'a T. - Cho struct
struct Foo<'a, 'b> { x: &'a &'b str }. Compiler báo lỗi gì? Sửa thế nào? - Khi nào nên viết outlives bound explicit và khi nào nên để compiler infer?
Đáp án
'a: 'bđọc "'a outlives 'b" —'asống ít nhất bằng'b, tức'adài hơn hoặc bằng'b.'staticlà region lớn nhất nên'static: 'ađúng với mọi'a; ngược lại'a: 'staticchỉ đúng khi'achính là'static.- Compile không được nếu trả về
ynhánh nào đó: compiler không biết'bcó outlive'ahay không nên không cho cast&'b strthành&'a str. Thêmwhere 'b: 'ađể bound'boutlive'a; lúc đóycoerce hợp lệ. - Covariant nghĩa là khi
'static: 'a(luôn đúng), thì&'static Tlà subtype của&'a T. Mọi nơi nhận&'a Tđều nhận được&'static T— đó là lý do literal"hello"(type&'static str) pass được vào hàm yêu cầu&'a strbất kỳ. - Lỗi
error[E0491]: in type&'a &'b strreference has a longer lifetime than the data it references. Sửa bằng thêm'b: 'a:struct Foo<'a, 'b: 'a> { x: &'a &'b str }. - Explicit khi: compiler báo
E0491hoặc lỗi lifetime ở chỗ sử dụng, khi viết API public cần document quan hệ outlives, khi struct có ref liên đới mà inference không lo nổi. Để inference khi: code đơn giản đã compile pass, function chỉ một lifetime, struct chỉ chứa một loại ref — thêm bound thừa làm noise và over-constrain.
Bài Tiếp Theo
Bài 184: '_ — Anonymous Lifetime — viết tắt cho lifetime ở những chỗ compiler có thể tự suy. Cú pháp impl Parser<'_> trong context không cần tên, khi nào nên dùng để rõ ràng hơn so với implicit elision, và lý do Rust 2018+ khuyến nghị '_ ở vị trí có ref ẩn trong type generic.
