Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu slice là một view động độ dài vào sequence — không sở hữu (own) data, chỉ giữ con trỏ và độ dài tới phần tử của storage khác (array, Vec, String buffer).
- Biết type bản chất
[T]là một Dynamically Sized Type (DST) — không có size biết được ở compile time, nên không thể dùng trực tiếp. Phải luôn truy cập qua reference&[T],&mut [T], hoặc smart pointerBox<[T]>/Arc<[T]>. - Hiểu vì sao
&[T]là fat pointer 16 byte trên hệ thống 64-bit (8 byte ptr + 8 byte len), khác với&Tthông thường chỉ 8 byte. - Thành thạo cú pháp range
0..5,0..=5,..5,2..,..để cắt slice từ array hoặc Vec. - Sử dụng mutable slice
&mut [T]để modify in-place qua các methodswap,reverse,sort; nhớ rule "chỉ 1 mutable slice tại một thời điểm". - Viết function signature idiom
fn sum(arr: &[i32]) -> i32thay vì&Vec<i32>hoặc[i32; N]cố định — caller có thể truyền array, Vec hoặc sub-slice. - Biết slice là foundation cho
&str(Group 8 / Group 11), Vec slicing, byte buffer parsing và zero-copy view trên dữ liệu lớn.
Slice Là Gì
Slice là một view dynamically-sized vào một chuỗi (sequence) liên tục các phần tử cùng kiểu trong bộ nhớ. Trọng tâm: slice không sở hữu data — nó chỉ giữ một con trỏ tới phần tử đầu và một độ dài. Storage thực tế có thể là một array [T; N] trên stack, một Vec<T> trên heap, hoặc thậm chí một Box<[T]>.
Type bản chất của slice là [T] — đọc là "slice of T". Type này có một đặc tính quan trọng: Dynamically Sized Type (DST), hay còn gọi là unsized type. Compile time không biết một [T] có bao nhiêu phần tử — số lượng chỉ xác định khi runtime. Vì compiler cần biết size của mọi value đặt trên stack, bạn không thể khai báo biến kiểu [T] trực tiếp.
// SAI: [i32] là DST, không có size ở compile time
// let x: [i32] = [1, 2, 3]; // compile error: the size for values of type `[i32]` cannot be known
// ĐÚNG: dùng qua các "wrapper" có size biết được
let r: &[i32] = &[1, 2, 3]; // fat pointer 16 byte
let m: &mut [i32] = &mut [1, 2, 3]; // fat pointer 16 byte
let b: Box<[i32]> = Box::new([1, 2, 3]); // smart pointer fat 16 byte trên heap
Có thể hình dung [T] giống một mảng "không biên" — bạn chỉ chạm vào nó thông qua một con trỏ kèm theo độ dài. Khi nói "slice" trong ngôn ngữ thường ngày, người ta thường ám chỉ &[T] hoặc &mut [T] chứ ít khi nói riêng về [T] bare DST.
Phân biệt với array [T; N] đã học ở Bài 41-42: array có size là phần của type, biết ở compile time, lưu thẳng trên stack và sở hữu N phần tử. Slice ngược lại — size động, không own data.
Fat Pointer — Khác Reference Thường
Một reference thông thường &T tới một type có size biết được (sized type) chỉ chứa một con trỏ — trên hệ thống 64-bit là 8 byte. Reference tới slice phải mang thêm thông tin runtime về độ dài, vì compiler không biết slice có bao nhiêu phần tử. Cách Rust giải quyết: ghép độ dài thẳng vào reference, tạo thành fat pointer 16 byte.
use std::mem::size_of;
fn main() {
// Reference thường tới sized type: 8 byte trên 64-bit
println!("size_of::<&i32>() = {}", size_of::<&i32>()); // 8
println!("size_of::<&[i32; 5]>() = {}", size_of::<&[i32; 5]>()); // 8
// Slice reference: fat pointer 16 byte = 8 byte ptr + 8 byte len
println!("size_of::<&[i32]>() = {}", size_of::<&[i32]>()); // 16
println!("size_of::<&mut [i32]>() = {}", size_of::<&mut [i32]>()); // 16
println!("size_of::<&str>() = {}", size_of::<&str>()); // 16 (str cũng là DST)
// Box<[T]> cũng fat 16 byte vì wrap [T] DST
println!("size_of::<Box<[i32]>>() = {}", size_of::<Box<[i32]>>()); // 16
}
Biểu diễn in-memory của một &[T]:
// (Khái niệm, không phải code chạy)
struct SliceRef<T> {
ptr: *const T, // 8 byte: trỏ tới phần tử đầu tiên của slice
len: usize, // 8 byte: số phần tử
} // tổng 16 byte trên 64-bit
Vài điểm cần chốt:
- Length nằm cạnh con trỏ, không nằm ở memory mà ptr trỏ tới — vì storage gốc (array hay Vec) không nhất thiết lưu length theo format slice mong muốn.
&[i32; 5](reference tới array sized) vẫn là 8 byte vì size 5 nằm trong type, không cần lưu runtime.- Khi viết hàm nhận
&[T], bạn pass 16 byte; nhận&Vec<T>, bạn pass 8 byte (con trỏ tới Vec header trên stack) — cost rất nhỏ ở cả hai phía, không phải lý do để chọn cái nào. &str,&dyn Trait,Box<[T]>,Rc<[T]>,Arc<[T]>đều là fat pointer 16 byte. Family này xuất hiện ở khắp Rust — biết một là biết hết.
Tạo Slice Từ Array
Cách phổ biến nhất để có một slice: dùng cú pháp &array[range]. Operator index [..] với một range trả về slice, không phải single element.
fn main() {
let a = [1, 2, 3, 4, 5];
// Slice 3 phần tử giữa: index 1, 2, 3 (exclusive 4)
let s: &[i32] = &a[1..4];
println!("{:?} len={}", s, s.len()); // [2, 3, 4] len=3
// Full slice - tương đương &a, nhờ deref coercion
let full: &[i32] = &a[..];
println!("{:?}", full); // [1, 2, 3, 4, 5]
// Từ đầu đến index 3 (exclusive)
let head = &a[..3];
println!("{:?}", head); // [1, 2, 3]
// Từ index 2 đến hết
let tail = &a[2..];
println!("{:?}", tail); // [3, 4, 5]
// Inclusive range (Rust 1.26+)
let inc = &a[0..=2];
println!("{:?}", inc); // [1, 2, 3]
}
Các dạng range syntax áp dụng với slice indexing:
a..b—std::ops::Range, exclusive cận trên. Phổ biến nhất.a..=b—RangeInclusive, bao gồm cận trên...b—RangeTo, từ đầu đến exclusive b...=b—RangeToInclusive, từ đầu đến inclusive b.a..—RangeFrom, từ a đến hết...—RangeFull, toàn bộ.
Out-of-bounds range vẫn panic runtime như indexing array đã học ở Bài 42: &a[1..10] với array 5 phần tử sẽ panic "range end index 10 out of range for slice of length 5". Khi range không chắc chắn hợp lệ, dùng a.get(1..10) trả Option<&[i32]>.
Slice Từ Vec — Deref Coercion
Vec<T> (Group 16 sẽ học sâu) là dynamic array trên heap. Vec implement Deref<Target = [T]>, nghĩa là một &Vec<T> có thể tự động coerce thành &[T] khi context cần. Tính năng này gọi là deref coercion.
fn main() {
let v: Vec<i32> = vec![10, 20, 30, 40, 50];
// Cách 1: deref coercion - &Vec<i32> tự coerce thành &[i32]
let s: &[i32] = &v;
println!("{:?} len={}", s, s.len()); // [10, 20, 30, 40, 50] len=5
// Cách 2: index với range, giống array
let mid: &[i32] = &v[1..4];
println!("{:?}", mid); // [20, 30, 40]
// Cách 3: explicit method
let same: &[i32] = v.as_slice();
println!("{:?}", same); // [10, 20, 30, 40, 50]
// Cả 3 cách trên đều tạo cùng kiểu &[i32] - chọn theo style
}
Sự khác nhau khi nhận tham số đầu vào:
- Hàm nhận
&Vec<i32>: chỉ chấp nhận reference tới Vec. Pass array hoặc sub-slice là compile error. - Hàm nhận
&[i32]: chấp nhận cả reference tới array (qua coercion[T; N]→[T]), reference tới Vec (qua deref coercion), và sub-slice. Đây là idiom Rust khuyến nghị. - Hàm nhận
[i32; N]: chỉ chấp nhận array cố định đúng size N. Cứng, hiếm khi dùng cho input.
Việc Vec deref được thành [T] là vì Vec lưu data trên heap như một mảng liên tục — buffer thực có cấu trúc giống hệt array. Header của Vec (ptr, len, cap) nằm trên stack; slice chỉ cần ptr + len, là tập con thông tin đã có sẵn trong Vec. Coercion gần như free về cost.
Mutable Slice &mut [T]
Slice cũng có dạng mutable &mut [T]. Qua nó bạn modify được phần tử của storage gốc in-place, không cần move hay re-allocate.
fn main() {
let mut a = [1, 2, 3, 4, 5];
// Mutable slice toàn bộ array
let s: &mut [i32] = &mut a[..];
s[0] = 99;
s[4] = 100;
println!("{:?}", s); // [99, 2, 3, 4, 100]
// Method modify in-place
s.swap(1, 3); // hoán vị index 1 và 3
println!("after swap: {:?}", s); // [99, 4, 3, 2, 100]
s.reverse();
println!("after reverse: {:?}", s); // [100, 2, 3, 4, 99]
s.sort();
println!("after sort: {:?}", s); // [2, 3, 4, 99, 100]
// sort_unstable() nhanh hơn (~20%) khi không cần ổn định
// sort_by(|a, b| b.cmp(a)) cho thứ tự tuỳ ý
// Mutable slice qua Vec cũng OK
let mut v = vec!["banana", "apple", "cherry"];
let vs: &mut [&str] = &mut v;
vs.sort();
println!("{:?}", v); // ["apple", "banana", "cherry"]
}
Quy tắc borrow áp dụng nguyên vẹn cho mutable slice (sẽ học sâu ở Group 10):
- Storage gốc (
a,v) phải khai báomut. - Tại một thời điểm chỉ tồn tại tối đa một
&mut [T]phủ một range cho trước; cũng không thể đồng thời có&[T]immutable trên cùng vùng. - Muốn chia thành hai mutable slice không overlap, dùng
slice.split_at_mut(i)trả(&mut [T], &mut [T])— compiler đảm bảo hai phần không đè nhau. sort(),reverse(),swap(),fill(),rotate_left()đều cần&mut self; gọi trên&[T]immutable sẽ compile error.
Slice Method Phổ Biến
Slice có hàng trăm method trong stdlib (std::slice). Dưới đây là các method dùng hàng ngày nhất — đáng nhớ ngay từ bài đầu.
fn main() {
let s = &[10, 20, 30, 40, 50][..];
println!("len = {}", s.len()); // 5
println!("empty = {}", s.is_empty()); // false
println!("first = {:?}", s.first()); // Some(10)
println!("last = {:?}", s.last()); // Some(50)
println!("contains = {}", s.contains(&30)); // true
// binary_search yêu cầu slice đã sort
println!("bin search = {:?}", s.binary_search(&30)); // Ok(2)
// Chia làm 2 tại index
let (left, right) = s.split_at(2);
println!("left={:?} right={:?}", left, right); // [10, 20] [30, 40, 50]
// Sum qua iterator
let total: i32 = s.iter().sum();
println!("total = {}", total); // 150
}
Hai method đặc biệt hữu ích cho bài toán xử lý dãy theo nhóm: chunks và windows.
fn main() {
let v = [1, 2, 3, 4, 5];
// chunks(n) - chia liên tiếp không overlap, batch cuối có thể ngắn hơn
for chunk in v.chunks(2) {
println!("chunk: {chunk:?}");
}
// chunk: [1, 2]
// chunk: [3, 4]
// chunk: [5]
// windows(n) - sliding window size n, di chuyển 1 phần tử mỗi bước
for w in v.windows(3) {
println!("window: {w:?}");
}
// window: [1, 2, 3]
// window: [2, 3, 4]
// window: [3, 4, 5]
}
Khi nào dùng cái nào: chunks hợp cho batch processing (chia file theo block, vẽ table cột); windows hợp cho moving average, n-gram, phát hiện pattern liên tiếp. Cả hai đều trả iterator của &[T] — sub-slice, không copy data.
Slice Trong Function Signature
Đây là idiom quan trọng nhất của Rust mà bài này muốn bạn nhớ: khi viết function nhận input một dãy phần tử, hãy nhận &[T] thay vì &Vec<T> hay [T; N] cố định. Lý do là tính linh hoạt: cùng một hàm gọi được cho cả array, Vec lẫn sub-range.
fn sum(arr: &[i32]) -> i32 {
arr.iter().sum()
}
fn main() {
let arr_static: [i32; 3] = [1, 2, 3];
let vec_dynamic: Vec<i32> = vec![4, 5, 6, 7];
println!("{}", sum(&arr_static)); // 6
println!("{}", sum(&vec_dynamic)); // 22 (4+5+6+7)
println!("{}", sum(&vec_dynamic[1..3])); // 11 (5+6)
}
Cùng một sum nhận được cả ba dạng đầu vào:
&arr_static— reference tới array fix size 3, coerce thành&[i32].&vec_dynamic— reference tới Vec, deref coercion thành&[i32].&vec_dynamic[1..3]— sub-slice 2 phần tử ở giữa Vec, đã là&[i32]sẵn.
So sánh với hai signature kém linh hoạt:
fn sum(arr: &Vec<i32>)— chỉ Vec hợp lệ. Pass&arr_staticcompile error "expected&Vec<i32>, found&[i32; 3]". Caller phải convert sang Vec, tốn allocation vô ích.fn sum(arr: &[i32; 3])— chỉ array đúng 3 phần tử. Vec, array 4 phần tử, sub-slice đều fail.
Clippy có lint ptr_arg sẽ warn khi bạn viết &Vec<T> / &String trong function signature và gợi ý đổi sang &[T] / &str. Tuân theo lint này — code idiomatic và caller-friendly hơn.
Vì Sao Slice Quan Trọng
Slice không chỉ là chiêu cú pháp tiện lợi — nó là một trong những concept nền tảng giúp Rust đạt được vừa an toàn vừa zero-cost. Mỗi lần bạn thấy slice, hãy nhớ những hệ quả sau:
- String slice
&str(Group 8 và Group 11) chính là một slice — cụ thể là&[u8]kèm bất biến UTF-8. Mọi rule fat pointer, range, deref coercion đã học ở đây áp dụng nguyên vẹn cho&str:&Stringderef được thành&strgiống Vec deref thành&[T]. - Vec slicing: khi cần xử lý một phần Vec mà không cắt thực, slice là cách rẻ nhất. Tạo
&v[a..b]chỉ tốn 16 byte stack, không allocate gì trên heap. - Byte buffer / protocol parsing: đọc TCP/HTTP/binary protocol nghĩa là cắt buffer
&[u8]liên tục theo header. Slice cho phép view nhiều phần tử của cùng buffer song song mà không copy — pattern này gọi là zero-copy parsing, nền tảng của hyper, tokio, serde, prost. - API ergonomics: hàm nhận
&[T]giúp caller không bị ép vào collection type cụ thể. Library nổi tiếng (itertools,regex,memchr,bytes) đều dùng&[T]/&strở mọi nơi có thể. - Memory safety không cost: slice mang theo length nên bound check chỉ là so sánh 2 số nguyên, compiler thường elide được trong loop. Bạn có toàn bộ an toàn của reference + array, không thêm allocation.
Trong Group 11 - Slices, bạn sẽ học sâu hơn: string slice và UTF-8 boundary, split_at_mut trick, idiom function signature, pitfall index out-of-bound trên chuỗi unicode. Bài này chỉ là preview để hiểu ngay cú pháp khi đọc code Rust trong các bài Vec, struct, trait sắp tới.
Tổng Kết
Tổng kết bài 43:
- Slice là view dynamically-sized vào một sequence; không own data, chỉ giữ ptr + len.
- Type bare
[T]là DST — không dùng trực tiếp, phải qua&[T],&mut [T]hoặcBox<[T]>. &[T]là fat pointer 16 byte trên 64-bit (8 byte ptr + 8 byte len), khác&Tsized chỉ 8 byte.- Tạo từ array:
&a[1..4]; tạo từ Vec:&vqua deref coercion, hoặc&v[1..4]. - Range syntax:
a..bexclusive,a..=binclusive,..b,a..,..full. &mut [T]modify in-place quaswap,reverse,sort; tuân quy tắc borrow.- Method phổ biến:
len,is_empty,first,last,iter,contains,binary_search,split_at,chunks,windows. - Idiom function signature:
fn f(arr: &[T])linh hoạt cho array, Vec, sub-slice — tránh&Vec<T>hay[T; N]cố định.
Tổng kết Nhóm 6 - Compound Types:
- Bài 39 Tuple Basic: nhóm giá trị khác loại, fixed arity, access qua
t.0,t.1. - Bài 40 Tuple Destructure:
let (a, b, c) = t;,_ignore,..rest, swap idiom Rust 1.59+. - Bài 41 Array Fixed Size:
[T; N], size là phần của type, init shorthand[0; 100], stack-allocated. - Bài 42 Array Indexing:
a[i]panic out-of-bounds,a.get(i)trả Option,iter/iter_mut. - Bài 43 Slice Preview:
&[T]/&mut [T]fat pointer view, deref coercion từ Vec, idiom signature.
Sau Nhóm 6 bạn đã đủ data structure cơ bản để xây hàm thực thụ. Nhóm 7 chuyển sang Functions & Control Flow — định nghĩa hàm bài bản, parameter, return type, if/else, loop/while/for, expression vs statement.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Giải thích vì sao
let x: [i32] = [1, 2, 3];compile error trong khilet x: &[i32] = &[1, 2, 3];hợp lệ. Khái niệm nào của Rust giải thích sự khác biệt? - Trên hệ thống 64-bit,
size_of::<&[i32]>()vàsize_of::<&[i32; 5]>()trả về giá trị bao nhiêu? Vì sao khác nhau? - Có Vec
let v = vec![10, 20, 30, 40, 50];. Viết một dòng tạo slice gồm 3 phần tử cuối. Có ít nhất 2 cách viết — liệt kê cả hai. - Cho hàm
fn first(s: &[i32]) -> Option<&i32> { s.first() }. Hàm này gọi được với những loại input nào? Tại sao thiết kế&[T]được ưu tiên hơn&Vec<T>? - Khác biệt giữa
v.chunks(3)vàv.windows(3)vớiv = [1, 2, 3, 4, 5]? Output từng method là gì?
Đáp án
- Type
[i32]là một Dynamically Sized Type (DST) — số phần tử không biết ở compile time, nên compiler không biết phải reserve bao nhiêu byte trên stack cho biếnx. Rust quy định biến local phải có size biết được.&[i32]là fat pointer luôn có size cố định 16 byte (ptr + len), nên khai báo được. Cùng quy tắc giải thích vì sao cóBox<[T]>,Rc<[T]>: smart pointer cũng đóng vai trò wrapper sized để chứa DST. size_of::<&[i32]>() = 16(fat pointer: 8 byte ptr + 8 byte len).size_of::<&[i32; 5]>() = 8(thin pointer, vì array[i32; 5]là sized type — size 5 đã có trong type, không cần lưu runtime). Đây là khác biệt cốt lõi giữa reference tới slice và reference tới array.- Hai cách: (1)
let last3 = &v[2..];— dùng range from. (2)let last3 = &v[v.len() - 3..];— tính explicit. Cách (1) ngắn hơn và rõ ràng hơn khi biết Vec có ít nhất 3 phần tử. Cách (3) thêmlet last3 = &v[v.len().saturating_sub(3)..];an toàn hơn khi Vec có thể ngắn hơn 3. - Gọi được với: (a)
&array— array fix size coerce tự động thành slice; (b)&vec— deref coercion quaDeref<Target = [T]>của Vec; (c)&vec[a..b]hoặc&array[a..b]— sub-slice đã sẵn là&[T]. Ưu tiên&[T]vì: linh hoạt cho mọi nguồn dữ liệu (không buộc caller phải có Vec), tránh ép caller allocate Vec không cần thiết, là idiom được clippy lintptr_argkhuyến nghị. chunks(3): chia không overlap, batch cuối có thể ngắn — output:[1, 2, 3]rồi[4, 5].windows(3): sliding window cùng size, mỗi bước trượt 1 phần tử — output:[1, 2, 3],[2, 3, 4],[3, 4, 5]. Khiv.len() < n,chunks(n)vẫn cho 1 batch ngắn cònwindows(n)không cho batch nào.
Bài Tiếp Theo
Bài 44: fn — Định Nghĩa Function Cơ Bản — mở đầu Nhóm 7 - Functions & Control Flow. Sau khi đã có đủ scalar types và compound types, sang phần xây dựng khối logic: cú pháp fn name(param: Type) -> ReturnType, snake_case convention, entry point fn main(), visibility default, và việc call site không quan tâm thứ tự khai báo (khác C).
Bài cuối Nhóm 6 - Compound Types. Tiếp theo sang Nhóm 7 - Functions & Control Flow.
