Danh sách bài viết

Bài 69: &mut — Mutable Reference

Bài 69 của series Rust Cơ Bản — học loại reference thứ hai: &mut x — mượn để sửa. Sửa qua reference cần đủ 3 điều kiện: original phải là mut, function nhận &mut T đúng signature, và tuân thủ quy tắc exclusive — tại một thời điểm chỉ được tồn tại đúng 1 mutable reference, không kèm bất kỳ immutable reference nào khác. Vi phạm quy tắc → compile error E0499 (nhiều &mut cùng lúc) hoặc E0502 (mix & với &mut). Đây là cơ chế ngăn data race ngay tại compile-time. Bài đi qua: cú pháp, function modify caller's String, exclusive rule, 2 loại error trên, reborrow tự động khi pass &mut vào function, và auto-deref cho method call vs explicit *r cho assignment.

09/06/2026
11 phút đọc
0 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Viết được let r = &mut x; để tạo mutable reference; biết type của r&mut T; biết hai cách "sửa qua r": *r = new_value (assignment) hoặc r.method(...) (method call).
  • Hiểu yêu cầu bắt buộc: original x phải khai báo bằng let mut x — nếu chỉ let x thì &mut x compile error (liên hệ Bài 26).
  • Định nghĩa function fn foo(s: &mut String) để modify caller's Stringkhông consume ownership — caller vẫn dùng lại được sau call.
  • Phát biểu quy tắc exclusive: tại mọi thời điểm, một value chỉ có thể có một trong hai: nhiều immutable reference hoặc đúng một mutable reference — không bao giờ cả hai.
  • Đọc và fix được hai error phổ biến: E0499 (nhiều &mut cùng lúc) và E0502 (& + &mut conflict) bằng scope/sequential pattern.
  • Giải thích cơ chế reborrow: vì sao pass &mut r vào function rồi vẫn dùng tiếp r ở caller được — compiler "mượn tạm" rồi "trả lại" tự động.
  • Phân biệt khi nào Rust auto-deref (method call r.push_str(...)) và khi nào cần explicit *r (assignment *r = new).

Đây là viên gạch thứ hai (sau &) trong Group 10. Bài kế tiếp B70 sẽ chính thức gọi tên và mở rộng quy tắc exclusive thành "Borrowing Rules" của Rust.

2

Cú Pháp &mut value

Cú pháp tạo mutable reference: dấu & kèm keyword mut đặt trước value. Type của reference là &mut T (đọc: "mutable reference tới T").

fn main() {
    let mut x = 5;          // 1) original phải là mut
    let r = &mut x;        // 2) r có type &mut i32
    *r = 10;                // 3) sửa qua dereference
    println!("{x}");        // in 10 — x đã thay đổi qua r
}

Ba phần cần chú ý trên cùng một đoạn code:

  • Khai báo: let mut x = 5nếu chỉ let x = 5 thì dòng kế tiếp compile error (mục 3 sẽ demo).
  • Tạo reference: &mut x trả về một mutable reference, ép kiểu binding r thành &mut i32.
  • Sửa giá trị: dùng dereference *r để truy cập value gốc, sau đó assign = 10. Sau dòng đó, x trở thành 10.

Với type "có method modify" như String hoặc Vec, có thể gọi thẳng method:

fn main() {
    let mut s = String::from("hello");
    let r = &mut s;          // r: &mut String
    r.push_str(" world");     // method call — Rust tự deref
    println!("{s}");          // "hello world"
}

r.push_str(...) chính là cách viết tắt của (*r).push_str(...) — Rust có auto-deref coercion cho method call, sẽ phân tích sâu ở mục 9. Đối lập, assignment *r = new phải viết tường minh dấu *.

3

Yêu Cầu: Original Phải mut

Bạn không thể mượn mutable từ một biến immutable. Logic đơn giản: nếu x tự nó đã immutable, "cho mượn để sửa" là vô lý.

fn main() {
    let x = 5;                // thiếu mut
    let r = &mut x;          // compile error
    *r = 10;
}

Compiler báo:

error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
 --> src/main.rs:3:14
  |
