Danh sách bài viết

Bài 79: Mutable Slice — &mut [T]

Bài 79 của series Rust Cơ Bản — đi sâu vào dạng mutable slice &mut [T] trong Rust 2024, một view có quyền ghi trực tiếp lên storage gốc (array, Vec) mà không cần move hay reallocate. Bao trùm cú pháp &mut a[..], modify element qua index, method workhorse sort / reverse / swap / fill / copy_from_slice, trick split_at_mut để có 2 mutable view không overlap, pattern iter_mut với deref *x, function signature fn double_all(s: &mut [i32]), exclusive borrow rule, và Vec auto-coerce sang &mut [T]. Mutable slice là API chính khi cần modify in-place an toàn.

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

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áo mut.
  • Modify một phần tử qua slice bằng s[i] = value và 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 pattern for 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 &mut view 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 trait DerefMut, giống &Vec<T>&[T] đã học ở Bài 77.
2

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ằng let a, compile error "cannot borrow a as mutable, as it is not declared as mutable".
  • Cụm &mut đi liền nhau, không phải & mut. Thiếu mut bạ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.

3

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]a[1] vì sub-slice bắt đầu từ index 1 của array.
  • Out-of-bounds vẫn panic runtime: s[10] = 0 trê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ùng s.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.
4

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()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_slice chỉ hoạt động cho T: Copy. Với type không Copy dùng clone_from_slice.
  • Mọi method modify đều cần &mut self — gọi trên &[T] immutable sẽ compile error "cannot borrow as mutable".
5

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]&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 unsafe bê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ếu i > 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).

6

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 slice tương đương for x in slice.iter_mut() — desugar dùng trait IntoIterator.
  • Trong vòng lặp, x có type &mut i32; *x có type i32; *x = ... ghi đè giá trị, *x *= 2 tươ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ì map tạ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.
7

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 v qua 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 signature fn quicksort(s: &mut [i32]).
  • Clippy lint ptr_arg sẽ 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>&mut [T] chỉ cho phép modify phần tử, không thay đổi length.

8

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ùng split_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.
9

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) hay s.pop() trên &mut [T] vì những method đó thuộc Vec<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 String auto-coerce thành &mut str qua DerefMut — pattern xuất hiện ở nhiều nơi khi học sang Group 8 (Strings).
10

Tổng Kết

  • &mut [T] là fat pointer 16 byte có quyền đọc và ghi trên storage gốc; cú pháp let s = &mut a[..]; yêu cầu storage khai báo mut.
  • Modify element qua index s[i] = value reflect 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; pattern for 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 &mut tạ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 trait DerefMut; mutable slice không cho phép resize Vec — cần resize thì phải nhận &mut Vec<T>.
11

Bài Tập Củng Cố

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

  1. 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ành 0. Sau cùng in a.
  2. Vì sao đoạn code sau compile error, và split_at_mut sử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);
  3. 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ột Vec<i32> vec![100, 200]. Vì sao cùng function dùng được cho cả hai?
  4. Khác biệt giữa s.fill(0) và vòng for x in s.iter_mut() { *x = 0; }? Khi nào hai cái khác nhau về hành vi?
  5. Cho function fn push_one(v: &mut Vec<i32>) { v.push(1); }. Có thể đổi signature sang fn push_one(s: &mut [i32]) không? Vì sao?
Đáp án
  1. 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]
  2. Đ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 a as 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.
  3. fn 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]
    }
    Cùng dùng được vì: array coerce &mut [T; N]&mut [T] (size N tan đi); Vec coerce &mut Vec<T>&mut [T] qua DerefMut. Đây là idiom Rust khuyến nghị cho function modify in-place.
  4. 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ành memset. (b) Vòng for linh hoạt hơn — bạn có thể chèn logic khác (if điều kiện, dùng index enumerate). Khi giá trị fill phụ thuộc closure side-effect (ví dụ counter ngoài), dùng fill_with(|| compute()) thay vì fill.
  5. Không thể. push là method của Vec<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.
12

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.