Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Tạo được mutable slice từ array hoặc Vec bằng cú pháp
let s = &mut a[..];và hiểu vì sao storage gốc bắt buộc khai báomut. - Modify một phần tử qua slice bằng
s[i] = valuevà biết thay đổi đó reflect ngay ở storage gốc — slice không copy data. - Sử dụng thành thạo các method mutable in-place:
sort,sort_unstable,reverse,swap(i, j),fill(v),copy_from_slice(&other),rotate_left. - Dùng
split_at_mut(i)để tách một&mut [T]thành hai&mut [T]không overlap — compile-time đảm bảo an toàn aliasing. - Modify từng phần tử qua
iter_mut()với patternfor x in s.iter_mut() { *x *= 2; }— chú ý dereference*x. - Viết function nhận
&mut [T]để modify in-place dữ liệu của caller mà không cần trả về. - Nhớ exclusive borrow rule: tại một thời điểm chỉ tồn tại đúng một
&mutview phủ một range; không đồng thời với&immutable trên cùng vùng. - Hiểu Vec deref coercion sang
&mut [T]qua traitDerefMut, giống&Vec<T>→&[T]đã học ở Bài 77.
Cú Pháp &mut [T]
Mutable slice là một fat pointer 16 byte (ptr + len) cấp quyền đọc và ghi trên một vùng liên tục của storage gốc. Cú pháp gần giống slice immutable, chỉ thêm từ khoá mut ở cả khai báo storage và biểu thức tạo slice.
fn main() {
// Storage gốc PHẢI khai báo mut
let mut a = [1, 2, 3, 4, 5];
// Mutable slice toàn bộ array
let s: &mut [i32] = &mut a[..];
println!("len = {}", s.len()); // 5
println!("first = {:?}", s.first()); // Some(1)
// Mutable sub-slice index 1..4
// (Phải drop slice cũ trước, hoặc tạo trong scope khác)
let mid: &mut [i32] = &mut a[1..4];
println!("mid = {:?}", mid); // [2, 3, 4]
}
Các điểm cú pháp đáng nhớ:
- Storage gốc phải
mut:let mut a = .... Nếu khai báo bằnglet a, compile error "cannot borrowaas mutable, as it is not declared as mutable". - Cụm
&mutđi liền nhau, không phải& mut. Thiếumutbạn chỉ có&[T]immutable. - Range syntax giống slice immutable:
&mut a[..]toàn bộ,&mut a[1..4],&mut a[2..],&mut a[..3],&mut a[0..=2]. - Mutable slice cũng là DST wrapper —
size_of::<&mut [i32]>() == 16, giống&[i32].
Type signature có thể annotate explicit như let s: &mut [i32] = &mut a[..]; hoặc để inference: let s = &mut a[..];. Compiler sẽ tự suy ra &mut [i32] từ context.
Modify Element Qua Index
Có mutable slice rồi, bạn ghi đè phần tử bằng cú pháp index thông thường s[i] = new_value. Thay đổi này reflect trực tiếp ở storage gốc vì slice chỉ là view, không copy data.
fn main() {
let mut a = [10, 20, 30, 40, 50];
{
let s: &mut [i32] = &mut a[..];
// Assign trực tiếp qua index
s[0] = 99;
s[4] = 100;
println!("slice = {:?}", s); // [99, 20, 30, 40, 100]
} // s drop ở đây - giải phóng mutable borrow
// Sau khi slice drop, array gốc đã bị modify
println!("array = {:?}", a); // [99, 20, 30, 40, 100]
// Sub-slice cũng modify được, thay đổi reflect cùng vùng
{
let mid: &mut [i32] = &mut a[1..4];
mid[0] = 200; // tương ứng a[1]
mid[2] = 400; // tương ứng a[3]
}
println!("array = {:?}", a); // [99, 200, 30, 400, 100]
}
Vài điểm cần chốt:
- Index trong slice là local index, bắt đầu từ 0 — không phải index gốc của array. Trong ví dụ trên,
mid[0]làa[1]vì sub-slice bắt đầu từ index 1 của array. - Out-of-bounds vẫn panic runtime:
s[10] = 0trên slice 5 phần tử sẽ panic "index out of bounds: the len is 5 but the index is 10". Khi không chắc, dùngs.get_mut(i)trảOption<&mut T>. - Mutable slice dùng xong cần drop để giải phóng borrow trước khi truy cập array gốc (Rust 2024 NLL/Polonius xử lý tốt hơn nhưng vẫn nên đặt trong scope
{ ... }rõ ràng khi cần). - Vì là view, modify qua slice không có chi phí allocation — chỉ là một store vào memory đã có sẵn.
Method Mutable Phổ Biến
Slice có một bộ method mutable in-place phong phú trong std::slice. Dưới đây là các method dùng hàng ngày nhất — chỉ cần một &mut [T] là dùng được trên cả array lẫn Vec.
fn main() {
let mut a = [5, 2, 4, 1, 3];
let s: &mut [i32] = &mut a[..];
// sort() - ổn định, in-place, O(n log n)
s.sort();
println!("after sort = {:?}", s); // [1, 2, 3, 4, 5]
// reverse() - đảo ngược thứ tự in-place
s.reverse();
println!("after reverse = {:?}", s); // [5, 4, 3, 2, 1]
// swap(i, j) - hoán vị 2 phần tử
s.swap(0, 4);
println!("after swap = {:?}", s); // [1, 4, 3, 2, 5]
// fill(v) - ghi đè TẤT CẢ phần tử bằng v (Rust 1.50+)
s.fill(0);
println!("after fill(0) = {:?}", s); // [0, 0, 0, 0, 0]
// copy_from_slice(&other) - copy từng phần tử từ slice khác
// Hai slice PHẢI cùng len, nếu khác sẽ panic
let src = [10, 20, 30, 40, 50];
s.copy_from_slice(&src);
println!("after copy = {:?}", s); // [10, 20, 30, 40, 50]
// rotate_left(n) - xoay sang trái n vị trí
s.rotate_left(2);
println!("after rot_left = {:?}", s); // [30, 40, 50, 10, 20]
}
Một số ghi chú quan trọng:
sort()là stable sort — giữ thứ tự tương đối của phần tử bằng nhau.sort_unstable()không ổn định nhưng nhanh hơn ~20% và không cần allocate scratch buffer; dùng khi không cần tính ổn định.sort_by(|a, b| b.cmp(a))cho thứ tự custom (ví dụ giảm dần).sort_by_key(|x| x.abs())sort theo derived key.fill_with(|| compute())dùng closure để tạo giá trị mỗi phần tử — hữu ích khi value cần init phức tạp.copy_from_slicechỉ hoạt động choT: Copy. Với type không Copy dùngclone_from_slice.- Mọi method modify đều cần
&mut self— gọi trên&[T]immutable sẽ compile error "cannot borrow as mutable".
split_at_mut — Tách 2 Mutable Slice Không Overlap
Đôi khi bạn cần hai mutable view vào cùng một array — ví dụ trong implement quicksort, merge sort, hoặc khi swap dữ liệu giữa hai vùng. Nhưng exclusive borrow rule cấm có hai &mut cùng lúc lên cùng storage. Giải pháp: split_at_mut(i).
fn main() {
let mut a = [1, 2, 3, 4, 5, 6];
// Tách tại index 3: left = [0..3], right = [3..6]
let (left, right): (&mut [i32], &mut [i32]) = a.split_at_mut(3);
println!("left = {:?}", left); // [1, 2, 3]
println!("right = {:?}", right); // [4, 5, 6]
// Cả hai đều mutable, modify song song
left[0] = 100;
right[2] = 600;
// Drop hai slice trước khi đọc array gốc
drop((left, right));
println!("a = {:?}", a); // [100, 2, 3, 4, 5, 600]
}
Tại sao Rust cho phép hai &mut cùng tồn tại ở đây?
split_at_mut(i)trả về hai slice không overlap:&mut a[..i]và&mut a[i..]. Compiler biết chắc hai vùng memory rời nhau — không có khả năng aliasing nào, nên data race không thể xảy ra.- Hàm này được hiện thực bằng
unsafebên trong stdlib (split raw pointer, tạo 2 slice mới) nhưng expose API safe vì invariant không overlap được kiểm tra runtime: nếui > len, panic ngay. - Pattern này là nền tảng của nhiều thuật toán: quicksort partition, merge sort merge step, parallel processing với rayon (
par_chunks_mut).
Ví dụ thực dụng — swap nội dung giữa nửa đầu và nửa sau của một slice:
fn swap_halves(s: &mut [i32]) {
let mid = s.len() / 2;
let (left, right) = s.split_at_mut(mid);
let n = left.len().min(right.len());
for i in 0..n {
std::mem::swap(&mut left[i], &mut right[i]);
}
}
fn main() {
let mut a = [1, 2, 3, 4, 5, 6];
swap_halves(&mut a);
println!("{:?}", a); // [4, 5, 6, 1, 2, 3]
}
Họ method cùng nhóm còn có split_first_mut(), split_last_mut(), split_at_mut_checked(i) (trả Option), và chunks_mut(n), windows (chỉ immutable vì window overlap).
iter_mut — Modify Qua Iterator
Khi cần áp dụng cùng một biến đổi cho mọi phần tử của slice, iter_mut() là cách idiom nhất. Mỗi vòng lặp đưa ra một &mut T — bạn phải dereference bằng *x để truy cập giá trị thực sự.
fn main() {
let mut a = [1, 2, 3, 4, 5];
let s: &mut [i32] = &mut a[..];
// Double mỗi phần tử
for x in s.iter_mut() {
*x *= 2; // *x deref &mut i32 thành i32
}
println!("{:?}", a); // [2, 4, 6, 8, 10]
// Cách viết tương đương dùng for-loop trên &mut slice
for x in &mut a {
*x += 1;
}
println!("{:?}", a); // [3, 5, 7, 9, 11]
// Kết hợp với enumerate() để có cả index lẫn &mut element
for (i, x) in a.iter_mut().enumerate() {
*x = (i as i32) * 10;
}
println!("{:?}", a); // [0, 10, 20, 30, 40]
}
Vài điểm cần chốt:
iter_mut()trả về một iterator cóItem = &mut T. Bỏ*sẽ là gán reference cho reference — compile error hoặc không có hiệu lực mong muốn.- Cú pháp ngắn gọn
for x in &mut slicetương đươngfor x in slice.iter_mut()— desugar dùng traitIntoIterator. - Trong vòng lặp,
xcó type&mut i32;*xcó typei32;*x = ...ghi đè giá trị,*x *= 2tương đương*x = *x * 2. - Iterator này có đầy đủ adapter combinator:
.enumerate(),.zip(),.take(n),.skip(n). Nhưng không có.map()trả về mutable iterator — vìmaptạo iterator mới chứ không modify in-place. - Cách functional dùng
for_each:s.iter_mut().for_each(|x| *x *= 2);tương đương vòng for thường, nhiều người thấy idiom hơn.
Function Nhận &mut [T]
Pattern phổ biến: tách logic modify in-place thành function nhận &mut [T]. Function modify dữ liệu của caller trực tiếp, caller vẫn giữ ownership array/Vec sau khi gọi.
// Function nhận mutable slice - modify in-place, không trả gì
fn double_all(s: &mut [i32]) {
for x in s.iter_mut() {
*x *= 2;
}
}
// Function clamp tất cả phần tử vào [min, max]
fn clamp_all(s: &mut [i32], min: i32, max: i32) {
for x in s.iter_mut() {
if *x < min {
*x = min;
} else if *x > max {
*x = max;
}
}
}
fn main() {
let mut arr: [i32; 4] = [1, 2, 3, 4];
double_all(&mut arr);
println!("{:?}", arr); // [2, 4, 6, 8]
let mut v: Vec<i32> = vec![-5, 0, 50, 200];
clamp_all(&mut v, 0, 100);
println!("{:?}", v); // [0, 0, 50, 100]
// Caller vẫn sở hữu arr / v đầy đủ sau call
println!("v.len() = {}", v.len()); // 4
}
Lý do &mut [T] là idiom mạnh:
- Linh hoạt nguồn dữ liệu: cùng function gọi được với array (
&mut arr), Vec (&mut vqua deref coercion), sub-slice (&mut v[1..3]) — không buộc caller phải có collection cụ thể. - Không cần trả về: caller giữ ownership nên không cần move data ra rồi gán lại. Tránh được idiom Java-style
arr = transform(arr). - Zero allocation: function chỉ ghi vào memory đã có sẵn của caller, không tạo collection mới.
- Compose tốt với
split_at_mut: function đệ quy chia slice nhỏ dần (sort algorithm) chỉ cần signaturefn quicksort(s: &mut [i32]). - Clippy lint
ptr_argsẽ khuyên đổi&mut Vec<T>thành&mut [T]khi function không cần thao tác Vec-specific (push,resize).
Lưu ý: nếu function thực sự cần resize collection (thêm/bớt phần tử), phải nhận &mut Vec<T> vì &mut [T] chỉ cho phép modify phần tử, không thay đổi length.
Exclusive Borrow Rule Vẫn Apply
Mutable slice tuân theo cùng quy tắc borrow đã học ở Nhóm 10 (Ownership & Borrowing): tại một thời điểm chỉ tồn tại tối đa một &mut phủ một range, và không thể đồng thời có bất kỳ & immutable nào trên cùng vùng đó.
fn main() {
let mut a = [1, 2, 3, 4, 5];
// SAI: hai &mut cùng phủ array
// let s1 = &mut a[..];
// let s2 = &mut a[..]; // error: cannot borrow `a` as mutable more than once
// println!("{:?} {:?}", s1, s2);
// SAI: & immutable cùng tồn tại với &mut
// let r = &a[..];
// let m = &mut a[..]; // error: cannot borrow `a` as mutable because it is also borrowed as immutable
// println!("{:?} {:?}", r, m);
// ĐÚNG: dùng xong slice cũ thì tạo slice mới (NLL)
{
let s1 = &mut a[..];
s1[0] = 10;
} // s1 drop
{
let s2 = &mut a[..];
s2[1] = 20;
}
println!("{:?}", a); // [10, 20, 3, 4, 5]
// ĐÚNG: split_at_mut cho 2 view không overlap
let (left, right) = a.split_at_mut(2);
left[0] = 100;
right[0] = 300;
println!("{:?} {:?}", left, right); // [100, 20] [300, 4, 5]
}
Vài điểm cần nhớ:
- Rule này tồn tại để loại trừ data race ở compile time — không có hai writer cùng lúc, hoặc một writer cùng readers.
- Rust 2024 với NLL (Non-Lexical Lifetimes) và Polonius mở rộng nhận biết: borrow chỉ "sống" đến lần dùng cuối, không kéo dài hết scope. Vì vậy nhiều pattern hợp lệ hơn so với Rust phiên bản đầu.
- Khi compiler từ chối, ba lối thoát phổ biến: (a) thu hẹp scope của borrow bằng
{ ... }, (b) dùngsplit_at_mutđể chia thành hai non-overlapping slice, (c) refactor để chỉ dùng một borrow tại một thời điểm. - Cùng quy tắc áp dụng khi truyền slice vào function — caller không được giữ thêm reference khác đến cùng vùng trong khi function còn đang mượn mutable.
Vec Auto-Coerce Sang &mut [T]
Bài 77 đã học &Vec<T> auto-coerce thành &[T] qua trait Deref. Tương tự, &mut Vec<T> auto-coerce thành &mut [T] qua trait DerefMut — Vec đã implement cả hai trait này. Coercion gần như free về cost.
fn double_all(s: &mut [i32]) {
for x in s.iter_mut() {
*x *= 2;
}
}
fn main() {
let mut v: Vec<i32> = vec![1, 2, 3, 4, 5];
// Cách 1: deref coercion - &mut Vec<i32> tự coerce thành &mut [i32]
let s: &mut [i32] = &mut v;
s[0] = 99;
s.reverse();
println!("{:?}", v); // [5, 4, 3, 2, 99]
// Cách 2: pass &mut Vec vào function nhận &mut [T]
double_all(&mut v);
println!("{:?}", v); // [10, 8, 6, 4, 198]
// Cách 3: index range tạo sub-slice mutable
let mid: &mut [i32] = &mut v[1..4];
mid.sort();
println!("{:?}", v); // [10, 4, 6, 8, 198]
// Cách 4: method explicit as_mut_slice()
let same: &mut [i32] = v.as_mut_slice();
same.fill(0);
println!("{:?}", v); // [0, 0, 0, 0, 0]
}
Vài lưu ý:
- Cả 4 cách trên tạo ra cùng type
&mut [i32]— chọn theo style. Cách 1 và 2 phổ biến nhất. - Mutable slice không cho phép resize Vec: bạn không thể gọi
s.push(6)hays.pop()trên&mut [T]vì những method đó thuộcVec<T>. Nếu cần thêm/bớt phần tử, function phải nhận&mut Vec<T>. - Coercion là chỉ một-chiều:
&mut Vec<T>coerce được sang&mut [T], nhưng&mut [T]không "đi ngược" thành&mut Vec<T>vì slice không có header của Vec. - Tương tự,
&mut Stringauto-coerce thành&mut strquaDerefMut— pattern xuất hiện ở nhiều nơi khi học sang Group 8 (Strings).
Tổng Kết
&mut [T]là fat pointer 16 byte có quyền đọc và ghi trên storage gốc; cú pháplet s = &mut a[..];yêu cầu storage khai báomut.- Modify element qua index
s[i] = valuereflect ngay ở storage gốc — slice không copy data. - Method mutable in-place phổ biến:
sort/sort_unstable,reverse,swap(i, j),fill(v)(Rust 1.50+),copy_from_slice(&other),rotate_left/right. split_at_mut(i)tách&mut [T]thành hai&mut [T]không overlap — Rust safe vì compiler biết hai vùng rời nhau; nền tảng thuật toán sort/divide-and-conquer.iter_mut()trả iterator&mut T; patternfor x in s.iter_mut() { *x *= 2; }— chú ý deref*x.- Function nhận
&mut [T]modify in-place, caller giữ ownership; linh hoạt với array, Vec, sub-slice; zero allocation. - Exclusive borrow rule: 1
&muttại 1 thời điểm, không đồng thời với&immutable; ba lối thoát: thu hẹp scope,split_at_mut, refactor. - Vec auto-coerce sang
&mut [T]qua traitDerefMut; mutable slice không cho phép resize Vec — cần resize thì phải nhận&mut Vec<T>.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Có array
let mut a = [3, 1, 4, 1, 5, 9, 2, 6];. Viết code dùng&mut [i32]để: (a) sort tăng dần, (b) đảo ngược thứ tự, (c) ghi đè 3 phần tử đầu thành0. Sau cùng ina. - Vì sao đoạn code sau compile error, và
split_at_mutsửa lỗi này như thế nào?let mut a = [1, 2, 3, 4]; let left = &mut a[..2]; let right = &mut a[2..]; println!("{:?} {:?}", left, right); - Viết function
fn add_one(s: &mut [i32])tăng mỗi phần tử lên 1 đơn vị. Test với một array[10, 20, 30]và mộtVec<i32>vec![100, 200]. Vì sao cùng function dùng được cho cả hai? - Khác biệt giữa
s.fill(0)và vòngfor x in s.iter_mut() { *x = 0; }? Khi nào hai cái khác nhau về hành vi? - Cho function
fn push_one(v: &mut Vec<i32>) { v.push(1); }. Có thể đổi signature sangfn push_one(s: &mut [i32])không? Vì sao?
Đáp án
let mut a = [3, 1, 4, 1, 5, 9, 2, 6]; { let s = &mut a[..]; s.sort(); // [1, 1, 2, 3, 4, 5, 6, 9] s.reverse(); // [9, 6, 5, 4, 3, 2, 1, 1] } { let head = &mut a[..3]; head.fill(0); // 3 phần tử đầu thành 0 } println!("{:?}", a); // [0, 0, 0, 4, 3, 2, 1, 1]- Đoạn code thật ra compile được với Rust hiện đại (NLL/Polonius) vì compiler nhận ra hai sub-slice không overlap — đây là lý do chính. Tuy nhiên một số pattern phức tạp hơn (ví dụ tạo slice trong loop, hoặc thông qua biến trung gian) compiler không suy ra được an toàn, và sẽ báo lỗi "cannot borrow
aas mutable more than once".split_at_mut(2)là cách chuẩn để có(left, right)trong mọi trường hợp — nó guarantee ở type level rằng hai slice không overlap, không phụ thuộc vào khả năng phân tích của compiler.
Cùng dùng được vì: array coercefn add_one(s: &mut [i32]) { for x in s.iter_mut() { *x += 1; } } fn main() { let mut arr = [10, 20, 30]; add_one(&mut arr); println!("{:?}", arr); // [11, 21, 31] let mut v = vec![100, 200]; add_one(&mut v); // &mut Vec coerce thành &mut [i32] println!("{:?}", v); // [101, 201] }&mut [T; N]→&mut [T](size N tan đi); Vec coerce&mut Vec<T>→&mut [T]quaDerefMut. Đây là idiom Rust khuyến nghị cho function modify in-place.- Về kết quả, hai cách cho output giống nhau. Khác biệt: (a)
fill(0)ngắn gọn và idiom hơn, có thể được tối ưu thànhmemset. (b) Vòngforlinh hoạt hơn — bạn có thể chèn logic khác (ifđiều kiện, dùng indexenumerate). Khi giá trị fill phụ thuộc closure side-effect (ví dụ counter ngoài), dùngfill_with(|| compute())thay vìfill. - Không thể.
pushlà method củaVec<T>, cần thay đổi length và có thể cần allocate buffer mới.&mut [T]chỉ cho phép modify phần tử trong vùng đã có — length cố định, không có header của Vec. Nguyên tắc chung: function chỉ thao tác phần tử dùng&mut [T]; function cần thay đổi cấu trúc collection (resize, push, pop, insert, remove) phải nhận&mut Vec<T>hoặc&mut String.
Bài Tiếp Theo
Bài 80: Slice Trong Function Signature — đi sâu vào idiom function signature đã đề cập sơ bộ trong bài này và Bài 77: vì sao fn sum(s: &[i32]) -> i32 linh hoạt hơn &Vec<T> hay [T; N], các nguyên tắc API design với slice, generic version với impl AsRef<[T]>, và những trường hợp nào nên dùng &Vec<T> dù clippy có warn.
