Xử lý Một Chuỗi Các Item với Iterators

Mẫu iterator cho phép bạn thực hiện một nhiệm vụ nào đó trên một chuỗi các phần tử lần lượt. Một iterator chịu trách nhiệm cho logic lặp qua từng phần tử và xác định khi nào chuỗi đã kết thúc. Khi bạn sử dụng iterators, bạn không phải tái thực hiện logic đó cho chính mình.

Trong Rust, iterators là lười biếng, nghĩa là chúng không có tác dụng gì cho đến khi bạn gọi các phương thức tiêu thụ iterator để sử dụng nó. Ví dụ, mã trong Listing 13-10 tạo một iterator trên các phần tử trong vector v1 bằng cách gọi phương thức iter được định nghĩa trên Vec<T>. Mã này tự nó không làm gì có ích.

fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();
}

Iterator được lưu trữ trong biến v1_iter. Một khi chúng ta đã tạo một iterator, chúng ta có thể sử dụng nó theo nhiều cách khác nhau. Trong Listing 3-5 ở Chương 3, chúng ta đã lặp qua một mảng bằng vòng lặp for để thực thi một số mã trên mỗi phần tử của nó. Bên dưới, điều này ngầm tạo ra và sau đó tiêu thụ một iterator, nhưng chúng ta đã bỏ qua cách nó hoạt động chính xác cho đến bây giờ.

Trong ví dụ ở Listing 13-11, chúng ta tách việc tạo iterator khỏi việc sử dụng iterator trong vòng lặp for. Khi vòng lặp for được gọi sử dụng iterator trong v1_iter, mỗi phần tử trong iterator được sử dụng trong một lần lặp của vòng lặp, điều này in ra mỗi giá trị.

fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {val}");
    }
}

Trong các ngôn ngữ không có iterators được cung cấp bởi thư viện chuẩn của chúng, bạn có thể sẽ viết cùng một chức năng này bằng cách bắt đầu với một biến ở chỉ số 0, sử dụng biến đó để truy cập vào vector để lấy một giá trị, và tăng giá trị biến trong một vòng lặp cho đến khi nó đạt đến tổng số phần tử trong vector.

Iterators xử lý tất cả logic đó cho bạn, cắt giảm mã lặp lại mà bạn có thể làm rối. Iterators cho bạn thêm sự linh hoạt để sử dụng cùng một logic với nhiều loại chuỗi khác nhau, không chỉ các cấu trúc dữ liệu mà bạn có thể truy cập theo chỉ mục, như vectors. Hãy xem cách iterators làm điều đó.

Trait Iterator và Phương thức next

Tất cả iterators đều thực hiện một trait có tên Iterator được định nghĩa trong thư viện chuẩn. Định nghĩa của trait trông như thế này:

#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // phương thức với các triển khai mặc định được lược bỏ
}
}

Chú ý rằng định nghĩa này sử dụng một số cú pháp mới: type ItemSelf::Item, đang định nghĩa một kiểu liên kết với trait này. Chúng ta sẽ nói về các kiểu liên kết một cách sâu sắc trong Chương 20. Hiện tại, tất cả những gì bạn cần biết là mã này nói rằng việc thực hiện trait Iterator yêu cầu bạn cũng phải định nghĩa một kiểu Item, và kiểu Item này được sử dụng trong kiểu trả về của phương thức next. Nói cách khác, kiểu Item sẽ là kiểu được trả về từ iterator.

Trait Iterator chỉ yêu cầu những người thực hiện định nghĩa một phương thức: phương thức next, trả về một phần tử của iterator mỗi lần, được bọc trong Some và, khi lặp kết thúc, trả về None.

Chúng ta có thể gọi phương thức next trực tiếp trên iterators; Listing 13-12 minh họa các giá trị nào được trả về từ các lệnh gọi lặp lại đến next trên iterator được tạo từ vector.

