Skip to content

Variables & Mutability Mechanics Memory Deep Dive

Immutability by Default — Không phải hạn chế, mà là Design Decision

Tại sao Rust chọn Immutable by Default?

Trong C/C++ hay Java, variables mặc định mutable:

cpp
// C++
int x = 5;
x = 10;  // OK - mặc định mutable

Rust đi ngược lại:

rust
let x = 5;
x = 10;  // ❌ ERROR: cannot assign twice to immutable variable

Tại sao? Bởi vì:

  1. Predictability: Immutable data dễ reason hơn — không ai modify nó "từ đâu đó"
  2. Concurrency Safety: Immutable data có thể shared giữa threads mà không cần locks
  3. Compiler Optimization: Compiler biết value không đổi → optimize tốt hơn

💡 TRIẾT LÝ RUST

"Make the common case safe by default. Opt-in to danger when you need it."

Chọn immutable by default → phải explicitly khai báo mutation → code dễ review hơn.


let vs let mut — Stack Memory View

Immutable Variable (let)

rust
fn main() {
    let x: i32 = 42;
    println!("{}", x);
}

Stack Layout:

                    STACK FRAME: main()
               ┌────────────────────────────┐
               │                            │
   ┌───────────┤   x: i32 = 42              │  ← 4 bytes, immutable
   │           │   [0x2A, 0x00, 0x00, 0x00] │    Little-endian
   │           │                            │
   │           └────────────────────────────┘

   └── Compiler marks this slot as READ-ONLY
       Any attempt to write → Compile Error

Mutable Variable (let mut)

rust
fn main() {
    let mut x: i32 = 42;
    x = 100;  // ✅ OK - đã khai báo mutable
    println!("{}", x);
}

Stack Layout (trước và sau mutation):

TRƯỚC:                           SAU x = 100:
┌────────────────────────┐      ┌────────────────────────┐
│ x: i32 = 42            │  →   │ x: i32 = 100           │
│ [0x2A, 0x00, 0x00, 0x00]│      │ [0x64, 0x00, 0x00, 0x00]│
└────────────────────────┘      └────────────────────────┘
     ↑                               ↑
     CÙNG địa chỉ memory             CÙNG slot, giá trị mới

Key insight: let mut cho phép overwrite cùng một slot memory.


Shadowing — Re-binding, Không phải Mutation

Shadowing là gì?

rust
fn main() {
    let x = 5;           // Binding #1: x = 5
    let x = x + 1;       // Binding #2: x = 6 (shadowing #1)
    let x = x * 2;       // Binding #3: x = 12 (shadowing #2)
    
    println!("{}", x);   // Output: 12
}

Đây KHÔNG phải mutation. Mỗi let x = ... tạo binding MỚI.

Shadowing trong Memory

                    STACK FRAME: main()
               ┌────────────────────────────────────┐
               │                                    │
    Slot 3 →   │   x: i32 = 12    ← ACTIVE binding │
               │   [0x0C, 0x00, 0x00, 0x00]         │
               ├────────────────────────────────────┤
    Slot 2 →   │   (shadowed) x: i32 = 6           │  ← Không còn accessible
               │   [0x06, 0x00, 0x00, 0x00]         │
               ├────────────────────────────────────┤
    Slot 1 →   │   (shadowed) x: i32 = 5           │  ← Không còn accessible
               │   [0x05, 0x00, 0x00, 0x00]         │
               │                                    │
               └────────────────────────────────────┘

⚠️ QUAN TRỌNG

Shadowing có thể tốn thêm stack space (trong unoptimized builds). Tuy nhiên, optimizer thường reuse slots nếu thấy giá trị cũ không còn được dùng.

Trong Release mode với optimizations, diagram trên có thể collapse thành 1 slot.

Shadowing cho phép thay đổi Type

rust
fn main() {
    let spaces = "   ";          // &str (3 bytes + fat pointer)
    let spaces = spaces.len();   // usize (8 bytes on 64-bit)
    
    println!("{}", spaces);      // Output: 3
}

Với let mut — KHÔNG THỂ:

rust
fn main() {
    let mut spaces = "   ";
    spaces = spaces.len();  // ❌ ERROR: expected `&str`, found `usize`
}

Memory view:

┌─────────────────────────────────────┐
│ Binding 1: spaces: &str             │
│   ptr  → "   " (3 bytes UTF-8)      │  ← 16 bytes (fat pointer)
│   len  = 3                          │
├─────────────────────────────────────┤
│ Binding 2: spaces: usize            │
│   = 3                               │  ← 8 bytes
└─────────────────────────────────────┘

  Khác TYPE, khác SIZE → phải là BINDING MỚI

const vs static — Binary Memory Segments

Constants (const)

rust
const MAX_POINTS: u32 = 100_000;

fn main() {
    println!("Max: {}", MAX_POINTS);
}

Đặc điểm const:

  • Compile-time constant: Giá trị phải biết lúc compile
  • INLINED: Compiler thay thế mọi chỗ dùng bằng giá trị trực tiếp
  • Không có địa chỉ memory cố định — không thể lấy &MAX_POINTS một cách ổn định

