Rust ownership borrowing: Hiểu memory safety không cần garbage collector

Rust ownership và borrowing là hai khái niệm cốt lõi tạo nên memory safety đặc trưng của Rust — mà không cần garbage collector, không cần runtime phức tạp. Hiểu đúng ownership không chỉ giúp viết Rust hiệu quả, mà còn thay đổi cách bạn nghĩ về memory management nói chung. Bài này đi sâu vào ownership, borrowing, lifetime và những pattern thực tế bạn sẽ gặp hàng ngày.

Ownership: 3 quy tắc cốt lõi

Rust ownership dựa trên 3 quy tắc đơn giản nhưng cực kỳ mạnh mẽ:

  1. Mỗi value có một owner duy nhất.
  2. Chỉ có một owner tại một thời điểm.
  3. Khi owner ra khỏi scope, value bị drop (giải phóng).

Trong nhiều ngôn ngữ, bạn có shallow copy (pointer), deep copy (clone), pass by reference. Rust có riêng move semantics: khi gán let b = a;, ownership chuyển từ a sang b — a không còn hợp lệ. Compiler enforce điều này ở compile-time, không cần runtime check. Điều này loại bỏ hoàn toàn double-free, use-after-free, và memory leak do forgetting to free.

Minh họa move semantics trong Rust: chuyển ownership giữa biến trên stack frame

Borrowing: Tham chiếu thay vì sở hữu

Nếu mọi lần đều phải chuyển ownership, Rust sẽ rất bất tiện. Borrowing giải quyết bằng cách cho phép tham chiếu đến value mà không lấy quyền sở hữu:

  • Immutable borrow: &T — nhiều reader được phép cùng đọc, không sửa. Compiler đảm bảo không có mutable borrow nào khác tồn tại cùng lúc.
  • Mutable borrow: &mut T — chỉ có một writer tại một thời điểm, không có reader nào khác. Writer có quyền đọc và sửa value.

Quy tắc vàng: “Nhiều immutable borrow, hoặc một mutable borrow — không cả hai cùng lúc.” Đây là rule đảm bảo không có data race at compile-time, thay vì runtime check như các ngôn ngữ khác.

fn calculate_length(s: &String) -> usize { // immutable borrow
    s.len()
}

fn append_world(s: &mut String) { // mutable borrow
    s.push_str(", world!");
}

fn main() {
    let mut s = String::from("hello");
    let len = calculate_length(&s); // immutable borrow
    append_world(&mut s);           // mutable borrow
    println!("{}", s);              // "hello, world!"
}

Lưu ý: immutable borrow và mutable borrow không thể coexist cùng scope. Nếu bạn cố gắng let r1 = &s; let r2 = &mut s; trong cùng scope, compiler sẽ báo lỗi ngay.

Lifetime: Đảm bảo reference không dangling

Lifetime là cách Rust đảm bảo borrowed reference không bao giờ dài hơn owner gốc. Trong đa số trường hợp, compiler suy luận lifetime tự động (lifetime elision rules). Bạn chỉ cần ghi rõ khi:

  • Hàm trả về reference dựa trên input reference.
  • Struct chứa reference.
  • Multiple input lifetimes cần phân biệt.
fn longest(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
    // return lifetime = min(lifetime x, lifetime y)
}

struct Config {
    path: &'a str, // Config không thể sống lâu hơn &str nó tham chiếu
}
Sơ đồ lifetime annotation: hai input string lifetime, return lifetime gắn với input ngắn hơn

Lifetime elision rules (Rust 2018+): 1) Mỗi tham số input có lifetime riêng. 2) Nếu chỉ có 1 input lifetime, nó gán cho tất cả output lifetime. 3) Nếu có &self hoặc &mut self, lifetime của self gán cho tất cả output lifetime. Với quy tắc này, 90% hàm không cần annotation lifetime.

Pattern thực tế: Khi nào dùng gì?

Pattern Ownership Borrowing Clone
Đọc value nhiều lần, không cần giữ   &T  
Sửa value trong scope nhỏ   &mut T  
Lưu value lâu dài, share nhiều chỗ Rc &T vào Rc  
Share + mutate đa thread Arc<Mutex> Lock giữ &mut tạm thời  
Pass giữa async tasks, không có ownership     T: Clone
Return value từ function Move ownership    
Recursive data structure (linked list, tree) Box    

Kiểm soát chi phí clone với smart pointers

