Mục lục
- Mục Tiêu Bài Học
T: 'aBound Là Gì- Khi Nào Cần Lifetime Bound Trên Generic
- Cú Pháp
struct Wrapper<'a, T: 'a> T: 'staticSpecial CaseBox<dyn Trait + 'a>— Trait Object Lifetime- Use Case Async — Vì Sao
'static - Compile Error E0309 "May Not Live Long Enough"
- Tổng Kết Nhóm 23
- 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ẽ:
- Hiểu chính xác ý nghĩa của bound
T: 'a— typeTkhông được chứa bất kỳ reference nào sống ngắn hơn lifetime'a; tương đương "Toutlive'a". - Biết khi nào phải thêm bound: struct generic chứa cả type parameter
Tvà reference&'a Thoặc&'a U. - Viết được cú pháp
struct Wrapper<'a, T: 'a>hoặc dùngwhere T: 'a. - Hiểu case đặc biệt
T: 'static:Tkhông chứa non-static reference — bắt buộc chostd::thread::spawn,tokio::spawn, async task moved qua executor. - Phân biệt
Box<dyn Trait>(ngầm+ 'static) vớiBox<dyn Trait + 'a>(lifetime giới hạn theo'a). - Đọc và fix lỗi
E0309 "parameter type may not live long enough"bằng cách thêm bound đúng vào generic.
Đây là bài cuối cùng của Nhóm 23 Lifetimes. Sau bài này, series chuyển sang Nhóm 24 — Testing.
T: 'a Bound Là Gì
Đến đây bạn đã quen với trait bound dạng T: Clone ("T phải implement Clone"). Lifetime bound là một dạng bound khác đặt giữa type và lifetime parameter:
T: 'a đọc là "T outlive 'a" — type T không được chứa bất kỳ reference nào sống ngắn hơn 'a. Mọi instance T mà compiler thấy phải vẫn valid trong toàn bộ scope 'a.
Các case cụ thể:
T = i32: không chứa reference → outlive mọi lifetime →i32: 'aluôn đúng.T = String: tương tự, không chứa ref →String: 'ađúng.T = &'b str: chứa ref với lifetime'b→ cần'b: 'ađểT: 'ahợp lệ.T = &'static str: chứa ref'static→T: 'ađúng với mọi'a.
Mục đích của bound là cho borrow checker biết: khi struct generic gắn T vào cùng một field &'a U, instance T sẽ không "biến mất" trước khi 'a kết thúc, nên không tạo ra dangling reference. Đây là cách Rust generalize việc safety check sang context có cả type và lifetime parameter.
Khi Nào Cần Lifetime Bound Trên Generic
Tình huống điển hình là struct generic chứa cả reference và type parameter:
// SAI: chưa có bound — compile fail
struct Wrapper<'a, T> {
data: &'a T,
}
Compiler sẽ phàn nàn vì với generic T bất kỳ, nó không biết T có chứa ref nội bộ sống ngắn hơn 'a hay không. Ví dụ: nếu user thay T = &'b str với 'b ngắn hơn 'a, thì field data: &'a T có thể trỏ tới ref đã chết — unsafe. Rust yêu cầu bạn khai báo rõ rằng T phải outlive 'a:
// ĐÚNG: có bound T: 'a
struct Wrapper<'a, T: 'a> {
data: &'a T,
}
Trong nhiều version Rust gần đây, compiler đã auto-infer bound này cho struct với reference field — nên đôi khi không cần viết tay. Tuy nhiên ở function generic, impl block, hoặc khi bound cần explicit, bạn vẫn phải viết. Mental model an toàn nhất: thấy generic + reference trong cùng struct → cân nhắc bound.
Quy tắc tổng quát: bất cứ khi nào bạn có một type T được lưu trong context lifetime 'a, T phải outlive 'a để safe.
Cú Pháp struct Wrapper<'a, T: 'a>
Hai cách viết tương đương — chọn theo độ phức tạp của bound list:
// Cách 1: inline trong generic list
struct Wrapper<'a, T: 'a> {
data: &'a T,
label: &'a str,
}
// Cách 2: dùng where clause — đọc dễ hơn khi nhiều bound
struct Wrapper<'a, T>
where
T: 'a,
{
data: &'a T,
label: &'a str,
}
Kết hợp với trait bound khác:
struct Printer<'a, T>
where
T: std::fmt::Display + 'a,
{
inner: &'a T,
}
impl<'a, T> Printer<'a, T>
where
T: std::fmt::Display + 'a,
{
fn show(&self) {
println!("{}", self.inner);
}
}
fn main() {
let s = String::from("blogcode.vn");
let p = Printer { inner: &s };
p.show(); // "blogcode.vn"
}
Cú pháp T: Display + 'a đọc là "T phải implement Display và outlive 'a". Toán tử + dùng chung cho trait bound và lifetime bound. Đây là cú pháp đầy đủ nhất — ít gặp ở code beginner nhưng rất phổ biến ở library code (serde, tokio, axum) khi viết generic struct chứa reference.
Khi instance Wrapper, Rust suy luận 'a dựa trên lifetime của &s truyền vào; bound T: 'a tự kiểm tra với type cụ thể của s. Nếu T = String (không chứa ref), check pass dễ dàng.
T: 'static Special Case
Trường hợp đặc biệt mạnh nhất của lifetime bound là T: 'static — đọc là "T không chứa bất kỳ non-static reference nào". Lưu ý: không có nghĩa instance của T sống mãi mãi, mà là type T không gắn với reference ngắn hạn.
Các type satisfy T: 'static: i32, String, Vec<u8>, HashMap<String, i32>, &'static str... — tất cả type owned hoặc chỉ chứa ref 'static. Không satisfy: &'a str với 'a < 'static, Wrapper<'a, _> với 'a ngắn hạn.
Use case kinh điển — std::thread::spawn:
use std::thread;
fn run_task<T: Send + 'static>(value: T) {
thread::spawn(move || {
// closure move `value` qua thread mới
// thread có thể sống lâu hơn function gọi spawn
// → value phải KHÔNG chứa ref ngắn hạn nào
drop(value);
});
}
fn main() {
run_task(String::from("ok")); // OK: String: 'static
// run_task(&s); // FAIL: &str có lifetime ngắn
}
Bound 'static cần thiết vì thread spawn có thể tồn tại lâu hơn function gọi nó — nếu cho phép T chứa ref ngắn, ref sẽ dangling khi function trở về và stack bị unwind. Tương tự, các executor async như tokio::spawn, async_std::task::spawn đều yêu cầu future : 'static vì cùng lý do — executor chạy task ở thread khác, không kiểm soát được lifetime caller.
Mẹo thực hành: muốn truyền data vào thread/task, hoặc dùng owned type (String, Vec), hoặc Arc<T> để share, hoặc String::from(&s) để clone thành owned.
Box<dyn Trait + 'a> — Trait Object Lifetime
Trait object có một quirk: mỗi Box<dyn Trait> ngầm gắn lifetime, và nếu không khai báo, mặc định là 'static:
// Hai dòng tương đương — default là 'static
let boxed: Box<dyn std::fmt::Display> = Box::new(42);
let boxed: Box<dyn std::fmt::Display + 'static> = Box::new(42);
Điều này có nghĩa: theo mặc định, Box<dyn Trait> không được chứa concrete type có reference ngắn hạn. Nếu bạn muốn boxing concrete type chứa &'a, phải khai báo explicit:
use std::fmt::Display;
struct Borrowed<'a>(&'a str);
impl<'a> Display for Borrowed<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "borrowed: {}", self.0)
}
}
fn make_displayer<'a>(s: &'a str) -> Box<dyn Display + 'a> {
// ^^^^^ explicit lifetime
Box::new(Borrowed(s))
}
fn main() {
let text = String::from("rust-bound");
let d = make_displayer(&text);
println!("{d}");
}
Nếu bỏ + 'a và viết Box<dyn Display>, compiler reject vì default 'static không tương thích với Borrowed<'a> mà 'a ngắn hơn static.
Quy tắc: viết Box<dyn Trait + 'a> khi muốn trait object chứa data borrowed; giữ Box<dyn Trait> (ngầm 'static) khi trait object chỉ chứa owned data. Pattern này gặp nhiều ở callback storage, builder pattern, plugin system trong các framework Rust.
Use Case Async — Vì Sao 'static
Một trong những "gotcha" lớn nhất khi học async Rust là yêu cầu 'static trên future spawn:
// tokio runtime — minh họa
async fn handler(data: &str) {
// KHÔNG được spawn với &str ngắn hạn
}
#[tokio::main]
async fn main() {
let s = String::from("hi");
// SAI: tokio::spawn yêu cầu future : 'static
// tokio::spawn(async {
// handler(&s).await; // capture &s — ref ngắn hạn → fail
// });
// ĐÚNG: move ownership vào future
tokio::spawn(async move {
handler(&s).await; // s đã được move, sống cùng future
});
}
Lý do: tokio::spawn nhận một future và đẩy lên thread pool. Future có thể chạy ở thread khác, kết thúc bất kỳ lúc nào — runtime không biết khi nào caller (function bao quanh) sẽ trở về. Nếu future capture reference ngắn hạn, reference đó có thể dangling khi caller stack frame bị unwind. Vì vậy tokio::spawn signature có dạng fn spawn<F>(future: F) where F: Future + Send + 'static.
Giải pháp tiêu chuẩn:
async move { ... }để move ownership vào future.Arc<T>nếu cần share data giữa nhiều task.- Clone data trước khi spawn (
let s2 = s.clone();). - Với scoped runtime (
std::thread::scope,tokio::task::JoinSetvới pattern phù hợp), có thể tránh'staticnhưng thuộc topic nâng cao.
Nắm vững 'static bound là điều kiện cần để học async Rust mà không bị "fight" với compiler ở mỗi spawn call.
Compile Error E0309 "May Not Live Long Enough"
Khi quên lifetime bound, compiler báo lỗi quen thuộc E0309 "parameter type may not live long enough":
// LỖI E0309
struct Wrapper<'a, T> {
inner: &'a T,
}
impl<'a, T> Wrapper<'a, T> {
fn into_box(self) -> Box<dyn AsRef<T> + 'a> {
Box::new(self.inner)
}
}
Compiler message tiêu biểu (rút gọn):
error[E0309]: the parameter type `T` may not live long enough
--> src/lib.rs:7:9
|
7 | Box::new(self.inner)
| ^^^^^^^^^^^^^^^^^^^^ ...so that the type `T` will meet its required lifetime bounds
|
help: consider adding an explicit lifetime bound
|
5 | impl<'a, T: 'a> Wrapper<'a, T> {
| +++++
Rustc đã chỉ đúng chỗ fix — chỉ cần thêm T: 'a. Sau khi sửa:
impl<'a, T: 'a> Wrapper<'a, T> {
fn into_box(self) -> Box<dyn AsRef<T> + 'a> {
Box::new(self.inner)
}
}
Quy trình debug E0309:
- Đọc dòng "may not live long enough" — xác định type parameter nào đang gặp vấn đề.
- Tìm lifetime
'aliên quan trong signature. - Thêm bound
T: 'a(hoặcT: 'staticnếu context yêu cầu). - Nếu rustc đề xuất sửa cụ thể (block
help:), thường copy-paste là xong.
E0309 không phải lỗi "khó" — nó luôn có fix mechanic rõ ràng. Khó là hiểu vì sao bound cần thiết, mà bài này đã trang bị mental model đó.
Tổng Kết Nhóm 23
Bảy bài Nhóm 23 đã đi qua toàn bộ public surface lifetime của Rust:
- Bài 179: lifetime annotation cơ bản,
fn foo<'a>(x: &'a str) -> &'a str. - Bài 180: 3 rules elision compiler tự suy lifetime cho bạn.
- Bài 181:
'static— lifetime sống suốt đời chương trình. - Bài 182: multiple lifetimes
fn foo<'a, 'b>, khi nào cần tách. - Bài 183: subtyping
'a: 'b, đọc là "'aoutlive'b". - Bài 184: anonymous lifetime
'_, dùng khi tên không quan trọng. - Bài 185 (bài này): lifetime bound trên generic
T: 'a,T: 'static,Box<dyn Trait + 'a>.
Điểm chốt nhớ:
T: 'a= "Tkhông chứa ref ngắn hơn'a" = "Toutlive'a".T: 'static= "Tkhông chứa non-static ref" — bắt buộc cho thread/async spawn.Box<dyn Trait>ngầm là+ 'static; viết explicit+ 'ađể boxing trait object có data borrowed.- E0309 "may not live long enough" → thêm bound theo gợi ý của rustc.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Đọc nghĩa của
T: 'abằng ngôn ngữ tự nhiên. Cho ví dụ một type satisfy và một type không satisfy bound đó với'angắn hơn'static. - Vì sao struct
struct Wrapper<'a, T> { data: &'a T }đôi khi cần boundT: 'aexplicit? Bound này ngăn unsafe scenario gì? - Phân biệt ý nghĩa của
T: 'static(bound trên type) vàlet s: &'static str(lifetime annotation trên reference). Hai cách dùng'staticnày khác nhau ở điểm nào? - Default lifetime của
Box<dyn Trait>là gì? Khi nào cần viết explicitBox<dyn Trait + 'a>thay vì để mặc định? - Đoạn code dưới fail với E0309. Bạn fix bằng cách nào?
fn store<'a, T>(x: &'a T) -> Box<dyn AsRef<T> + 'a> where T: AsRef<T>, { Box::new(x) }
Đáp án
T: 'a= "typeToutlive lifetime'a" = "Tkhông chứa bất kỳ reference nào sống ngắn hơn'a". Satisfy với'angắn:String,i32,Vec<u8>(không chứa ref). Không satisfy:&'b strvới'b < 'a— ref nội bộ chết trước khi'akết thúc.- Vì với generic
Tbất kỳ, compiler không biếtTcó chứa ref nội bộ ngắn hơn'ahay không. Nếu user instantiateT = &'b strvới'bngắn hơn'a, field&'a Tcó thể trỏ tới ref đã chết → dangling reference. BoundT: 'aép user chỉ instantiateTmà mọi ref bên trong đều outlive'a. Một số version Rust auto-infer bound này cho struct với reference field, nhưng impl/function vẫn cần explicit. T: 'staticlà lifetime bound trên type parameter — ràng buộc typeTkhông chứa non-static ref (kể cải32vàStringđều satisfy).&'static strlà lifetime annotation trên reference — reference cụ thể này trỏ tới data sống suốt đời chương trình. Hai khái niệm khác level: type-level vs reference-level. Type satisfy'staticrất rộng (mọi owned type); reference'staticrất hẹp (string literal, leaked Box, lazy static...).- Default
Box<dyn Trait>làBox<dyn Trait + 'static>— trait object chỉ chứa concrete type không có non-static ref. Viết explicit+ 'akhi muốn boxing concrete type chứa data borrowed lifetime'a(ví dụ structBorrowed<'a>(&'a str)implement Trait). Bỏ+ 'asẽ bị reject vì'angắn hơn default'static. - Lỗi:
Ttrong return typeBox<dyn AsRef<T> + 'a>nhưng compiler chưa biếtTcó outlive'akhông. Fix: thêm boundT: 'a:
Hoặc gộp trongfn store<'a, T: 'a>(x: &'a T) -> Box<dyn AsRef<T> + 'a> where T: AsRef<T>, { Box::new(x) }where:where T: AsRef<T> + 'a.
Bài Tiếp Theo
Hết Nhóm 23 — Lifetimes. Bài 186: cargo test — Basics Workflow mở Nhóm 24 — Testing: chạy mọi #[test] function, đọc output pass/fail/ignored, hiểu test parallel mặc định, các flag thường dùng (--release, -- --nocapture, -- --test-threads=1) cho debug và quản lý output.