Compiled output (pseudo-assembly):

asm
; println!("Max: {}", MAX_POINTS);
; MAX_POINTS được INLINE thành literal 100000
mov rdi, 100000   ; Không có memory load, giá trị ngay trong instruction
call print_u32

Statics (static)

rust
static GLOBAL_COUNT: u32 = 42;

fn main() {
    println!("Count: {}", GLOBAL_COUNT);
}

Đặc điểm static:

  • Fixed memory address: Tồn tại trong binary, có địa chỉ cố định
  • Lifetime 'static: Sống suốt đời chương trình
  • Không inlined: Mọi access đều load từ cùng địa chỉ

Memory Segments

┌───────────────────────────────────────────────────────────────┐
│                      BINARY LAYOUT                              │
├───────────────────────────────────────────────────────────────┤
│                                                               │
│  .text (code)                                                 │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │ mov rdi, 100000   ; const MAX_POINTS INLINED here       │  │
│  │ call print_u32                                          │  │
│  │ ...                                                     │  │
│  └─────────────────────────────────────────────────────────┘  │
│                                                               │
│  .rodata (read-only data)                                     │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │ GLOBAL_COUNT: 0x0000002A  (42 in little-endian)         │  │
│  │                ↑                                         │  │
│  │                static biến sống ở đây                    │  │
│  └─────────────────────────────────────────────────────────┘  │
│                                                               │
│  .data (initialized mutable data)                             │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │ (static mut variables would live here)                  │  │
│  └─────────────────────────────────────────────────────────┘  │
│                                                               │
│  .bss (uninitialized data - zeroed at startup)               │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │ (static variables with default/zero values)             │  │
│  └─────────────────────────────────────────────────────────┘  │
│                                                               │
└───────────────────────────────────────────────────────────────┘

So sánh const vs static

Đặc điểmconststatic
Compile-time value✅ Bắt buộc✅ Bắt buộc
Có địa chỉ cố định
Inlined✅ Mọi nơi
Binary size impactCó thể tăng (nhiều copies)1 copy
Mutable❌ Không bao giờ⚠️ static mut (unsafe)
Thread-safe⚠️ Cần sync primitives

Khi nào dùng gì?

rust
// ✅ DÙNG const: Mathematical constants, config values
const PI: f64 = 3.14159265358979;
const MAX_BUFFER_SIZE: usize = 1024 * 1024;

// ✅ DÙNG static: Khi cần địa chỉ cố định (FFI, lazy init)
static VERSION: &str = "1.0.0";

// ⚠️ static mut: TRÁNH nếu có thể - unsafe và dễ race condition
static mut COUNTER: u32 = 0;  // Cần unsafe block để access

Type Inference và Annotations

Rust compiler suy luận type rất mạnh:

rust
fn main() {
    let x = 5;           // Type: i32 (default integer)
    let y = 2.0;         // Type: f64 (default float)
    let z = true;        // Type: bool
    
    // Explicit annotation khi cần
    let a: i64 = 5;      // i64, không phải i32
    let b: f32 = 2.0;    // f32, không phải f64
    
    // Suffix notation
    let c = 5i64;        // i64 via suffix
    let d = 2.0f32;      // f32 via suffix
}

Khi nào CẦN type annotation?

rust
// 1. Khi compiler không thể suy luận
let parsed: i32 = "42".parse().unwrap();  // parse() returns Result<T, E>
                                           // T không xác định được

// 2. Khi muốn type khác default
let small: i8 = 10;  // Thay vì i32

// 3. Collection với item type chưa rõ
let mut numbers: Vec<i32> = Vec::new();  // Chưa có .push() để compiler suy luận

Bảng Tóm tắt

ConceptSyntaxMemory BehaviorUse Case
Immutablelet x = 5;Stack slot, read-onlyDefault choice
Mutablelet mut x = 5;Stack slot, read-writeWhen need to modify in-place
Shadowinglet x = ...; (lại)New stack slot (có thể optimized)Change type, transform value
Constantconst X: T = val;Inlined everywhereCompile-time known values
Staticstatic X: T = val;Fixed address in binaryGlobal state, FFI

Bài tập tự kiểm tra

💪 Bài 1: Dự đoán output
rust
fn main() {
    let x = 5;
    let x = x + 1;
    {
        let x = x * 2;
        println!("Inner: {}", x);
    }
    println!("Outer: {}", x);
}
Đáp án
Inner: 12
Outer: 6

Giải thích:

  • x = 5x = 6 (shadowing)
  • Trong block: x = 12 (shadow lại)
  • Ra khỏi block: shadow trong block hết scope → quay về x = 6
💪 Bài 2: Sửa lỗi compile
rust
fn main() {
    let msg = "hello";
    msg = "world";
}
Đáp án

Cách 1: Dùng let mut

rust
let mut msg = "hello";
msg = "world";

Cách 2: Dùng shadowing

rust
let msg = "hello";
let msg = "world";

Cả hai đều valid, nhưng có semantic khác:

  • let mut: Cùng binding, cùng type
  • Shadowing: Binding mới