Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Iterator

Iterator xử lý một chuỗi giá trị lần lượt từng phần tử, theo trait Iterator trong thư viện chuẩn — chỉ cần đúng một method bắt buộc:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

next() trả Some(gia_tri) cho từng phần tử, None khi hết. Self::Item là kiểu phần tử — mỗi kiểu implement Iterator tự quyết định Item cụ thể của mình.

Tính lười

fn main() {
    let v = vec![1, 2, 3];
    let mut iter = v.iter();

    println!("{:?}", iter.next());
    println!("{:?}", iter.next());
    println!("{:?}", iter.next());
    println!("{:?}", iter.next());
}

Tạo một iterator (v.iter()) không làm gì cả — không có phần tử nào được xử lý cho tới khi next() (trực tiếp hoặc gián tiếp qua for, hay các adaptor dưới đây) thật sự được gọi. Đây gọi là tính lười (lazy): định nghĩa một chuỗi phép biến đổi trên iterator không tốn chi phí gì cho tới khi có thứ gì đó thật sự “tiêu thụ” (consume) nó.

v.iter() cho iterator các tham chiếu bất biến (&i32, không lấy quyền sở hữu v); v.into_iter() lấy quyền sở hữu v, cho iterator các giá trị sở hữu hẳn (i32); v.iter_mut() cho tham chiếu khả biến, sửa được từng phần tử ngay trong lúc duyệt.

Adaptor tiêu thụ

Một số method gọi next() lặp lại cho tới khi hết, “tiêu thụ” hẳn iterator để tính ra một giá trị duy nhất:

fn main() {
    let v = vec![1, 2, 3];
    let tong: i32 = v.iter().sum();
    println!("{tong}");
}

for cũng tiêu thụ iterator theo cách tương tự, gọi next() ở mỗi vòng lặp — cách viết for x in v.iter() chẳng qua là đường cú pháp gọn cho đúng cơ chế next() này.

Adaptor biến đổi

Một số method khác không tiêu thụ ngay mà trả về một iterator mới, đã áp thêm một phép biến đổi — vẫn lười, chưa chạy gì cho tới khi có một adaptor tiêu thụ (hoặc for) phía sau:

fn main() {
    let v = vec![1, 2, 3];
    let gap_doi: Vec<i32> = v.iter().map(|x| x * 2).collect();
    println!("{gap_doi:?}");
}

map không tự chạy — nó trả về một iterator mới, mô tả “sẽ nhân đôi từng phần tử khi được hỏi tới”. collect() mới là bước thật sự tiêu thụ, gom kết quả vào một collection cụ thể (ở đây là Vec<i32>, suy luận từ chú thích kiểu — collect generic trên kiểu đích nên gần như luôn cần một gợi ý kiểu ở đâu đó, qua let hoặc cú pháp turbofish ::<Vec<i32>>()).

filter giữ lại phần tử thoả một điều kiện, thường kết hợp với closure capture biến từ scope ngoài:

#[derive(Debug)]
struct SanPham {
    ten: String,
    gia: u32,
}

fn loc_theo_ngan_sach(danh_sach: Vec<SanPham>, ngan_sach: u32) -> Vec<SanPham> {
    danh_sach
        .into_iter()
        .filter(|sp| sp.gia <= ngan_sach)
        .collect()
}

fn main() {
    let san_pham = vec![
        SanPham {
            ten: String::from("Bàn phím"),
            gia: 500,
        },
        SanPham {
            ten: String::from("Chuột"),
            gia: 150,
        },
    ];

    let phu_hop = loc_theo_ngan_sach(san_pham, 200);
    println!("{phu_hop:?}");
}

filter capture ngan_sach từ scope ngoài closure — kết hợp iterator với closure là cách rất phổ biến để viết logic xử lý dữ liệu ngắn gọn mà không cần vòng lặp for tường minh.

Hiệu năng: trừu tượng hoá chi phí bằng không

Chuỗi map/filter/collect trông như tạo ra nhiều bước trung gian, nhưng trình biên dịch tối ưu hoá toàn bộ chuỗi adaptor xuống thành mã máy tương đương hệt một vòng lặp for viết tay, không có overhead gọi hàm hay cấp phát trung gian thừa. Đây là ví dụ điển hình cho triết lý “trừu tượng hoá chi phí bằng không” (zero-cost abstraction) của Rust — dùng iterator không đánh đổi hiệu năng để lấy sự ngắn gọn, khác với cảm giác trực giác rằng code “trừu tượng hơn” ắt phải “chậm hơn”.