3 |     let r = &mut x;
  |              ^^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
2 |     let mut x = 5;
  |         +++

Fix theo gợi ý của compiler: đổi thành let mut x = 5;. Đây là cùng một mut bạn đã học ở Bài 26 — chính keyword đó quyết định binding có "cho mượn mutable" được không.

Quan trọng: mut trên binding (let mut x) khác với mut trên reference (&mut x). Cái đầu nói "binding này có thể rebind / mutate". Cái sau nói "reference này có quyền sửa value qua nó". Hai cái thường đi cặp khi muốn modify, nhưng vẫn là hai khái niệm độc lập. Bạn cũng có thể có let mut r = &mut x; — vừa rebind được r sang reference khác, vừa sửa được x qua r.

4

Function Modify Qua &mut

Use case thực tế nhất của &mut: function muốn sửa giá trị của caller nhưng không consume ownership. Caller giữ nguyên binding, gọi function xong vẫn dùng tiếp được.

fn append_world(s: &mut String) {
    s.push_str(" world");
}

fn main() {
    let mut greet = String::from("hello");
    append_world(&mut greet);     // pass &mut greet
    println!("{greet}");           // "hello world"
}

Phân tích từng vai trò:

  • Caller khai báo let mut greet (vì sắp cho mượn mutable) và truyền &mut greet — tạo mutable reference tại call site.
  • Function nhận s: &mut String, gọi s.push_str(" world") — modify thẳng vào buffer trên heap của greet.
  • Sau call, greet vẫn là owner; chỉ có nội dung của nó đã thay đổi. println! đọc bình thường.

So sánh với hai pattern đã học:

  • B64pass by move (fn take(s: String)): caller mất ownership sau call.
  • B68pass by immutable reference (fn read(s: &String)): function chỉ đọc, không sửa được.
  • Bài này — pass by mutable reference (fn modify(s: &mut String)): function sửa được, caller vẫn giữ ownership.

Ba pattern là ba "mức quyền": lấy luôn (move), mượn đọc (&), mượn sửa (&mut). Bạn chọn mức quyền thấp nhất đủ dùng — đó là idiom Rust.

5

Exclusive Rule: Chỉ 1 &mut Tại 1 Thời Điểm

Đây là quy tắc key của bài. Compiler đảm bảo: với một value bất kỳ, tại mọi thời điểm active borrow, chỉ tồn tại đúng một trong hai trạng thái:

  • Trạng thái A — shared: 0 hoặc nhiều &x (immutable reference), KHÔNG có &mut x.
  • Trạng thái B — exclusive: đúng 1 &mut x, KHÔNG có thêm &x hay &mut x nào khác.

Phát biểu rút gọn: "shared XOR mutable, never both". Bạn có thể có vô số người đọc cùng lúc, hoặc đúng một người sửa độc quyền — không có chuyện vừa đọc vừa sửa từ nhiều phía.

Vì sao có quy tắc này? Câu trả lời ngắn: chống data race ngay tại compile-time. Data race xảy ra khi có hai access đồng thời tới cùng một vùng nhớ, ít nhất một là write, và không có synchronization. Bằng cách cấm "shared + mutable" tại mọi thời điểm, compiler chứng minh được không bao giờ tồn tại race condition kiểu đó — kể cả trong code single-thread. Trong multi-thread, quy tắc này lan thành nền tảng cho trait Send/Sync, học sâu ở group Concurrency.

Hệ quả phụ rất tích cực: compiler có thể tối ưu mạnh hơn. Khi thấy &mut x tồn tại, nó biết chắc không ai khác đang xem cùng vùng nhớ — tương tự keyword restrict trong C nhưng được kiểm tra static cho mọi reference. Điều này mở khóa register allocation, reordering, vectorization an toàn.

Hai mục tiếp theo (6 và 7) demo hai cách vi phạm phổ biến và message error tương ứng. Học kỹ chúng giúp bạn debug 80% lỗi borrow checker trong thực tế.

6

Compile Error Nhiều &mut — E0499

Vi phạm trạng thái B: cố tạo 2 mutable reference cùng active. Compiler từ chối với error[E0499].

