Danh sách bài viết

Bài 181: 'static Lifetime — Sống Suốt Đời Chương Trình

Bài 181 của series Rust Cơ Bản — 'static là longest possible lifetime, biểu diễn data sống suốt từ khi chương trình start đến lúc exit. Khác với 'a, 'b là tên do programmer đặt, 'static là keyword built-in trong ngôn ngữ. Bài đi qua: ý nghĩa cốt lõi của 'static, lý do mọi string literal "hello" đều có type &'static str (literal được embed vào binary read-only), const và static item đều ngầm 'static, phân biệt cốt lõi giữa T: 'static bound (dùng cho thread::spawn / async task) và &'static T (reference sống mãi), anti-pattern ép 'static để workaround lifetime error, use case thực tế (config singleton, regex const, axum handler), cuối cùng là mental rule khi compiler đòi 'static mà bạn không expect.

09/06/2026
10 phút đọc
1 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Hiểu 'staticlongest possible lifetime — data sống từ khi program start đến khi exit, và là keyword built-in chứ không phải tên do programmer tự đặt.
  • Nhận diện mọi string literal "hello" đều có type &'static str vì nội dung literal được compiler embed vào binary read-only segment.
  • Hiểu const NAME: &str = "..."static GREETING: &str = "..." đều ngầm gán 'static cho mọi reference bên trong — không cần viết tay.
  • Phân biệt cốt lõi giữa T: 'static bound ("T không chứa non-static reference") và &'static T ("reference đến T sống mãi") — hai khái niệm dễ nhầm.
  • Áp dụng T: 'static đúng cách cho thread::spawn, tokio::spawn, axum handler — nơi data phải sống qua mọi async boundary.
  • Tránh anti-pattern ép 'static để workaround lifetime error: đó luôn là chỉ dấu của design flaw, cần restructure ownership thay vì leak hoặc ép kiểu.
  • Biết khi compiler bất ngờ đòi 'static mà bạn không expect, đó là tín hiệu data đang vượt scope dự kiến — debug theo hướng "ai giữ ref qua boundary nào".
2

'static Là Gì

Trước hết, phân biệt 'static với các lifetime khác trên ba khía cạnh:

'staticlongest possible lifetime trong Rust: bắt đầu từ thời điểm chương trình khởi chạy và kéo dài đến khi process kết thúc. Đây là keyword built-in của ngôn ngữ — không thể đặt tên trùng, không thể redefine, có ý nghĩa cố định trong mọi context.

Hệ quả thực tế: bất kỳ data nào có lifetime 'static đều "không bao giờ bị drop trong quá trình chạy". Bạn có thể giữ reference đến nó từ mọi thread, mọi async task, mọi closure mà không sợ dangling. Đó là lý do 'static xuất hiện ở các API yêu cầu data tồn tại qua thread/task boundary.

Nguồn gốc của 'static data có thể là:

  • String literal ("hello") — embed vào binary.
  • const item — inline value, không có địa chỉ runtime nhưng mọi reference sinh ra đều sống mãi.
  • static item — có địa chỉ runtime cố định, sống suốt chương trình.
  • Owned data được leak chủ ý qua Box::leak hoặc Vec::leak (advanced).
  • Data thoả mãn T: 'static bound (chứa zero non-static reference) — owned hoàn toàn.

Hai cách dùng phổ biến của 'static trong syntax cần phân biệt sớm: &'static T (annotation trên reference) và T: 'static (lifetime bound trên type parameter). Hai cái nhìn giống nhau nhưng khác nhau hoàn toàn về ngữ nghĩa; mục 6 dưới sẽ phân tích.

3

String Literal &'static str

String literal là nguồn 'static phổ biến nhất bạn gặp mỗi ngày. Khi bạn viết "hello" trong code, compiler:

  1. Embed các byte UTF-8 "hello" vào binary executable ở segment read-only (thường tên .rodata).
  2. Sinh ra một reference trỏ vào đó, type &'static str.
  3. Vì data nằm trong binary, nó sống đúng từ khi process load đến khi process exit — lifetime 'static hoàn hảo.
fn main() {
    let s = "hello, rust";        // type: &'static str
    let owned: &'static str = "blogcode.vn";   // viết explicit cho rõ
    println!("{s} | {owned}");

    // Đưa qua function không khai báo lifetime cũng OK
    // vì &'static str hợp với mọi &'a str
    print_msg(s);
}

fn print_msg(msg: &str) {
    println!("> {msg}");
}

Vài hệ quả quan trọng:

  • Literal read-only: không thể mutate được. Muốn modify phải .to_string() tạo String owned.
  • Literal không tốn heap — chỉ tốn bytes trong binary file. Tốt cho startup time và memory footprint.
  • &'static str tương thích với mọi &'a str nhờ subtyping: 'static "outlives" mọi 'a nên có thể coerce xuống.
  • Byte string literal b"hello" có type &'static [u8; 5] — tương tự cơ chế, embed bytes vào binary.
4

const / static Variable Cũng 'static

Ngoài string literal, conststatic item cũng đều có lifetime 'static cho mọi reference bên trong — quy tắc gọi là static lifetime elision được đặc tả tại Rust Reference.

// Mọi reference trong const/static được ngầm gán 'static
const APP_NAME: &str = "blogcode";          // == &'static str
const VERSION: &str = "1.0.0";

static GREETING: &str = "Xin chào Rust";    // == &'static str
static ENDPOINTS: &[&str] = &[
    "/api/v1/users",
    "/api/v1/posts",
];                                            // &'static [&'static str]

fn main() {
    println!("{APP_NAME} v{VERSION}");
    println!("{GREETING}");
    for ep in ENDPOINTS {
        println!("- {ep}");
    }
}

Lý do: const được inline ở mỗi nơi sử dụng còn static có địa chỉ cố định trong binary — cả hai đều không thể "biến mất giữa chừng" trong quá trình chạy. Vì vậy compiler thấy không có lựa chọn nào khác ngoài 'static nên áp luôn, không yêu cầu bạn viết tay.

Bạn vẫn được viết explicit nếu muốn cho rõ ràng, ví dụ const APP_NAME: &'static str = "blogcode" — hợp lệ và không thay đổi ý nghĩa. Idiom hiện đại thường bỏ 'static trong khai báo này vì redundant.

Lưu ý phân biệt với let binding bình thường: let s = "hello" tạo binding s có scope theo block, nhưng data mà s trỏ tới vẫn là &'static str. Binding khác với lifetime của data.

5

T: 'static Bound

Khi xuất hiện ở vị trí bound trên generic, T: 'static mang ngữ nghĩa hoàn toàn khác với &'static T:

T: 'static nghĩa là "type T không chứa bất kỳ non-static reference nào bên trong". Tất cả owned data (String, Vec<u8>, i32, struct chỉ chứa owned field) đều thoả 'static. Chỉ những type chứa &'a T với 'a < 'static mới không thoả.

Use case kinh điển: std::thread::spawn yêu cầu closure phải 'static vì OS thread có thể outlive function caller, nên data move vào closure phải bảo đảm sống đủ lâu:

use std::thread;

fn main() {
    let name = String::from("blogcode");   // owned String -> thoả 'static
    let handle = thread::spawn(move || {
        // Closure capture `name` bằng move, name là owned data
        // → closure type thoả 'static
        println!("Hello from thread: {name}");
    });
    handle.join().unwrap();
}

Code trên compile được vì name: String là owned hoàn toàn — không chứa reference nào → tự động thoả T: 'static. Đối lập với case sau sẽ fail:

use std::thread;

fn bad() {
    let name = String::from("blogcode");
    let r: &str = &name;
    thread::spawn(move || {
        // ERROR: `r` chứa reference đến `name`,
        // không thoả 'static vì name sẽ drop ở cuối `bad()`.
        println!("{r}");
    });
}

Tương tự, Tokio runtime cũng dùng F: Future + Send + 'static cho tokio::spawn. Lý do: future có thể chạy trên thread worker khác, sống lâu hơn function caller, nên cần đảm bảo không reference data có scope hẹp hơn.

6

Vs &'static T

Đây là cặp khái niệm rất dễ nhầm. Tách rõ ra:

  • T: 'staticbound trên type parameter, mang nghĩa "type T không chứa reference nào sống ngắn hơn 'static". Data có thể được owned, không bắt buộc reference.
  • &'static Treference đến T với lifetime cố định 'static, nghĩa là reference sống mãiT cũng phải sống mãi.

So sánh trực quan:

// (1) T: 'static — chấp nhận owned data
fn store<T: 'static>(value: T) {
    // value có thể là String, Vec<u8>, i32, ... bất cứ owned data nào
}

fn main() {
    let s: String = String::from("blogcode");
    store(s);          // OK — String là owned, thoả T: 'static
    store(42i32);      // OK
    store("hello");    // OK — &'static str cũng thoả
}

// (2) &'static T — chỉ chấp nhận reference sống mãi
fn need_ref(r: &'static str) {
    println!("{r}");
}

fn main2() {
    need_ref("hello");                       // OK — string literal
    let owned = String::from("blogcode");
    // need_ref(&owned);                     // ERROR — owned drop khi main2 kết thúc
}

