Mục lục
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ủarlà&mut T; biết hai cách "sửa qua r":*r = new_value(assignment) hoặcr.method(...)(method call). - Hiểu yêu cầu bắt buộc: original
xphải khai báo bằnglet mut x— nếu chỉlet xthì&mut xcompile error (liên hệ Bài 26). - Định nghĩa function
fn foo(s: &mut String)để modify caller'sStringmà khô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&mutcùng lúc) vàE0502(&+&mutconflict) bằng scope/sequential pattern. - Giải thích cơ chế reborrow: vì sao pass
&mut rvào function rồi vẫn dùng tiếprở 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.
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 = 5— nếu chỉlet x = 5thì dòng kế tiếp compile error (mục 3 sẽ demo). - Tạo reference:
&mut xtrả về một mutable reference, ép kiểu bindingrthành&mut i32. - Sửa giá trị: dùng dereference
*rđể truy cập value gốc, sau đó assign= 10. Sau dòng đó,xtrở 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 *.
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.
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ọis.push_str(" world")— modify thẳng vào buffer trên heap củagreet. - Sau call,
greetvẫ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:
- B64 — pass by move (
fn take(s: String)): caller mất ownership sau call. - B68 — pass 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.
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&xhay&mut xnà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ế.
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:
- 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}"); } - 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
r1sau khi tạor2, 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.
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.
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.
Method Call Auto Deref
Khi r là &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.
Tổng Kết
- Mutable reference:
let r = &mut x;tạo reference type&mut T; sửa qua*r = new(assignment) hoặcr.method(...)(method, auto-deref). - Yêu cầu: original phải khai báo
let mut x;&muttrê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&mutcùng active. Fix: scope hoặc sequential (nhờ NLL).E0502:&+&mutcùng active (theo cả hai chiều). Fix tương tự.- Reborrow: compiler tự "mượn tạm" khi pass
&mutvà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.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- 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? - 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ì?
- 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ì? - 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? - 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
- Không compile. Error
E0596: "cannot borrowxas mutable, as it is not declared as mutable". Fix: đổilet x = 5thànhlet mut x = 5. Logic: muốn tạo&mutthì binding gốc phải khai báo mut — đây là cùng keywordmutđã học ở Bài 26. - "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.
- Error
E0499: "cannot borrowxas mutable more than once at a time". Fix (a) scope: đặtr1trong block{ let r1 = &mut x; ... }để nó drop trước khi tạor2; fix (b) sequential: dùng xongr1(lần dùng cuối) rồi mới tạor2— NLL kết thúc borrow tại last-use, không phải cuối block. - Error
E0502: "cannot borrowxas mutable because it is also borrowed as immutable". Hoán đổi (mut trước, immutable sau) vẫn lỗiE0502như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. - Method call (và field access): Rust có auto-deref coercion — thấy
r.somethingvớir: &mut T, compiler tự derefrđể tìm method/field trênT. Vì receiver luôn rõ ràng là "valuertrỏ tới", không có nhập nhằng. Assignmentr = ...ngược lại có nghĩa "rebind biếnrsang reference khác" — hoàn toàn khác "sửa value màrtrỏ tới". Để phân biệt, syntax bắt buộc bạn viết*r = ...khi muốn ghi đè value gốc.
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ế.