fn main() {
    let mut x = String::from("hi");
    let r1 = &mut x;
    let r2 = &mut x;          // second mutable borrow
    println!("{r1} {r2}");
}

Output từ cargo check:

error[E0499]: cannot borrow `x` as mutable more than once at a time
 --> src/main.rs:4:14
  |
3 |     let r1 = &mut x;
  |              ------ first mutable borrow occurs here
4 |     let r2 = &mut x;
  |              ^^^^^^ second mutable borrow occurs here
5 |     println!("{r1} {r2}");
  |                ---       first borrow later used here

Compiler chỉ rõ ba điểm: nơi borrow lần đầu, nơi borrow lần hai (chỗ vi phạm), và nơi reference đầu tiên còn được dùng. Borrow đầu vẫn "live" tới dòng 5 nên dòng 4 đụng — đó là vi phạm.

Hai cách fix:

  1. Scope — chia thành hai block tách biệt, borrow đầu hết scope thì borrow sau bắt đầu:
    fn main() {
        let mut x = String::from("hi");
        {
            let r1 = &mut x;
            r1.push_str(" 1");
        }   // r1 drop ở đây
        let r2 = &mut x;
        r2.push_str(" 2");
        println!("{x}");
    }
  2. Sequential — nhờ NLL, borrow kết thúc tại "lần dùng cuối" thay vì cuối block. Nếu không dùng r1 sau khi tạo r2, code chạy được:
    fn main() {
        let mut x = String::from("hi");
        let r1 = &mut x;
        r1.push_str(" 1");
        println!("{r1}");          // r1 dùng lần cuối ở đây
        // r1 không còn active sau dòng trên
        let r2 = &mut x;          // ok, r1 đã hết
        r2.push_str(" 2");
        println!("{x}");
    }

NLL (Non-Lexical Lifetime) là cải tiến của Rust 2018+ giúp borrow checker thông minh hơn — không "đếm scope cứng" mà tính theo last-use. Chi tiết sẽ ở B72.

7

Compile Error Mixed & + &mut — E0502

Vi phạm thứ hai: cùng lúc tồn tại &x (shared) và &mut x (exclusive). Compiler bắt với error[E0502].

fn main() {
    let mut x = String::from("hi");
    let r1 = &x;              // immutable borrow
    let r2 = &mut x;          // mutable borrow trong lúc r1 còn active
    println!("{r1} {r2}");
}

Output:

error[E0502]: cannot borrow `x` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:14
  |
3 |     let r1 = &x;
  |              -- immutable borrow occurs here
4 |     let r2 = &mut x;
  |              ^^^^^^ mutable borrow occurs here
5 |     println!("{r1} {r2}");
  |                ---       immutable borrow later used here

Đối xứng với E0499: hai loại reference "đụng" nhau ngay tại điểm tạo cái thứ hai. Phía nào tạo trước, phía nào tạo sau không quan trọng — quy tắc symmetric. Nếu hoán đổi:

    let r1 = &mut x;          // mutable borrow trước
    let r2 = &x;              // immutable trong lúc &mut còn active
    println!("{r1} {r2}");

Vẫn là E0502 (chỉ đảo "as mutable because also borrowed as immutable" → "as immutable because also borrowed as mutable"). Cùng một quy tắc, cùng một fix: sắp xếp sao cho hai reference không sống cùng thời điểm.

Fix giống mục 6: scope (đóng block immutable trước khi mở mutable) hoặc sequential (dùng r1 xong rồi mới let r2 = &mut x). Nguyên tắc tinh thần: kiểm tra xem ở điểm tạo reference mới, có reference nào kiểu khác còn được dùng sau đó không. Nếu có → vi phạm.

8

Reborrow — Pass &mut Vào Function

Đọc nguyên quy tắc exclusive, có một câu hỏi tự nhiên: nếu tôi đã có let r = &mut x, rồi pass r vào một function nhận &mut T, không phải là tạo "mutable reference thứ hai" sao? Trong function tồn tại r2, ngoài caller tồn tại r — vi phạm?

