Bỏ qua để đến nội dung

Slice trong Rust

Slice cho phép bạn mượn một đoạn của mảng hoặc vector — một “khung nhìn” vào một phần của nó — mà không sao chép dữ liệu hay lấy quyền sở hữu. Nếu bạn đã dùng mượn (borrowing) với một giá trị đơn lẻ, thì slice là cùng ý tưởng đó áp dụng cho một dãy phần tử.

fn main() {
let numbers = [10, 20, 30, 40, 50];
let middle = &numbers[1..4]; // các phần tử ở chỉ số 1, 2, 3
println!("{:?}", middle);
}

Kết quả:

[20, 30, 40]
▶ Tự chạy thử

1..4 là cùng loại range bạn đã thấy trong vòng lặp for — nó bắt đầu ở chỉ số 1 và dừng trước chỉ số 4, nên bạn nhận được các chỉ số 1, 2 và 3.

Vài cách viết tắt hay gặp:

fn main() {
let numbers = [10, 20, 30, 40, 50];
println!("{:?}", &numbers[..3]); // từ đầu đến trước chỉ số 3 -> [10, 20, 30]
println!("{:?}", &numbers[2..]); // từ chỉ số 2 đến hết -> [30, 40, 50]
println!("{:?}", &numbers[..]); // toàn bộ -> [10, 20, 30, 40, 50]
}
▶ Tự chạy thử

Thật ra bạn đã dùng slice từ trước — &str là một slice của String (hoặc của một chuỗi khác)! Đó là lý do hai kiểu này phối hợp với nhau ăn ý đến vậy:

fn main() {
let sentence = String::from("The quick brown fox");
let first_word = &sentence[0..3];
println!("{first_word}"); // "The"
}
▶ Tự chạy thử

Câu ví dụ được giữ nguyên tiếng Anh vì [0..3] cắt theo byte, không theo chữ cái: mỗi chữ tiếng Việt có dấu chiếm 2–3 byte, nên cắt giữa chừng một chữ như vậy sẽ gây panic.

Bạn có thể nghĩ đến việc giải quyết chuyện này bằng cách sao chép ra một Vec hay String mới cho đoạn mình cần. Slice tránh hoàn toàn việc đó — nó chỉ trỏ vào một phần của dữ liệu sẵn có, giống như một tham chiếu (&) trỏ vào một giá trị đơn lẻ. Nghĩa là không cấp phát thêm, không sao chép, và các quy tắc mượn mà bạn đã biết từ bài Mượn vẫn áp dụng.

Slice đặc biệt hữu ích khi làm tham số của hàm, vì nó chấp nhận cả mảng lẫn vector:

fn sum(numbers: &[i32]) -> i32 {
let mut total = 0;
for n in numbers {
total += n;
}
total
}
fn main() {
let array = [1, 2, 3];
let vector = vec![4, 5, 6];
println!("{}", sum(&array));
println!("{}", sum(&vector));
}
▶ Tự chạy thử

&[i32] chấp nhận slice từ mảng hoặc từ Vec — một hàm duy nhất, cả hai chỗ gọi đều chạy được.

Đến giờ, slice của chúng ta chỉ đọc dữ liệu. Một slice khả biến (mutable slice), &mut [T], cho phép một hàm thay đổi những phần tử nó được giao, nhưng chỉ những phần tử đó:

fn double_all(numbers: &mut [i32]) {
for i in 0..numbers.len() {
numbers[i] *= 2;
}
}
fn main() {
let mut scores = vec![1, 2, 3, 4, 5];
double_all(&mut scores[1..3]); // chỉ chỉ số 1 và 2
println!("{:?}", scores);
double_all(&mut scores); // cả vector
println!("{:?}", scores);
}

Kết quả:

[1, 4, 6, 4, 5]
[2, 8, 12, 8, 10]
▶ Tự chạy thử

Lần gọi đầu truyền &mut scores[1..3], nên double_all chỉ thấy chỉ số 1 và 2. Mọi thứ bên ngoài đoạn đó đều an toàn trước nó. Bên trong hàm, slice lại bắt đầu từ chỉ số 0: với double_all, nó chỉ là một danh sách gồm hai số.

Điều này an toàn nhờ một quy tắc mà bạn sẽ học kỹ trong bài Mượn: khi hàm đang giữ một slice &mut, không thứ gì khác được dùng các phần tử đó, nên hai phần code của bạn không bao giờ thay đổi chúng cùng lúc. Vector cũng phải được khai báo mut, nếu không Rust sẽ không cho bạn mượn nó theo kiểu khả biến.

Tạo vector vec![3, 6, 9, 12, 15, 18]. In nửa đầu và nửa sau của nó thành hai slice riêng. Gợi ý: tính let mid = numbers.len() / 2; và dùng ..mid cùng mid...

Xem lời giải
fn main() {
let numbers = vec![3, 6, 9, 12, 15, 18];
let mid = numbers.len() / 2;
let first_half = &numbers[..mid];
let second_half = &numbers[mid..];
println!("Nửa đầu: {:?}", first_half);
println!("Nửa sau: {:?}", second_half);
}

Kết quả:

Nửa đầu: [3, 6, 9]
Nửa sau: [12, 15, 18]
▶ Tự chạy thử

Vì mid được tính từ len(), code vẫn chạy đúng nếu bạn thêm hay bớt số. Với độ dài lẻ, phần tử dư sẽ nằm ở nửa sau.

Viết hàm largest(numbers: &[i32]) -> i32 trả về số lớn nhất. Gọi nó với một mảng, một vector, và một slice chỉ gồm phần tử đầu tiên của vector. Gợi ý: bắt đầu với numbers[0] rồi duyệt qua phần còn lại, như bạn làm với mảng.

Xem lời giải
fn largest(numbers: &[i32]) -> i32 {
let mut biggest = numbers[0];
for n in numbers {
if *n > biggest {
biggest = *n;
}
}
biggest
}
fn main() {
let array = [7, 2, 9, 4];
let vector = vec![10, 35, 21];
println!("{}", largest(&array));
println!("{}", largest(&vector));
println!("{}", largest(&vector[..1]));
}

Kết quả:

9
35
10
▶ Tự chạy thử

Duyệt qua một slice cho bạn tham chiếu đến từng phần tử (&i32), chứ không phải bản thân con số. *n nghĩa là “giá trị mà n trỏ tới”, nên *n > biggest so sánh hai con số bình thường. Phiên bản này panic với slice rỗng, vì numbers[0] không tồn tại. Sau này bạn sẽ học cách trả về một Option cho trường hợp đó.

  • Slice (&[T]) mượn một đoạn của mảng hoặc vector mà không sao chép.
  • 1..4 chọn các chỉ số từ 1 đến 3; bỏ trống một bên của .. để đi từ đầu hoặc đến hết.
  • Bản thân &str là một slice văn bản — vì thế chuỗi và slice có rất nhiều hành vi chung.
  • Ưu tiên &[T] làm kiểu tham số của hàm khi bạn muốn nhận cả mảng lẫn vector.
  • Slice khả biến (&mut [T]) cho phép hàm chỉ thay đổi những phần tử nó được giao.

Kiểm tra nhanh

1. Slice cho phép bạn làm gì với một phần của mảng hoặc vector?

2. &numbers[1..4] gồm những chỉ số nào?

3. Vì sao &[i32] là lựa chọn tốt cho kiểu tham số của hàm?

Điểm: 0 / 3