#[cfg(test)]
mod tests {
    #[test]
    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 chúng ta cần làm cho v1_iter có thể thay đổi: gọi phương thức next trên một iterator thay đổi trạng thái nội bộ mà iterator sử dụng để theo dõi vị trí của nó trong chuỗi. Nói cách khác, mã này tiêu thụ, hoặc sử dụng hết, iterator. Mỗi lệnh gọi đến next tiêu thụ một phần tử từ iterator. Chúng ta không cần làm cho v1_iter có thể thay đổi khi chúng ta sử dụng vòng lặp for vì vòng lặp đã lấy quyền sở hữu của v1_iter và làm cho nó có thể thay đổi đằng sau hậu trường.

Cũng lưu ý rằng các giá trị chúng ta nhận được từ các lệnh gọi đến next là các tham chiếu bất biến đến các giá trị trong vector. Phương thức iter tạo ra một iterator trên các tham chiếu bất biến. Nếu chúng ta muốn tạo một iterator lấy quyền sở hữu của v1 và trả về các giá trị thuộc sở hữu, chúng ta có thể gọi into_iter thay vì iter. Tương tự, nếu chúng ta muốn lặp qua các tham chiếu có thể thay đổi, chúng ta có thể gọi iter_mut thay vì iter.

Các Phương thức Tiêu thụ Iterator

Trait Iterator có một số phương thức khác nhau với các triển khai mặc định được cung cấp bởi thư viện chuẩn; bạn có thể tìm hiểu về các phương thức này bằng cách xem tài liệu API thư viện chuẩn cho trait Iterator. Một số phương thức này gọi phương thức next trong định nghĩa của chúng, đó là lý do tại sao bạn được yêu cầu thực hiện phương thức next khi thực hiện trait Iterator.

Các phương thức gọi next được gọi là bộ điều hợp tiêu thụ vì gọi chúng sử dụng hết iterator. Một ví dụ là phương thức sum, lấy quyền sở hữu của iterator và lặp qua các phần tử bằng cách liên tục gọi next, do đó tiêu thụ iterator. Khi nó lặp qua, nó thêm mỗi phần tử vào một tổng đang chạy và trả về tổng khi lặp hoàn thành. Listing 13-13 có một test minh họa việc sử dụng phương thức sum.

#[cfg(test)]
mod tests {
    #[test]
    fn iterator_sum() {
        let v1 = vec![1, 2, 3];

        let v1_iter = v1.iter();

        let total: i32 = v1_iter.sum();

        assert_eq!(total, 6);
    }
}

Chúng ta không được phép sử dụng v1_iter sau lệnh gọi đến sumsum lấy quyền sở hữu của iterator mà chúng ta gọi nó trên.

Các Phương thức tạo ra Iterator Khác

Bộ điều hợp iterator là các phương thức được định nghĩa trên trait Iterator không tiêu thụ iterator. Thay vào đó, chúng tạo ra các iterator khác nhau bằng cách thay đổi một số khía cạnh của iterator gốc.

Listing 13-14 hiển thị một ví dụ về việc gọi phương thức điều hợp iterator map, lấy một closure để gọi trên mỗi phần tử khi các phần tử được lặp qua. Phương thức map trả về một iterator mới tạo ra các phần tử đã được sửa đổi. Closure ở đây tạo ra một iterator mới trong đó mỗi phần tử từ vector sẽ được tăng lên 1:

fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

    v1.iter().map(|x| x + 1);
}

Tuy nhiên, mã này tạo ra một cảnh báo:

$ cargo run
   Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: iterators are lazy and do nothing unless consumed
  = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
  |
4 |     let _ = v1.iter().map(|x| x + 1);
  |     +++++++