Câu trả lời: không vi phạm, nhờ cơ chế reborrow. Compiler tự động xử lý: khi pass r vào function, nó được "mượn tạm" — tạo một mutable reference mới sống trong scope function, và r ở caller tạm thời bị đông cứng đúng trong lúc function chạy. Sau khi function trả về, "mượn tạm" hết hiệu lực, r ở caller được "trả lại" để dùng tiếp.

fn append_x(s: &mut String) {
    s.push('x');
}

fn main() {
    let mut data = String::from("hi");
    let r = &mut data;

    append_x(r);           // reborrow: compiler tạo &mut tạm cho call
    append_x(r);           // r vẫn dùng được sau call đầu

    r.push_str("!");       // dùng thẳng r
    println!("{data}");    // "hixx!"
}

Bạn không phải viết gì đặc biệt — compiler tự reborrow. Nếu viết tường minh, dạng đầy đủ sẽ là append_x(&mut *r): dereference r ra value gốc, rồi tạo mutable reference mới từ đó. Compiler chèn dấu này ngầm cho bạn ở chỗ đối số function.

Cơ chế này giải thích vì sao code "tự nhiên" như r.push_str("a"); foo(r); r.push_str("b"); chạy được dù trông như có nhiều &mut tồn tại. Mỗi method call hoặc function call thực ra dùng một reborrow ngắn ngủi, không xung đột với chính r.

Reborrow là chủ đề chính của B73 — bài đó sẽ phân tích chi tiết closure, mutable chain method, và case edge khi compiler không tự reborrow được mà phải viết tường minh &mut *r.

9

Method Call Auto Deref

Khi r&mut String và bạn viết r.push_str(" world"), kỳ thực bạn đang gọi method trên String (kiểu bên dưới reference). Đầy đủ phải là (*r).push_str(" world"). Rust làm việc này cho bạn bằng auto-deref coercion: thấy phép gọi method, compiler thử r, không có method tương ứng thì thử *r, rồi **r... cho tới khi tìm được.

fn main() {
    let mut s = String::from("hello");
    let r = &mut s;

    r.push_str(" world");      // auto-deref: tương đương (*r).push_str(...)
    r.push('!');               // auto-deref
    let len = r.len();         // auto-deref cho immutable method

    println!("{r} ({len})");   // "hello world! (12)"
}

Auto-deref áp dụng cho cả immutable method (như .len()) lẫn mutable method (.push_str). Compiler chọn signature đúng: nếu method nhận &self, nó coerce &mut thành & (downgrade tạm); nếu method nhận &mut self, dùng thẳng.

Có một chỗ không auto-deref: assignment trực tiếp lên reference. Nếu muốn ghi đè toàn bộ value gốc, phải viết tường minh *r = new_value:

fn main() {
    let mut n = 5;
    let r = &mut n;

    *r = 10;            // assignment qua dereference — bắt buộc *
    // r = 10;          // rebind r (sai type) — không phải sửa n

    let mut s = String::from("hi");
    let r2 = &mut s;
    *r2 = String::from("bye");    // ghi đè toàn bộ String mới
    println!("{s}");              // "bye"
}

Lý do tách bạch: r = ... sẽ ép Rust hiểu là "rebind biến r sang reference khác" — hoàn toàn khác ý đồ "sửa value mà r trỏ tới". Để tránh nhập nhằng, syntax bắt buộc bạn viết *r = ... khi muốn sửa qua reference. Còn method call thì rõ ràng (lúc nào cũng là tác động lên receiver), nên Rust tha cho dấu *.

Quy tắc nhớ: method call → auto-deref được, không cần *; field access (r.field) → auto-deref được; assignment (*r = ...) hoặc field assignment ((*r).field = ...) → cần * tường minh.

10

