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 ba 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

PostgreSQL JSONB: Lưu trữ và truy vấn dữ liệu không cấu trúc hiệu năng

PostgreSQL JSONB là gì và tại sao mạnh hơn JSON thuần JSONB (Binary JSON) là kiểu dữ liệu PostgreSQL để lưu trữ và truy vấn dữ liệu JSON dưới dạng…

Xem thêm
Zig programming language logo

Zig vs Rust: So sánh ngôn ngữ system programming an toàn bộ nhớ 2025

Zig và Rust là hai ngôn ngữ system programming hiện đại được thiết kế để thay thế C/C++ với an toàn bộ nhớ (memory safety) và hiệu năng cao. Cả…

Xem thêm

Go 1.23 Iterators: Range Over Functions thay đổi code như thế nào

Go 1.23 Iterators: Range Over Functions thay đổi code như thế nào Go 1.22 đã giới thiệu generics, Go 1.23 tiếp tục đột phá với range over func iterators —…

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