Sai lầm thường gặp: thấy compiler đòi 'static, programmer nghĩ "phải có reference sống mãi" rồi cố làm Box::leak hoặc khai global static — trong khi thực tế chỉ cần move owned data vào là đủ. Hiểu đúng bound giúp tránh leak memory không cần thiết.

Cách đọc gọn: thấy T: 'static đọc là "T sống đủ lâu"; thấy &'static T đọc là "reference sống mãi". Hai mệnh đề khác hẳn nhau.

7

Tránh 'static Workaround

Anti-pattern phổ biến với newcomer: gặp lifetime error → thử thêm 'static vào signature → "lucky compile" → lưu lại như giải pháp. Đây gần như luôn là cách sai.

// Anti-pattern: ép 'static để compiler khỏi than
struct Cache {
    data: &'static str,            // ép cứng 'static
}

fn build(input: String) -> Cache {
    // Bắt buộc phải leak để có &'static str
    Cache {
        data: Box::leak(input.into_boxed_str()),   // leak memory!
    }
}

Hậu quả của giải pháp trên:

  • Memory leak có chủ đích: Box::leak không trả heap về allocator nữa — nếu hàm này được gọi nhiều lần, heap tăng tuyến tính cho đến khi process exit.
  • Mất khả năng drop data theo scope — không kiểm soát được lifetime, lose ergonomic Rust mang lại.
  • Lừa borrow checker đồng nghĩa với việc chính bạn phải chịu trách nhiệm chứng minh logic an toàn, mà bạn chưa có kiến thức để chứng minh.

Cách đúng: own dữ liệu thay vì giữ reference:

// Đúng: own data bằng String
struct Cache {
    data: String,
}

fn build(input: String) -> Cache {
    Cache { data: input }            // move ownership, sạch
}

Hoặc nếu cần share read-only giữa nhiều consumer, dùng Arc<str>:

use std::sync::Arc;

struct Cache {
    data: Arc<str>,
}

Quy tắc: nếu thấy thêm 'static để workaround → dừng lại, đổi sang owned data hoặc Arc. 'static chỉ nên xuất hiện khi bản chất data thực sự là static (literal, const, leaked với lý do thiết kế rõ ràng).

8

Use Case Thực Tế

Khi đã hiểu đúng, 'static là công cụ rất hữu ích trong nhiều tình huống thực tế. Bốn use case phổ biến:

(a) Log target / error code

Tag string truyền vào logger thường là literal — &'static str tự nhiên, không heap allocate, hash nhanh.

const LOG_TARGET_AUTH: &str = "app::auth";
const LOG_TARGET_DB: &str = "app::db";

fn login() {
    log::info!(target: LOG_TARGET_AUTH, "user logged in");
}

(b) Config singleton qua OnceLock

use std::sync::OnceLock;

static CONFIG: OnceLock<String> = OnceLock::new();

fn config() -> &'static str {
    CONFIG.get_or_init(|| std::env::var("APP_ENV").unwrap_or_else(|_| "dev".into()))
}

(c) Axum handler trả &'static str

use axum::{routing::get, Router};

async fn health() -> &'static str {
    "OK"          // literal, no allocation per request
}

fn router() -> Router {
    Router::new().route("/health", get(health))
}

(d) Regex compile-time const

Với crate regex, có thể giữ pattern là &'static str rồi compile lazy qua OnceLock<Regex> — vừa hiệu năng vừa idiomatic.

9

Đo Cẩn Thận Khi Compiler Đòi 'static

Khi compiler báo lỗi đại loại "argument requires that X must outlive 'static" trong khi bạn không có chủ ý gì với 'static, đừng vội ép kiểu. Đây gần như luôn là chỉ dấu của design flaw.

Quy trình debug theo thứ tự:

  1. Xác định ai đang đòi 'static: thường là thread::spawn, tokio::spawn, framework handler — đọc kỹ tài liệu API.
  2. Truy lại data đang vi phạm: error message luôn chỉ ra "the data is borrowed for the lifetime '_" — đó là ref đang sống ngắn hơn yêu cầu.
  3. Đổi ref thành owned: String thay &str, Vec<T> thay &[T], Arc<T> nếu cần share giữa nhiều task.
  4. Nếu data cần share mà clone nặng: cân nhắc Arc<T> hoặc Arc<Mutex<T>>, không phải Box::leak.
  5. Chỉ leak khi data thực sự sống mãi theo thiết kế: ví dụ config được load 1 lần khi startup. Dùng OnceLock hoặc OnceCell thay Box::leak raw.

