Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu
'staticlà longest 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 strvì nội dung literal được compiler embed vào binary read-only segment. - Hiểu
const NAME: &str = "..."vàstatic GREETING: &str = "..."đều ngầm gán'staticcho mọi reference bên trong — không cần viết tay. - Phân biệt cốt lõi giữa
T: 'staticbound ("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 chothread::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
'staticmà 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".
'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:
'static là longest 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. constitem — inline value, không có địa chỉ runtime nhưng mọi reference sinh ra đều sống mãi.staticitem — có địa chỉ runtime cố định, sống suốt chương trình.- Owned data được leak chủ ý qua
Box::leakhoặcVec::leak(advanced). - Data thoả mãn
T: 'staticbound (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.
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:
- Embed các byte
UTF-8"hello" vào binary executable ở segment read-only (thường tên.rodata). - Sinh ra một reference trỏ vào đó, type
&'static str. - Vì data nằm trong binary, nó sống đúng từ khi process load đến khi process exit — lifetime
'statichoà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ạoStringowned. - Literal không tốn heap — chỉ tốn bytes trong binary file. Tốt cho startup time và memory footprint.
&'static strtương thích với mọi&'a strnhờ subtyping:'static"outlives" mọi'anê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.
const / static Variable Cũng 'static
Ngoài string literal, const và static 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.
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.
Vs &'static T
Đây là cặp khái niệm rất dễ nhầm. Tách rõ ra:
T: 'static— bound 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 T— reference đến T với lifetime cố định'static, nghĩa là reference sống mãi và T 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.
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::leakkhô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).
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.
Đ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ự:
- Xác định ai đang đòi
'static: thường làthread::spawn,tokio::spawn, framework handler — đọc kỹ tài liệu API. - 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. - Đổi ref thành owned:
Stringthay&str,Vec<T>thay&[T],Arc<T>nếu cần share giữa nhiều task. - Nếu data cần share mà clone nặng: cân nhắc
Arc<T>hoặcArc<Mutex<T>>, không phảiBox::leak. - 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
OnceLockhoặcOnceCellthayBox::leakraw.
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.
Tổng Kết
'staticlà longest 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 strvì byte được embed vào binary read-only segment, sống đúng từ load tới exit. const/staticitem ngầm gán'staticcho mọi reference bên trong qua static lifetime elision; không cần viết tay.T: 'staticbound nghĩa là "T không chứa non-static reference" — owned data luôn thoả. Dùng chothread::spawn,tokio::spawn, async task vượt thread boundary.- Phân biệt cốt lõi:
T: 'staticnói "data sống đủ lâu" (có thể owned);&'static Tnó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
'staticbằngBox::leakđể workaround lifetime error → memory leak không cần thiết. Đúng là move ownership / dùngArc. - Use case thực tế: log target, config singleton qua
OnceLock, axum handler trả&'static str, regex pattern const. - Compiler đòi
'staticmà không expect = chỉ dấu design flaw → đổi ref thành owned, không ép kiểu.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Type của expression
"blogcode.vn"là gì? Vì sao compiler có thể gán lifetime'staticcho nó mà không cần programmer khai báo? - Phân biệt ngắn gọn
T: 'staticbound và&'static T. Cho ví dụ owned data thoảT: 'staticnhưng không phải&'staticreference. - Vì sao
std::thread::spawnyêu cầu closure phải'static? Đoạn code nào sau đây compile được:thread::spawn(move || println!("{name}"))vớiname: String, hay vớiname: &strtrỏ vào local variable? - 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? - Khi compiler báo "argument requires that
xmust outlive'static" trong khi bạn không expect, bước debug đầu tiên nên làm là gì?
Đáp án
- 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. 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")làStringowned, thoảT: 'staticnhưng không phải&'static str; còn"hi"literal mới là&'static str.thread::spawnyêu cầu closure'staticvì 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ớiname: Stringcompile được (owned data thoảT: 'static). Vớiname: &strtrỏ vào local variable thì fail — reference đó không phải'static.- 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ùngArc<T>hoặcArc<Mutex<T>>nếu cần share giữa nhiều task / thread. - 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 (Stringthay&str,Vecthay&[T]). Không ép kiểu'statichoặc dùngBox::leaknhư reflex đầu tiên.
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.
