Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu
&[T]là view contiguous vào một chuỗi liên tục các phần tử cùng kiểu T trong bộ nhớ — không own data, chỉ giữ con trỏ và độ dài. T có thể là kiểu nguyên thuỷ (i32,u8,f64), tuple, struct, hay bất kỳ type sized nào. - Biết cách tạo slice từ array fix-size:
let a = [1, 2, 3, 4, 5]; let s = &a[1..4];trả về slice 3 phần tử (index 1, 2, 3). - Biết cách tạo slice từ Vec qua deref coercion:
let v = vec![1, 2, 3]; let s: &[i32] = &v;hoặc sub-slice&v[1..2]. - Thành thạo cú pháp range đầy đủ:
0..5exclusive,0..=5inclusive,..3from start,2..to end,..full. - Hiểu vì sao
&[T]chiếm 16 byte trên stack trên hệ thống 64-bit (8 byte ptr + 8 byte len) và chứng minh được bằngsize_of. - Sử dụng nhuần nhuyễn các method workhorse:
len,iter,contains,first,last,chunks,windows,binary_search. - Viết function nhận
&[T]theo idiom Rust — caller được phép pass array fix-size, Vec hoặc sub-slice mà không cần convert. - Biết
&[u8]là slice xuất hiện khắp nơi trong code Rust thực tế: nội dung file đọc quastd::fs, payload TCP/HTTP, binary protocol, image/audio decoder.
Array Slice &[T] Là Gì
Array slice &[T] là một view dynamically-sized vào một chuỗi liên tục (contiguous sequence) các phần tử cùng kiểu T trong bộ nhớ. Đọc thành lời: "reference tới slice of T". Bản chất là một fat pointer ghép từ hai field: con trỏ tới phần tử đầu tiên và số phần tử (length).
Type bare [T] là một Dynamically Sized Type (DST) — số phần tử không biết ở compile time, nên bạn không dùng trực tiếp được. Phải luôn truy cập qua một wrapper sized: &[T], &mut [T], hoặc smart pointer Box<[T]> / Arc<[T]>. Bài 43 đã giới thiệu preview; bài này tập trung vào &[T] immutable và các use case xoay quanh.
Tổng quát của T: T có thể là bất kỳ kiểu sized nào — i32, u8, f64, String, (u32, u32), struct người dùng định nghĩa, thậm chí Box<dyn Trait>. Storage gốc của các phần tử phải lưu liên tục cùng kiểu — đó là điều kiện duy nhất để có slice.
// Slice trên kiểu nguyên thuỷ
let nums: &[i32] = &[10, 20, 30];
let bytes: &[u8] = &[0x48, 0x69];
let floats: &[f64] = &[1.5, 2.5, 3.5];
// Slice trên tuple, struct
let pairs: &[(u32, u32)] = &[(1, 2), (3, 4)];
#[derive(Debug)]
struct Point { x: i32, y: i32 }
let pts: &[Point] = &[Point { x: 0, y: 0 }, Point { x: 1, y: 1 }];
// In ra
println!("{:?} len={}", nums, nums.len()); // [10, 20, 30] len=3
println!("{:?} len={}", pts, pts.len()); // [Point { x: 0, y: 0 }, ...] len=2
Đặc tính cốt lõi của slice — nhớ ngay:
- Không own data: drop slice không drop element. Element vẫn thuộc storage gốc (array, Vec).
- Borrowed: slice là một borrow nên phải tuân quy tắc lifetime — storage gốc phải sống ít nhất bằng slice.
- Contiguous: phần tử nằm liên tục — không thể tạo slice từ
HashMap,HashSet, hayLinkedList. - Cùng kiểu: tất cả phần tử trong slice có cùng type T — không thể có
&[Mix]mỗi phần tử kiểu khác nhau (cần&[Box<dyn Trait>]thay thế).
Tạo Slice Từ Array
Cách phổ biến nhất để có một &[T]: lấy reference tới một array fix-size kèm range. Operator index [range] trên array trả về một slice — không phải single element.
fn main() {
let a: [i32; 5] = [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
// Reference tới toàn bộ array → coerce thành slice full
let full: &[i32] = &a;
println!("{:?}", full); // [1, 2, 3, 4, 5]
// Tương đương dùng range full
let full2: &[i32] = &a[..];
println!("{:?}", full2); // [1, 2, 3, 4, 5]
// Slice trên kiểu khác — không chỉ i32
let names: [&str; 3] = ["alice", "bob", "carol"];
let two: &[&str] = &names[..2];
println!("{:?}", two); // ["alice", "bob"]
}
Vài điểm quan trọng:
- Array
[T; N]có size biết ở compile time. Khi viết&a[1..4], compiler chèn bound check runtime đảm bảo range nằm trong0..=N; out-of-bounds panic chứ không undefined behavior. &a(không có range) có kiểu&[i32; 5]— thin pointer 8 byte. Nó coerce được thành&[i32]16 byte khi context cần (annotation, return type, hàm param).&a[..]luôn cho&[i32]— buộc compiler tạo fat pointer ngay từ đầu.- Khi range có thể không hợp lệ (ví dụ tính từ input người dùng), dùng
a.get(start..end)trảOption<&[T]>để xử lý lỗi mềm thay vì panic.
Tạo Slice Từ Vec — Deref Coercion
Vec<T> (Group 16) là dynamic array trên heap. Vec implement Deref<Target = [T]>, nghĩa là một &Vec<T> tự động coerce được thành &[T] khi context yêu cầu. Đây gọi là deref coercion — feature ngầm rất hữu ích.
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 cắt sub-slice, giống array
let mid: &[i32] = &v[1..2];
println!("{:?}", mid); // [20]
let mid3: &[i32] = &v[1..4];
println!("{:?}", mid3); // [20, 30, 40]
// Cách 3: method as_slice() - explicit nhất, không phụ thuộc coercion
let same: &[i32] = v.as_slice();
println!("{:?}", same); // [10, 20, 30, 40, 50]
// Cả 3 cách trên đều cho cùng kiểu &[i32]
}
Vì sao deref coercion hoạt động: header của Vec trên stack có cấu trúc (ptr, len, cap). Buffer thực sự trên heap chính là một mảng liên tục của T — có cấu trúc giống hệt array. Khi cần &[T], Rust chỉ việc lấy (ptr, len) từ header Vec — không allocate, không copy.
Một số trường hợp deref coercion không kích hoạt và bạn phải explicit:
- Khi gọi generic function chưa specify
&[T], ví dụ pass vào closure mà compiler không suy ra được — dùngv.as_slice()hoặc&v[..]. - Khi muốn slice cụ thể một range — phải dùng
&v[a..b], không thể coerce ngầm. - Khi macro yêu cầu kiểu chính xác — viết
&v[..]để buộc thành slice rõ ràng.
Range Syntax Đầy Đủ
Rust cung cấp 6 dạng range — tất cả đều áp dụng được khi cắt slice. Hiểu rõ exclusive vs inclusive giúp tránh bug off-by-one phổ biến.
fn main() {
let a = [1, 2, 3, 4, 5];
// 0..5 - Range, exclusive cận trên (PHỔ BIẾN NHẤT)
let r1: &[i32] = &a[0..5];
println!("0..5 = {:?}", r1); // [1, 2, 3, 4, 5]
// 0..=4 - RangeInclusive, bao gồm cận trên
let r2: &[i32] = &a[0..=4];
println!("0..=4 = {:?}", r2); // [1, 2, 3, 4, 5]
// ..3 - RangeTo, từ đầu đến exclusive 3
let r3: &[i32] = &a[..3];
println!("..3 = {:?}", r3); // [1, 2, 3]
// ..=2 - RangeToInclusive, từ đầu đến inclusive 2
let r4: &[i32] = &a[..=2];
println!("..=2 = {:?}", r4); // [1, 2, 3]
// 2.. - RangeFrom, từ index 2 đến hết
let r5: &[i32] = &a[2..];
println!("2.. = {:?}", r5); // [3, 4, 5]
// .. - RangeFull, toàn bộ
let r6: &[i32] = &a[..];
println!(".. = {:?}", r6); // [1, 2, 3, 4, 5]
}
Bảng tóm tắt loại type của từng range (chi tiết hơn ở Bài 78):
a..b→std::ops::Range. Exclusive cận trên. Mặc định phổ biến.a..=b→RangeInclusive. Inclusive cả hai đầu...b→RangeTo. Từ đầu tới exclusive b...=b→RangeToInclusive. Từ đầu tới inclusive b.a..→RangeFrom. Từ a tới hết...→RangeFull. Toàn bộ.
Lưu ý: chỉ Range và RangeInclusive là iterator (dùng được trong for i in 0..5). Các dạng còn lại chỉ dùng cho slice indexing, không lặp được trực tiếp.
Size Trên Stack: 16 Byte
Một &[T] chiếm 16 byte trên stack ở hệ thống 64-bit: 8 byte cho pointer + 8 byte cho length (usize). Đây là fat pointer, khác với reference thường &T tới sized type chỉ 8 byte.
use std::mem::size_of;
fn main() {
// Thin pointer 8 byte - sized type
println!("size_of::<&i32>() = {}", size_of::<&i32>()); // 8
println!("size_of::<&[i32; 5]>() = {}", size_of::<&[i32; 5]>()); // 8
println!("size_of::<&Vec<i32>>() = {}", size_of::<&Vec<i32>>()); // 8
// Fat pointer 16 byte - DST hoặc trait object
println!("size_of::<&[i32]>() = {}", size_of::<&[i32]>()); // 16
println!("size_of::<&[u8]>() = {}", size_of::<&[u8]>()); // 16
println!("size_of::<&mut [i32]>() = {}", size_of::<&mut [i32]>()); // 16
println!("size_of::<&str>() = {}", size_of::<&str>()); // 16
}
Mental model: tưởng tượng &[T] như một struct ẩn:
// 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
len: usize, // 8 byte: số phần tử
} // tổng: 16 byte trên 64-bit
Hệ quả thực tế:
- Pass
&[T]vào function tốn 16 byte stack (qua register thường xuyên — không đáng kể về performance). - Pass
&Vec<T>chỉ 8 byte (con trỏ tới Vec header trên stack). Truy cập phần tử cần thêm một bước indirection so với&[T]. - Cả hai đều rất rẻ — chọn
&[T]vì linh hoạt cho caller, không vì performance. &str,Box<[T]>,Rc<[T]>,Arc<[T]>đều là fat pointer 16 byte cùng family với&[T].
Method Workhorse Cho Slice
Slice có hàng trăm method trong stdlib (std::slice). Dưới đây là các method dùng hàng ngày — đáng thuộc lòng từ bài này.
fn main() {
let s: &[i32] = &[10, 20, 30, 40, 50];
// Truy vấn cơ bản
println!("len = {}", s.len()); // 5
println!("is_empty = {}", s.is_empty()); // false
println!("first = {:?}", s.first()); // Some(10)
println!("last = {:?}", s.last()); // Some(50)
println!("contains = {}", s.contains(&30)); // true
// Lặp qua iterator (Group 22 sâu hơn)
let total: i32 = s.iter().sum();
println!("total = {}", total); // 150
let max = s.iter().max();
println!("max = {:?}", max); // Some(50)
// binary_search yêu cầu slice đã sort theo thứ tự tăng
println!("bin search 30 = {:?}", s.binary_search(&30)); // Ok(2)
println!("bin search 35 = {:?}", s.binary_search(&35)); // Err(3) - vị trí chèn
}
Hai method đặc biệt hữu ích cho bài toán xử lý dãy theo nhóm: chunks và windows. Cả hai trả về iterator của &[T] — sub-slice, không copy data.
fn main() {
let v: [i32; 6] = [1, 2, 3, 4, 5, 6];
// chunks(n): chia liên tiếp KHÔNG OVERLAP, batch cuối có thể ngắn hơn
println!("--- chunks(2) ---");
for chunk in v.chunks(2) {
println!("{chunk:?}");
}
// [1, 2]
// [3, 4]
// [5, 6]
// chunks(4) - batch cuối chỉ 2 phần tử
println!("--- chunks(4) ---");
for chunk in v.chunks(4) {
println!("{chunk:?}");
}
// [1, 2, 3, 4]
// [5, 6]
// windows(n): sliding window size n, di chuyển 1 phần tử mỗi bước
println!("--- windows(3) ---");
for w in v.windows(3) {
println!("{w:?}");
}
// [1, 2, 3]
// [2, 3, 4]
// [3, 4, 5]
// [4, 5, 6]
}
Khi nào dùng cái nào:
chunks: batch processing — chia file theo block 4 KB, gom record để insert batch DB, vẽ table theo cột.windows: phân tích pattern liên tiếp — moving average n ngày, n-gram NLP, phát hiện chuỗi tăng dần, dò trùng lặp adjacent.- Khi
v.len() < n:chunks(n)vẫn cho 1 batch ngắn, cònwindows(n)không cho batch nào (iterator rỗng).
Function Nhận &[T] — Idiom Linh Hoạt
Đây là idiom quan trọng nhất bạn cần nhớ từ bài này: khi viết function nhận input một dãy phần tử, luôn ưu tiên &[T]. Lý do: cùng một hàm gọi được cho cả array fix-size, Vec, lẫn sub-slice — caller không bị ép vào kiểu cụ thể.
fn sum(arr: &[i32]) -> i32 {
arr.iter().sum()
}
fn main() {
// CALLER 1: pass array fix-size
let arr_static: [i32; 3] = [1, 2, 3];
println!("{}", sum(&arr_static)); // 6
// CALLER 2: pass Vec
let vec_dynamic: Vec<i32> = vec![4, 5, 6, 7];
println!("{}", sum(&vec_dynamic)); // 22
// CALLER 3: pass sub-slice từ Vec
println!("{}", sum(&vec_dynamic[1..3])); // 11 (5 + 6)
}
Cùng một sum nhận được cả ba dạng đầu vào nhờ:
&arr_static— unsized coercion từ&[i32; 3]sang&[i32]. Compiler tự thêm length 3 vào fat pointer.&vec_dynamic— deref coercion quaVec: Deref<Target = [T]>. Compiler lấy ptr + len từ Vec header.&vec_dynamic[1..3]— đã sẵn là&[i32]từ slice indexing.
So sánh với hai signature kém linh hoạt — tránh viế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:sum(&arr_static.to_vec())— tốn một allocation Vec vô ích.fn sum(arr: &[i32; 3])— chỉ array đúng 3 phần tử. Vec compile error, array 4 phần tử compile error, sub-slice compile error.
Clippy có lint ptr_arg tự warn khi bạn viết &Vec<T> hoặc &String trong function signature và gợi ý đổi sang &[T] / &str. Tuân theo lint — code idiomatic, caller-friendly, dễ test hơn (test pass array literal không cần allocate Vec).
&[u8] — Slice Cho Byte Buffer
Trong code Rust thực tế, một biến thể đặc biệt phổ biến của array slice là &[u8] — slice byte. Mọi thứ "raw bytes" đều xuất hiện dưới dạng này: nội dung file đọc từ std::fs::read, payload TCP/HTTP, message queue, binary protocol (Protobuf, MessagePack, Bincode), image/audio decoder, hash input.
use std::fs;
fn count_zero_bytes(buf: &[u8]) -> usize {
buf.iter().filter(|&&b| b == 0).count()
}
fn parse_magic(buf: &[u8]) -> Option<&str> {
// Inspect 4 byte đầu - magic number cho định dạng file
match &buf[..4.min(buf.len())] {
[0x89, b'P', b'N', b'G'] => Some("PNG"),
[0xFF, 0xD8, 0xFF, _] => Some("JPEG"),
[b'G', b'I', b'F', b'8'] => Some("GIF"),
[b'%', b'P', b'D', b'F'] => Some("PDF"),
_ => None,
}
}
fn main() -> std::io::Result<()> {
// Đọc file → Vec<u8>
let content: Vec<u8> = fs::read("/tmp/sample.png")?;
// Truyền cho function nhận &[u8] - deref coercion từ Vec<u8>
println!("zeros = {}", count_zero_bytes(&content));
println!("type = {:?}", parse_magic(&content));
// Sub-slice byte: skip 8 byte header
if content.len() > 8 {
let payload: &[u8] = &content[8..];
println!("payload len = {}", payload.len());
}
Ok(())
}
Trade-off &[u8] vs Vec<u8> khi thiết kế API:
- Function input → ưu tiên
&[u8]. Caller có thể pass slice từ file đọc về (Vec<u8>qua deref), từ byte literalb"hello"(kiểu&[u8; 5]coerce), từ memory-mapped file, từ network buffer — tất cả không cần copy hay convert. - Function output → trả
Vec<u8>khi cần own data; trả&[u8]khi data đã có lifetime hợp lệ trong storage gốc (giúp zero-copy). - Field struct →
Vec<u8>nếu struct own dữ liệu (default safe);&'a [u8]nếu struct chỉ là view, có lifetime gắn với storage gốc (nâng cao, dùng trong parser zero-copy). - Hash / crypto API: gần như mọi crate (
sha2,blake3,md5) nhận input dưới dạng&[u8]qua methodupdate(&[u8])để hỗ trợ streaming.
Byte literal nhỏ trong test: viết b"hello" cho &[u8; 5], hoặc &[0x01, 0x02, 0x03] cho &[u8; 3] — cả hai coerce ngầm sang &[u8] khi gọi function. Tiện cho viết unit test không cần đọc file.
Tổng Kết
- Array slice
&[T]= view contiguous vào sequence cùng kiểu T; không own data, chỉ giữ ptr + len. - T tổng quát: bất kỳ kiểu sized nào — i32, u8, f64, tuple, struct, &str.
- Tạo từ array:
let a = [1, 2, 3, 4, 5]; let s = &a[1..4];cho slice 3 phần tử. - Tạo từ Vec:
let v = vec![1, 2, 3]; let s: &[i32] = &v;qua deref coercion; sub-slice&v[1..2]. - Range syntax:
0..5exclusive (phổ biến),0..=5inclusive,..3from start,2..to end,..full. - Size 16 byte: fat pointer 8 byte ptr + 8 byte len trên 64-bit; khác
&Tsized chỉ 8 byte. - Method workhorse:
len,iter,contains,first,last,chunks,windows,binary_search. - Idiom function signature: nhận
&[T]linh hoạt cho caller pass array, Vec hoặc sub-slice — tránh&Vec<T>hoặc&[T; N]. &[u8]byte buffer: nội dung file, payload TCP/HTTP, binary protocol — input chuẩn cho hash/parser/decoder API.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Có array
let a = [10, 20, 30, 40, 50];. Viết slice gồm phần tử thứ 2 đến thứ 4 (đếm từ 1) — kết quả mong muốn[20, 30, 40]. Dùng cả range exclusive1..4và inclusive1..=3— kết quả có giống nhau không? - Có
let v: Vec<i32> = vec![1, 2, 3, 4, 5];. Liệt kê ba cách khác nhau để có&[i32]trỏ tới toàn bộ Vec. Method nào explicit nhất? - Trên hệ thống 64-bit,
size_of::<&[u8]>(),size_of::<&[u8; 4]>(),size_of::<&Vec<u8>>()trả về giá trị bao nhiêu? Giải thích vì sao khác nhau. - Viết function
fn max_value(arr: &[i32]) -> Option<i32>trả về phần tử lớn nhất, hoặcNonenếu slice rỗng. Gọi function này với (a) array literal&[3, 1, 4, 1, 5, 9, 2, 6], (b) Vecvec![10, 20, 30], (c) sub-slice&vec[..2]. - Cho
let v = [1, 2, 3, 4, 5];. Output củav.chunks(2)vàv.windows(2)khác nhau như thế nào? Khi nào dùngchunks, khi nào dùngwindows? - Viết function
fn first_n_bytes(buf: &[u8], n: usize) -> &[u8]trả về n byte đầu của buffer (hoặc toàn bộ nếu buffer ngắn hơn n). Đảm bảo không panic. Hint: dùngn.min(buf.len()).
Đáp án
- Cả hai cho kết quả giống nhau
[20, 30, 40].&a[1..4]lấy index 1, 2, 3 (exclusive 4).&a[1..=3]lấy index 1, 2, 3 (inclusive 3). Khác cú pháp, cùng kết quả. Trong code Rust, dạng exclusivea..bphổ biến hơn vìb - angay lập tức cho số phần tử. - Ba cách: (a)
let s: &[i32] = &v;— dùng deref coercion (ngầm). (b)let s: &[i32] = &v[..];— explicit range full. (c)let s: &[i32] = v.as_slice();— method explicit nhất, không phụ thuộc coercion. Cách (c) rõ ràng nhất khi đọc code review. size_of::<&[u8]>() = 16(fat pointer DST).size_of::<&[u8; 4]>() = 8(thin pointer — size 4 đã có trong type, không cần lưu runtime).size_of::<&Vec<u8>>() = 8(thin pointer — Vec là sized type, header (ptr, len, cap) 24 byte nằm ở vị trí mà reference trỏ tới, không nằm trong reference).fn max_value(arr: &[i32]) -> Option<i32> { arr.iter().max().copied() }. Gọi:max_value(&[3, 1, 4, 1, 5, 9, 2, 6])→Some(9).max_value(&vec![10, 20, 30])→Some(30).max_value(&vec[..2])→Some(20). Cả ba caller pass được nhờ idiom&[T].v.chunks(2)→[1, 2],[3, 4],[5](3 batch, không overlap, batch cuối ngắn).v.windows(2)→[1, 2],[2, 3],[3, 4],[4, 5](4 cửa sổ, di chuyển 1 phần tử). Dùngchunkscho batch processing (chia file, gom record); dùngwindowscho phân tích pattern liên tiếp (moving average, n-gram, dò chuỗi tăng dần).fn first_n_bytes(buf: &[u8], n: usize) -> &[u8] { &buf[..n.min(buf.len())] }. Trickn.min(buf.len())đảm bảo upper bound không vượt độ dài buffer, nên slice indexing luôn an toàn — không panic. Đây là pattern phổ biến trong code xử lý buffer.
Bài Tiếp Theo
Bài 78: Range Syntax: &s[0..5], &s[..], &s[2..] — đi sâu vào range syntax mà bài này đã giới thiệu lướt. Bạn sẽ hiểu chi tiết 6 loại type range trong Rust (Range, RangeInclusive, RangeTo, RangeToInclusive, RangeFrom, RangeFull), khi nào dùng được như iterator (for i in 0..10) và khi nào chỉ dùng cho slice indexing, cùng pitfall thường gặp khi đảo cận (start > end cho slice rỗng, không panic).
Bài thứ hai của Nhóm 11 - Slices. Còn 4 bài nữa hoàn thành nhóm: range syntax, mutable slice, slice trong function signature, UTF-8 boundary panic.
