Danh sách bài viết

Bài 185: Lifetime Bound Trên Generic — T: 'a

Bài 185 của series Rust Cơ Bản — generic kết hợp với lifetime: khi một type tham số T và một reference &'a cùng xuất hiện trong một struct hay function, Rust cần biết T có chứa reference sống ngắn hơn 'a không. Nếu không khai báo gì, borrow checker không thể chứng minh safety và yêu cầu thêm bound T: 'a — đọc đúng nghĩa "type T phải sống ít nhất bằng 'a". Bài đi qua: cú pháp struct Wrapper<'a, T: 'a>, case đặc biệt T: 'static bắt buộc khi spawn task qua thread hoặc executor, Box<dyn Trait + 'a> để giới hạn lifetime trait object thay vì mặc định 'static, lý do async block thường cần 'static, và cách đọc error E0309 "may not live long enough" cùng fix add bound. Bài cuối Nhóm 23 — bước tiếp theo: Nhóm 24 Testing với cargo test.

0 lượt xem
1

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 — type T không được chứa bất kỳ reference nào sống ngắn hơn lifetime 'a; tương đương "T outlive 'a".
  • Biết khi nào phải thêm bound: struct generic chứa cả type parameter T và reference &'a T hoặc &'a U.
  • Viết được cú pháp struct Wrapper<'a, T: 'a> hoặc dùng where T: 'a.
  • Hiểu case đặc biệt T: 'static: T không chứa non-static reference — bắt buộc cho std::thread::spawn, tokio::spawn, async task moved qua executor.
  • Phân biệt Box<dyn Trait> (ngầm + 'static) với Box<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.

2

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: 'a luô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: 'a hợp lệ.
  • T = &'static str: chứa ref 'staticT: '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.

3

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.

4

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 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.

5

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.

6

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>'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.

7

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::JoinSet với pattern phù hợp), có thể tránh 'static như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.

8

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:

  1. Đọc dòng "may not live long enough" — xác định type parameter nào đang gặp vấn đề.
  2. Tìm lifetime 'a liên quan trong signature.
  3. Thêm bound T: 'a (hoặc T: 'static nếu context yêu cầu).
  4. 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 đó.

9

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à "'a outlive '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 = "T không chứa ref ngắn hơn 'a" = "T outlive 'a".
  • T: 'static = "T khô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.
10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Đọc nghĩa của T: 'a bằng ngôn ngữ tự nhiên. Cho ví dụ một type satisfy và một type không satisfy bound đó với 'a ngắn hơn 'static.
  2. Vì sao struct struct Wrapper<'a, T> { data: &'a T } đôi khi cần bound T: 'a explicit? Bound này ngăn unsafe scenario gì?
  3. 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 'static này khác nhau ở điểm nào?
  4. Default lifetime của Box<dyn Trait> là gì? Khi nào cần viết explicit Box<dyn Trait + 'a> thay vì để mặc định?
  5. Đ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
  1. T: 'a = "type T outlive lifetime 'a" = "T không chứa bất kỳ reference nào sống ngắn hơn 'a". Satisfy với 'a ngắn: String, i32, Vec<u8> (không chứa ref). Không satisfy: &'b str với 'b < 'a — ref nội bộ chết trước khi 'a kết thúc.
  2. Vì với generic T bất kỳ, compiler không biết T có chứa ref nội bộ ngắn hơn 'a hay không. Nếu user instantiate T = &'b str với 'b ngắn hơn 'a, field &'a T có thể trỏ tới ref đã chết → dangling reference. Bound T: 'a ép user chỉ instantiate T mà 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.
  3. T: 'staticlifetime bound trên type parameter — ràng buộc type T không chứa non-static ref (kể cả i32String đều satisfy). &'static strlifetime 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 'static rất rộng (mọi owned type); reference 'static rất hẹp (string literal, leaked Box, lazy static...).
  4. Default Box<dyn Trait>Box<dyn Trait + 'static> — trait object chỉ chứa concrete type không có non-static ref. Viết explicit + 'a khi muốn boxing concrete type chứa data borrowed lifetime 'a (ví dụ struct Borrowed<'a>(&'a str) implement Trait). Bỏ + 'a sẽ bị reject vì 'a ngắn hơn default 'static.
  5. Lỗi: T trong return type Box<dyn AsRef<T> + 'a> nhưng compiler chưa biết T có outlive 'a không. Fix: thêm bound T: 'a:
    fn store<'a, T: 'a>(x: &'a T) -> Box<dyn AsRef<T> + 'a>
    where
        T: AsRef<T>,
    {
        Box::new(x)
    }
    Hoặc gộp trong where: where T: AsRef<T> + 'a.
11

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.