warning: `iterators` (bin "iterators") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/iterators`

Mã trong Listing 13-14 không làm gì cả; closure mà chúng ta đã chỉ định không bao giờ được gọi. Cảnh báo nhắc nhở chúng ta lý do tại sao: bộ điều hợp iterator là lười biếng, và chúng ta cần tiêu thụ iterator ở đây.

Để sửa cảnh báo này và tiêu thụ iterator, chúng ta sẽ sử dụng phương thức collect, mà chúng ta đã sử dụng trong Chương 12 với env::args trong Listing 12-1. Phương thức này tiêu thụ iterator và thu thập các giá trị kết quả vào một kiểu dữ liệu tập hợp.

Trong Listing 13-15, chúng ta thu thập các kết quả của việc lặp qua iterator được trả về từ lệnh gọi đến map vào một vector. Vector này sẽ kết thúc chứa mỗi phần tử từ vector gốc, tăng lên 1.

fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

    assert_eq!(v2, vec![2, 3, 4]);
}

Bởi vì map lấy một closure, chúng ta có thể chỉ định bất kỳ hoạt động nào mà chúng ta muốn thực hiện trên mỗi phần tử. Đây là một ví dụ tuyệt vời về cách closures cho phép bạn tùy chỉnh một số hành vi trong khi tái sử dụng hành vi lặp lại mà trait Iterator cung cấp.

Bạn có thể nối nhiều lệnh gọi đến bộ điều hợp iterator để thực hiện các hoạt động phức tạp theo cách dễ đọc. Nhưng bởi vì tất cả iterators đều lười biếng, bạn phải gọi một trong các phương thức bộ điều hợp tiêu thụ để có được kết quả từ các lệnh gọi đến bộ điều hợp iterator.

Sử dụng Closures Capture Môi trường của Chúng

Nhiều bộ điều hợp iterator lấy closure làm đối số, và thường các closure mà chúng ta sẽ chỉ định làm đối số cho bộ điều hợp iterator sẽ là closure capture môi trường của chúng.

Cho ví dụ này, chúng ta sẽ sử dụng phương thức filter nhận một closure. Closure lấy một phần tử từ iterator và trả về một bool. Nếu closure trả về true, giá trị sẽ được bao gồm trong lần lặp được tạo ra bởi filter. Nếu closure trả về false, giá trị sẽ không được bao gồm.

Trong Listing 13-16, chúng ta sử dụng filter với một closure capture biến shoe_size từ môi trường của nó để lặp qua một tập hợp các phiên bản struct Shoe. Nó sẽ chỉ trả về những đôi giày có kích thước được chỉ định.

#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn filters_by_size() {
        let shoes = vec![
            Shoe {
                size: 10,
                style: String::from("sneaker"),
            },
            Shoe {
                size: 13,
                style: String::from("sandal"),
            },
            Shoe {
                size: 10,
                style: String::from("boot"),
            },
        ];

        let in_my_size = shoes_in_size(shoes, 10);

        assert_eq!(
            in_my_size,
            vec![
                Shoe {
                    size: 10,
                    style: String::from("sneaker")
                },
                Shoe {
                    size: 10,
                    style: String::from("boot")
                },
            ]
        );
    }
}

Hàm shoes_in_size lấy quyền sở hữu của một vector giày và một kích thước giày làm tham số. Nó trả về một vector chỉ chứa giày có kích thước được chỉ định.

Trong phần thân của shoes_in_size, chúng ta gọi into_iter để tạo một iterator lấy quyền sở hữu của vector. Sau đó, chúng ta gọi filter để điều chỉnh iterator đó thành một iterator mới chỉ chứa các phần tử mà closure trả về true.

Closure capture tham số shoe_size từ môi trường và so sánh giá trị đó với kích thước của mỗi đôi giày, giữ lại chỉ những đôi giày có kích thước được chỉ định. Cuối cùng, gọi collect thu thập các giá trị được trả về bởi iterator đã điều chỉnh vào một vector được trả về bởi hàm.

Test cho thấy rằng khi chúng ta gọi shoes_in_size, chúng ta chỉ nhận lại những đôi giày có cùng kích thước với giá trị mà chúng ta đã chỉ định.