Đừng clone bừa bãi. Rust cung cấp tool tối ưu chi phí ownership:

  • Box: Heap allocation, single owner. Dùng khi data lớn, recursive type (linked list, tree), hoặc cần boxed closure (Box). Zero-cost abstraction — chỉ pointer đến heap.
  • Rc + Weak: Reference counting single-thread. Weak phá vòng tham chiếu (reference cycle) thường gặp trong parent-child tree hoặc graph. Chỉ tăng strong count khi clone(), không tăng khi downgrade().
  • Arc + Mutex/RwLock: Shared ownership multi-thread, lock khi cần write. RwLock cho phép multiple readers hoặc single writer — tốt hơn Mutex cho read-heavy workload.
  • Cow (Clone on Write): Tối ưu cho trường hợp thường chỉ read, đôi khi cần write. Tránh clone không cần thiết: Cow::Borrowed cho read, Cow::Owned cho write.

Interior mutability: Mutate qua immutable reference

Đôi khi bạn cần mutate data ngay cả khi chỉ có &T. Rust cung cấp interior mutability qua Cell, RefCell (single-thread) và Atomic*>, Mutex, RwLock (multi-thread). Các type này thực hiện runtime borrow check thay vì compile-time:

  • Cell: Copy type, không borrow check, dùng set()/get(). Dùng cho primitives, small Copy types.
  • RefCell: Non-Copy type, runtime borrow check (panic nếu vi phạm rule). Dùng cho Rc<RefCell> pattern trong graph, observer pattern.
  • OnceCell / LazyLock (Rust 1.70+): One-time initialization, thread-safe. Thay thế lazy_static! macro.

So sánh với GC và manual memory management

Rust ownership mang lại:

  • Deterministic deallocation: Drop ngay khi owner ra scope — không stop-the-world GC pause, predictable latency cho real-time system.
  • Zero-cost abstractions: Borrowing không có overhead so với C/C++ pointer trực tiếp. Move semantics compile xuống memcpy hoặc register shuffle.
  • Compile-time safety: Memory bug (use-after-free, double-free, dangling pointer, data race) bị bắt lỗi trước khi chạy. “If it compiles, it works” — không phải 100% đúng nhưng đúng trong 95% memory issues.
  • Fearless concurrency: Data race là compile-time error, không phải runtime heisenbug. Send/Sync trait tự động đảm bảo thread safety.

Đánh đổi: learning curve cao hơn (khoảng 2-3 tháng để comfortable). Nhưng một khi nắm ownership model, bạn có thể phát triển hệ thống real-time, embedded, game engine mà không lo GC pause hay segfault bất ngờ.

Khi nào chọn Rust thay vì Go/Java/C++?

Rust phù hợp khi bạn cần predictable latency (trading engine, game loop, OS kernel), resource constraint (embedded no-std, Wasm edge computing), hoặc concurrency đa luồng an toàn (high-throughput network server, database, parallel compute). Nếu throughput không phải bottleneck và team thiếu kinh nghiệm Rust, GC language (Go, Java, C#, Node.js) vẫn practical hơn cho business logic, web API, data pipeline.

Bắt đầu với Rust ownership? Hãy viết nhỏ, đọc The Rust Book chapter 4 và thực hành rustlings exercises. Ownership chỉ thật sự trở thành bản năng sau 50+ giờ code. Cộng đồng Rust rất thân thiện — đừng ngần ngại hỏi trên Rust Users Forum hoặc Rust Discord.

Tôi là một lập trình viên IOS. Code chính là IOS nhưng thỉnnh thoảng vẫn đá sang Android hoặc web. Mặc dù không quá thông thạo nhưng tôi sẽ chia sẻ những kiến thức mà mình đã tìm hiểu, áp dụng qua.

Bài viết liên quan

Docker container illustration - khái niệm container hóa

Docker: Container hóa ứng dụng từ cơ bản đến production

Docker là nền tảng mã nguồn mở cho phép đóng gói, vận hành và phân phối ứng dụng trong môi trường được cô lập gọi là container. Khi các ứng…

Xem thêm

Ghostty: Terminal GPU-rendered thay thế iTerm2 và Alacritty

Ghostty: Terminal GPU-rendered thay thế iTerm2 và Alacritty Ghostty là terminal emulator mới phát hành phiên bản 1.0 vào cuối 2024, được viết bằng Zig và sử dụng GPU rendering…

Xem thêm

Feather Robotics: Nền tảng robot mô-đun giá 30k USD cho developer

Feather Robotics, khởi nghiệp humanoid do cựu kỹ sư Tesla và founder trước của 1X thành lập, đang định vị mình là “Android of robotics” — cung cấp nền tảng…

Xem thêm
0 0 đánh giá
Article Rating
Theo dõi
Thông báo của
guest
0 Comments
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất