Skip to content

Lifetimes & Advanced Borrowing Advanced

Sau khi hiểu Ownership & Borrowing cơ bản — Đây là level tiếp theo

📋 PREREQUISITE

Bạn PHẢI hiểu rõ Ownership - The LawBorrowing & References trước khi đọc bài này.

Lifetimes — Generic Parameters cho References

Vấn đề: Compiler không biết reference sống bao lâu

rust
// ❌ KHÔNG COMPILE
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

Compiler Error:

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:33
  |
1 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, 
          but the signature does not say whether it is borrowed from `x` or `y`
}

Giải pháp: Lifetime Annotations

rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Ý nghĩa: Return reference sẽ sống ít nhất là 'a — intersection của lifetimes của xy.

Lifetime Visualization

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

                  x: &'a str ─────────────────────────┐

                                                      ├──▶ return: &'a str
                                                      │    (valid for min of both)
                  y: &'a str ─────────────────────────┘


Ví dụ cụ thể:
═══════════════════════════════════════════════════════════════════

fn main() {
    let s1 = String::from("long string");    // ──┐ 's1 lifetime
    {                                         //   │
        let s2 = String::from("short");       // ──┼─┐ 's2 lifetime
        let result = longest(&s1, &s2);       //   │ │
        println!("{}", result);  // ✅ OK     //   │ │
    }                                         // ──┼─┘ s2 dropped
    // result không còn valid ở đây vì        //   │
    // nó có thể trỏ đến s2 (đã dropped)      //   │
}                                             // ──┘ s1 dropped

Sai: Dùng result sau khi shorter lifetime kết thúc

rust
fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("short");
        result = longest(&s1, &s2);  // result tied to s2's lifetime
    }  // s2 dropped
    
    println!("{}", result);  // ❌ ERROR: s2 does not live long enough
}

Lifetime Elision Rules

Compiler tự động thêm lifetimes theo 3 rules để giảm boilerplate:

Rule 1: Mỗi input reference có lifetime riêng

rust
// Bạn viết:
fn first_word(s: &str) -> &str

// Compiler thấy:
fn first_word<'a>(s: &'a str) -> &str  // (chưa xong)

Rule 2: Nếu chỉ có 1 input lifetime → output dùng nó

rust
// Bạn viết:
fn first_word(s: &str) -> &str

// Compiler thấy (sau Rule 1 & 2):
fn first_word<'a>(s: &'a str) -> &'a str  // ✅ Complete

Rule 3: Nếu có &self hoặc &mut self → output dùng self's lifetime

rust
impl MyStruct {
    // Bạn viết:
    fn get_name(&self) -> &str
    
    // Compiler thấy:
    fn get_name<'a>(&'a self) -> &'a str
}

Khi nào PHẢI viết explicit lifetime?

rust
// ❌ Elision rules không đủ — cần explicit
fn longest(x: &str, y: &str) -> &str  // 2 inputs, which one?

// ✅ Phải chỉ định
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

'static Lifetime

'static = reference sống suốt chương trình:

rust
// String literals có 'static lifetime
let s: &'static str = "hello world";  // Embedded trong binary

// Tại sao?
// "hello world" được lưu trong .rodata section của binary
// Không bao giờ bị deallocate → sống mãi mãi

'static trong Trait Bounds

rust
// T phải own data hoặc chỉ chứa 'static references
fn spawn_thread<T: Send + 'static>(task: T) {
    std::thread::spawn(move || {
        // task phải sống đủ lâu cho thread
    });
}

Non-Lexical Lifetimes (NLL)

Trước Rust 2018: Borrows kéo dài đến hết scope

rust
// Rust 2015 behavior
fn main() {
    let mut data = vec![1, 2, 3];
    
    let first = &data[0];  // immutable borrow bắt đầu
    println!("{}", first);
    
    // Trong Rust 2015: first vẫn "sống" đến cuối scope
    data.push(4);  // ❌ ERROR trong Rust 2015
}

Rust 2018+: Borrows kết thúc tại lần sử dụng cuối

rust
// Rust 2018+ với NLL
fn main() {
    let mut data = vec![1, 2, 3];
    
    let first = &data[0];  // immutable borrow bắt đầu
    println!("{}", first); // ← immutable borrow KẾT THÚC Ở ĐÂY
    
    data.push(4);  // ✅ OK trong Rust 2018+
    println!("{:?}", data);
}

