Giao diện
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 mutableRust đi ngược lại:
rust
let x = 5;
x = 10; // ❌ ERROR: cannot assign twice to immutable variableTại sao? Bởi vì:
- Predictability: Immutable data dễ reason hơn — không ai modify nó "từ đâu đó"
- Concurrency Safety: Immutable data có thể shared giữa threads mà không cần locks
- 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 ErrorMutable 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ớiKey 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ỚIconst 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_POINTSmộ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_u32Statics (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ểm | const | static |
|---|---|---|
| Compile-time value | ✅ Bắt buộc | ✅ Bắt buộc |
| Có địa chỉ cố định | ❌ | ✅ |
| Inlined | ✅ Mọi nơi | ❌ |
| Binary size impact | Có 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 để accessType 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ậnBảng Tóm tắt
| Concept | Syntax | Memory Behavior | Use Case |
|---|---|---|---|
| Immutable | let x = 5; | Stack slot, read-only | Default choice |
| Mutable | let mut x = 5; | Stack slot, read-write | When need to modify in-place |
| Shadowing | let x = ...; (lại) | New stack slot (có thể optimized) | Change type, transform value |
| Constant | const X: T = val; | Inlined everywhere | Compile-time known values |
| Static | static X: T = val; | Fixed address in binary | Global 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: 6Giải thích:
x = 5→x = 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