Mental rule: "compiler đòi 'static" = "API này cần data sống qua boundary, hãy cho nó owned data". Không phải "hãy biến reference thành &'static".

Hiểu đúng quy trình này sẽ giúp bạn ít panic khi đọc lỗi async / multithreading. Phần lớn lỗi 'static trên Stack Overflow đều có cùng pattern, và giải pháp đúng đa phần là move ownership, không phải lifetime trickery.

10

Tổng Kết

  • 'staticlongest possible lifetime — data sống từ start program đến exit. Đây là keyword built-in, không phải tên do programmer đặt.
  • String literal "hello" luôn có type &'static str vì byte được embed vào binary read-only segment, sống đúng từ load tới exit.
  • const / static item ngầm gán 'static cho mọi reference bên trong qua static lifetime elision; không cần viết tay.
  • T: 'static bound nghĩa là "T không chứa non-static reference" — owned data luôn thoả. Dùng cho thread::spawn, tokio::spawn, async task vượt thread boundary.
  • Phân biệt cốt lõi: T: 'static nói "data sống đủ lâu" (có thể owned); &'static T nói "reference sống mãi" (đòi data cũng sống mãi). Hai khái niệm khác hẳn.
  • Anti-pattern: ép 'static bằng Box::leak để workaround lifetime error → memory leak không cần thiết. Đúng là move ownership / dùng Arc.
  • Use case thực tế: log target, config singleton qua OnceLock, axum handler trả &'static str, regex pattern const.
  • Compiler đòi 'static mà không expect = chỉ dấu design flaw → đổi ref thành owned, không ép kiểu.
11

Bài Tập Củng Cố

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

  1. Type của expression "blogcode.vn" là gì? Vì sao compiler có thể gán lifetime 'static cho nó mà không cần programmer khai báo?
  2. Phân biệt ngắn gọn T: 'static bound và &'static T. Cho ví dụ owned data thoả T: 'static nhưng không phải &'static reference.
  3. Vì sao std::thread::spawn yêu cầu closure phải 'static? Đoạn code nào sau đây compile được: thread::spawn(move || println!("{name}")) với name: String, hay với name: &str trỏ vào local variable?
  4. Anti-pattern dùng Box::leak để workaround lifetime error có nhược điểm gì? Hai cách đúng thay thế khi cần data sống qua thread boundary?
  5. Khi compiler báo "argument requires that x must outlive 'static" trong khi bạn không expect, bước debug đầu tiên nên làm là gì?
Đáp án
  1. Type là &'static str. Compiler gán 'static được vì byte UTF-8 của literal được embed thẳng vào binary executable (segment .rodata) — data sống đúng từ khi process load đến lúc exit, fit hoàn hảo với định nghĩa 'static.
  2. T: 'static = "type T không chứa non-static reference" — owned data luôn thoả. &'static T = "reference đến T với lifetime sống mãi". Ví dụ: String::from("hi")String owned, thoả T: 'static nhưng không phải &'static str; còn "hi" literal mới là &'static str.
  3. thread::spawn yêu cầu closure 'static vì OS thread có thể outlive function caller, nên data move vào closure không được tham chiếu local data của caller (sẽ dangling). thread::spawn(move || println!("{name}")) với name: String compile được (owned data thoả T: 'static). Với name: &str trỏ vào local variable thì fail — reference đó không phải 'static.
  4. Nhược điểm: (i) memory leak có chủ đích, heap không trả về allocator; (ii) mất scope-based drop; (iii) lừa borrow checker → tự chịu trách nhiệm an toàn. Hai cách đúng: (a) move owned data (String, Vec, struct chỉ chứa owned field) qua boundary; (b) dùng Arc<T> hoặc Arc<Mutex<T>> nếu cần share giữa nhiều task / thread.
  5. Bước đầu tiên: truy lại data đang vi phạm theo error message ("the data is borrowed for the lifetime '_") để xác định ref đang sống ngắn hơn yêu cầu. Sau đó đổi reference thành owned (String thay &str, Vec thay &[T]). Không ép kiểu 'static hoặc dùng Box::leak như reflex đầu tiên.
12

Bài Tiếp Theo

Bài 182: Multiple Lifetimes: fn foo<'a, 'b> — khi function có nhiều input reference với lifetime khác nhau và output gắn với một lifetime cụ thể, bạn cần khai báo nhiều lifetime parameter cùng lúc. Bài tiếp phân tích cú pháp fn parse<'src, 'log>(src: &'src str, log: &'log mut Logger) -> Token<'src>, lý do compiler không thể elide trong tình huống này, và cách đặt tên lifetime cho code đọc tốt.