Danh sách bài viết

Bài 77: Array Slice — &[T]

Bài 77 của series Rust Cơ Bản — đi sâu vào array slice &[T], slice tổng quát cho mọi kiểu phần tử T trong Rust 2024. Bao gồm fat pointer 16 byte (ptr + len), cách tạo slice từ array fix-size lẫn từ Vec<T> qua deref coercion, range syntax đầy đủ, các method workhorse như chunks, windows, binary_search, idiom function nhận &[T] linh hoạt, và đặc biệt là &[u8] — slice byte dùng khắp nơi từ file I/O đến network protocol parsing.

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

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Hiểu &[T]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..5 exclusive, 0..=5 inclusive, ..3 from 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ằng size_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 qua std::fs, payload TCP/HTTP, binary protocol, image/audio decoder.
2

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, hay LinkedList.
  • 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ế).
3

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 trong 0..=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.
4

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ùng v.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.
5

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..bstd::ops::Range. Exclusive cận trên. Mặc định phổ biến.
  • a..=bRangeInclusive. Inclusive cả hai đầu.
  • ..bRangeTo. Từ đầu tới exclusive b.
  • ..=bRangeToInclusive. Từ đầu tới inclusive b.
  • a..RangeFrom. Từ a tới hết.
  • ..RangeFull. Toàn bộ.

Lưu ý: chỉ RangeRangeInclusive 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.

6

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].
7

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: chunkswindows. 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òn windows(n) không cho batch nào (iterator rỗng).
8

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_staticunsized coercion từ &[i32; 3] sang &[i32]. Compiler tự thêm length 3 vào fat pointer.
  • &vec_dynamicderef coercion qua Vec: 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_static compile 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).

9

&[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 literal b"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 structVec<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 method update(&[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.

10

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..5 exclusive (phổ biến), 0..=5 inclusive, ..3 from start, 2.. to end, .. full.
  • Size 16 byte: fat pointer 8 byte ptr + 8 byte len trên 64-bit; khác &T sized 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.
11

Bài Tập Củng Cố

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

  1. 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 exclusive 1..4 và inclusive 1..=3 — kết quả có giống nhau không?
  2. 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?
  3. 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.
  4. Viết function fn max_value(arr: &[i32]) -> Option<i32> trả về phần tử lớn nhất, hoặc None nếu slice rỗng. Gọi function này với (a) array literal &[3, 1, 4, 1, 5, 9, 2, 6], (b) Vec vec![10, 20, 30], (c) sub-slice &vec[..2].
  5. Cho let v = [1, 2, 3, 4, 5];. Output của v.chunks(2)v.windows(2) khác nhau như thế nào? Khi nào dùng chunks, khi nào dùng windows?
  6. 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ùng n.min(buf.len()).
Đáp án
  1. 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 exclusive a..b phổ biến hơn vì b - a ngay lập tức cho số phần tử.
  2. 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.
  3. 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).
  4. 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].
  5. 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ùng chunks cho batch processing (chia file, gom record); dùng windows cho phân tích pattern liên tiếp (moving average, n-gram, dò chuỗi tăng dần).
  6. fn first_n_bytes(buf: &[u8], n: usize) -> &[u8] { &buf[..n.min(buf.len())] }. Trick n.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.
12

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.