NLL Control Flow Graph


Fat Pointers Internals

Slice &[T] — ptr + len

rust
let arr = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..4];  // [2, 3, 4]
Array: arr = [1, 2, 3, 4, 5]
         ┌─────┬─────┬─────┬─────┬─────┐
Memory:  │  1  │  2  │  3  │  4  │  5  │
         └─────┴─────┴─────┴─────┴─────┘
          0     4     8     12    16   (byte offset)


Slice: &arr[1..4]
       ┌─────────────┐
       │ ptr ────────┘
       ├─────────────┤
       │ len: 3      │
       └─────────────┘
         16 bytes total (fat pointer)

Trait Object &dyn Trait — data_ptr + vtable_ptr

rust
trait Animal {
    fn speak(&self);
}

struct Dog;
impl Animal for Dog {
    fn speak(&self) { println!("Woof!"); }
}

fn main() {
    let dog = Dog;
    let animal: &dyn Animal = &dog;  // fat pointer
    animal.speak();
}

Vtable Layout:

&dyn Animal (16 bytes fat pointer):
┌──────────────────┐
│ data_ptr ────────────▶ Dog instance (0 bytes, ZST)
├──────────────────┤
│ vtable_ptr ──────────▶ ┌─────────────────────┐
└──────────────────┘     │ drop_fn             │ → Dog::drop
                         ├─────────────────────┤
                         │ size: 0             │
                         ├─────────────────────┤
                         │ align: 1            │
                         ├─────────────────────┤
                         │ speak_fn ───────────│─▶ Dog::speak
                         └─────────────────────┘

💡 VTABLE INSIGHT

Vtable chứa: drop function pointer, size, alignment, và tất cả trait method function pointers. Mỗi concrete typemột vtable cho mỗi trait nó implement.

Static dispatch (generics): Monomorphized, no vtable, inlined.
Dynamic dispatch (dyn Trait): Vtable lookup at runtime.


Advanced Borrowing Patterns

Reborrow Pattern

rust
fn main() {
    let mut v = vec![1, 2, 3];
    
    // Reborrow: &mut T → &mut T (với lifetime ngắn hơn)
    let r1 = &mut v;
    let r2 = &mut *r1;  // reborrow, r1 tạm thời "frozen"
    r2.push(4);
    // r1 có thể dùng lại sau khi r2 hết scope
    r1.push(5);
}

Split Borrowing

rust
fn main() {
    let mut data = [1, 2, 3, 4, 5];
    
    // ❌ SAI: không thể có 2 &mut vào cùng array
    // let (a, b) = (&mut data[0], &mut data[1]);
    
    // ✅ ĐÚNG: split_at_mut tách thành 2 slices độc lập
    let (left, right) = data.split_at_mut(2);
    left[0] = 10;
    right[0] = 30;
    // data = [10, 2, 30, 4, 5]
}

Struct với Mixed Lifetimes

rust
struct Context<'a, 'b> {
    data: &'a str,
    config: &'b str,
}

impl<'a, 'b> Context<'a, 'b> {
    fn get_data(&self) -> &'a str {
        self.data  // Return reference với lifetime 'a, không phải 'self
    }
}

Lifetime Bounds

'b: 'a — 'b outlives 'a

rust
fn example<'a, 'b>(x: &'a str, y: &'b str) -> &'a str 
where 
    'b: 'a  // 'b sống lâu hơn hoặc bằng 'a
{
    if x.len() > 0 { x } else { y }  // y có thể return vì 'b >= 'a
}

T: 'a — T sống ít nhất là 'a

rust
struct Ref<'a, T: 'a> {
    data: &'a T,
}

// T: 'a nghĩa là nếu T chứa references, chúng phải sống >= 'a

Bảng Tóm tắt

ConceptSyntaxMeaning
Lifetime param'aGeneric cho reference lifetime
Static'staticSống suốt chương trình
Elision Rule 1fn f(x: &T)Mỗi input có lifetime riêng
Elision Rule 2Single inputOutput dùng input's lifetime
Elision Rule 3&self methodOutput dùng self's lifetime
NLLNon-LexicalBorrow ends at last use
Fat pointer slice&[T]16 bytes (ptr + len)
Fat pointer trait&dyn Trait16 bytes (ptr + vtable)
Outlives'b: 'a'b sống >= 'a