Tổng Kết

  • Mutable reference: let r = &mut x; tạo reference type &mut T; sửa qua *r = new (assignment) hoặc r.method(...) (method, auto-deref).
  • Yêu cầu: original phải khai báo let mut x; &mut trên biến immutable → E0596.
  • Function modify caller: fn foo(s: &mut String) sửa được nội dung mà không consume ownership; caller dùng tiếp bình thường.
  • Quy tắc exclusive: tại 1 thời điểm, value chỉ có một trong hai — nhiều & hoặc đúng 1 &mut, không bao giờ cả hai. Mục đích: chống data race compile-time + cho phép compiler tối ưu.
  • E0499: hai &mut cùng active. Fix: scope hoặc sequential (nhờ NLL).
  • E0502: & + &mut cùng active (theo cả hai chiều). Fix tương tự.
  • Reborrow: compiler tự "mượn tạm" khi pass &mut vào function, sau call trả lại để caller dùng tiếp — không vi phạm exclusive.
  • Auto-deref cho method call và field access (r.push_str(...), r.field); cần * tường minh cho assignment (*r = new) để không nhập nhằng với rebind.
11

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Code let x = 5; let r = &mut x; *r = 10; compile được không? Nếu không, error code là gì và fix thế nào?
  2. Phát biểu quy tắc exclusive bằng một câu duy nhất. Vì sao quy tắc này tồn tại — lý do quan trọng nhất là gì?
  3. Cho code let mut x = 1; let r1 = &mut x; let r2 = &mut x; println!("{r1} {r2}");. Compiler báo error nào? Hai cách fix là gì?
  4. Cho code let mut x = 1; let r1 = &x; let r2 = &mut x; println!("{r1} {r2}");. Error nào? Vì sao việc hoán đổi thứ tự (mut trước, immutable sau) vẫn lỗi?
  5. Tại sao r.push_str("a") không cần dấu * nhưng *r = String::new() bắt buộc phải có?
Đáp án
  1. Không compile. Error E0596: "cannot borrow x as mutable, as it is not declared as mutable". Fix: đổi let x = 5 thành let mut x = 5. Logic: muốn tạo &mut thì binding gốc phải khai báo mut — đây là cùng keyword mut đã học ở Bài 26.
  2. "Tại mọi thời điểm, một value chỉ có thể có nhiều immutable reference HOẶC đúng một mutable reference — không bao giờ cả hai cùng lúc." Lý do quan trọng nhất: chống data race ngay tại compile-time — nếu cùng tồn tại reader và writer (hoặc nhiều writer), không có cách nào đảm bảo nhất quán; bằng cách cấm trạng thái đó static, compiler chứng minh chương trình free khỏi data race kể cả khi mở rộng sang multi-thread.
  3. Error E0499: "cannot borrow x as mutable more than once at a time". Fix (a) scope: đặt r1 trong block { let r1 = &mut x; ... } để nó drop trước khi tạo r2; fix (b) sequential: dùng xong r1 (lần dùng cuối) rồi mới tạo r2 — NLL kết thúc borrow tại last-use, không phải cuối block.
  4. Error E0502: "cannot borrow x as mutable because it is also borrowed as immutable". Hoán đổi (mut trước, immutable sau) vẫn lỗi E0502 nhưng message đảo ngược: "as immutable because also borrowed as mutable". Quy tắc symmetric: bất kỳ thời điểm nào có cả shared và exclusive reference cùng tồn tại đều bị cấm — không phụ thuộc thứ tự ai có trước.
  5. Method call (và field access): Rust có auto-deref coercion — thấy r.something với r: &mut T, compiler tự deref r để tìm method/field trên T. Vì receiver luôn rõ ràng là "value r trỏ tới", không có nhập nhằng. Assignment r = ... ngược lại có nghĩa "rebind biến r sang reference khác" — hoàn toàn khác "sửa value mà r trỏ tới". Để phân biệt, syntax bắt buộc bạn viết *r = ... khi muốn ghi đè value gốc.
12

Bài Tiếp Theo

Bài 70: Quy Tắc Borrowing — Multiple Immutable Hoặc 1 Mutable — chính thức gọi tên và hệ thống hóa hai quy tắc bạn đã thấy ở bài này: rule aliasing (shared XOR mutable), rule lifetime (reference không sống lâu hơn data nó trỏ tới). Bài đó tổng kết toàn bộ borrow checker, đưa ra mental model để đọc bất kỳ lỗi borrow nào trong thực tế.