Trong JavaScript, bên cạnh việc sử dụng các vòng lặp như for để duyệt qua mảng thì có nhiều cách làm điều tương tự. Ví dụ lặp qua một mảng các số.
const arr = [1, 2, 3, 4];
arr.map(fn());
arr.filter(fn());
arr.reduce(fn());
Đây là các phương thức được triển khai đối với kiểu dữ liệu có thể lặp qua. Trong Rust chúng ta cũng có khái niệm tương tự: Iterator.
Iterator chịu trách nhiệm cho logic lặp qua từng mục và xác định khi nào chuỗi kết thúc, giúp người lập trình không cần phải tự triển khai lại logic này. Iterator trong Rust được tạo ra bằng cách gọi phương thức như iter() trên các cấu trúc dữ liệu (ví dụ: Vec<T>). Iterators là lazy (lười), nghĩa là chúng không có tác dụng cho đến khi bạn gọi các phương thức tiêu thụ (consume) chúng.
Ví dụ:
fn main() {
let v1 = vec![6-8];
let v1_iter = v1.iter(); // Tạo iterator
for val in v1_iter { // Vòng lặp for ngầm tiêu thụ iterator
println!("Got: {val}");
}
}
Tất cả các Iterator đều triển khai trait Iterator được định nghĩa trong thư viện chuẩn. Cấu trúc của trait Iterator trông giống như sau:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Khi một hàm sử dụng Iterator , nói gọi hàm next để duyệt qua phần tử. Hoặc có thể trực tiếp gọi next nếu tự triển khai lại hàm xử lý.
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
Lưu ý rằng việc gọi next sẽ thay đổi trạng thái nội bộ mà trình lặp sử dụng để theo dõi vị trí của nó trong chuỗi. Nói cách khác, nó tiêu thụ một mục của iterator cho đến khi hoàn tất, iterator đó sẽ không còn truy cập được nữa.
Các giá trị trả về từ next khi sử dụng phương thức iter() là các tham chiếu bất biến (immutable references) đến các giá trị trong vector. Nếu muốn lấy quyền sở hữu (ownership) các giá trị, ta dùng into_iter(). Nếu muốn lặp qua các tham chiếu khả biến (mutable references), ta dùng iter_mut()
Có 2 loại hàm sử dụng theo 2 cách khác nhau, một loại thì tiêu thụ hết iterator, một loại thì không tiêu thụ, thay vào đó nó tạo ra một iterator mới và xử lý dữ liệu ở trên đó. Ví dụ sum tiêu thụ iterator, nếu gọi sum thì sẽ không thể sử dụng lại iterator đã tạo ra trước đó.
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
}
Các phương thức khác như map, filter thì không tiêu thụ mà thay vào đó tạo ra các iterator khác đã được thay đổi. Tuy nhiên, vì các iterator đều lazy, cần phải gọi một consuming adapter (như collect) để thực sự nhận được kết quả từ chuỗi các iterator.
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
Trong ví dụ trên bạn thấy sự xuất hiện của closure bên trong hàm map. Nhiều iterator adapters nhận closures làm đối số. Việc sử dụng closure cho phép bạn tùy chỉnh hành vi trong khi vẫn tái sử dụng được hành vi lặp mà trait Iterator cung cấp.
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter() // Tạo iterator, lấy quyền sở hữu vector
.filter(|s| s.size == shoe_size)
.collect() // Tiêu thụ iterator và thu thập kết quả vào Vec
}
Hàm filter nhận một closure trả về bool. Nếu closure trả về true, giá trị sẽ được giữ lại trong quá trình lặp.
let v1: Vec<i32> = vec![6-8];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
Ví dụ khác, phương thức map nhận một closure để gọi trên mỗi mục, tạo ra một iterator mới với các mục đã được biến đổi.
Đến đây thì bạn đọc thấy cách sử dụng map và filter khá tương đồng với JavaScript phải không!
Hiệu suất giữa việc sử dụng vòng lặp for và sử dụng Iterators là tương đương nhau. Iterators là một trong những zero-cost abstractions (trừu tượng hóa không tốn chi phí) của Rust, nghĩa là việc sử dụng cơ chế trừu tượng hóa này không gây thêm chi phí runtime nào.
Vì không có hình phạt hiệu suất khi sử dụng iterators, các lập trình viên Rust thường ưu tiên sử dụng phong cách iterator vì nó rõ ràng và dễ hiểu hơn khi bạn đã quen với các adapter khác nhau. Phong cách iterator giúp code tập trung vào mục tiêu cấp cao của vòng lặp, trừu tượng hóa các chi tiết lặp lại.
Iterators trong Rust không chỉ cung cấp một cách tiếp cận linh hoạt và tiện lợi để duyệt qua các cấu trúc dữ liệu mà còn đảm bảo hiệu suất tối ưu nhờ cơ chế zero-cost abstraction. So với các vòng lặp truyền thống như for, Iterators giúp lập trình viên tập trung vào mục tiêu cấp cao hơn, giảm thiểu sự phức tạp khi phải tự triển khai logic lặp. Với khả năng tạo ra các Iterators mới thông qua các adapter như map và filter, cùng việc sử dụng closure để tùy chỉnh hành vi trên từng phần tử, Rust mang đến một phong cách lập trình mạnh mẽ và rõ ràng, tương tự như các phương pháp xử lý mảng trong JavaScript.
Sự phân biệt giữa các kiểu Iterators như iter, into_iter, và iter_mut giúp linh hoạt trong việc quản lý quyền sở hữu và tính bất biến của dữ liệu. Việc kết hợp giữa các hàm tiêu thụ và không tiêu thụ cho phép lập trình viên kiểm soát chặt chẽ hơn cách xử lý dữ liệu. Iterators không chỉ là một công cụ mà còn là một phần không thể thiếu trong việc viết mã hiệu quả, rõ ràng và dễ bảo trì trong Rust.