Обробка послідовностей елементів за допомогою ітераторів

Шаблон ітератора дозволяє вам виконати певну задачу з послідовністю елементів по черзі. Ітератор відповідає за логіку ітерації по елементах і визначення, коли послідовність закінчується. Коли ви користуєтесь ітераторами, вам не треба самостійно реалізовувати цю логіку.

У Rust ітератори є лінивими, тобто вони нічого не роблять до того моменту, коли ви викличете метод, що поглине ітератор і використає його. Наприклад, код у Блоці коду 13-10 створює ітератор по елементах вектора v1, викликавши метод iter, визначений для Vec<T>. Цей код як такий не робить нічого корисного.

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

    let v1_iter = v1.iter();
}

Блок коду 13-10: створення ітератора

Ітератор зберігається у змінній v1_iter. Після того, як ми створили ітератор, ми можемо використовувати його у різні способи. У Блоці коду 3-5 з Розділу 3 ми ітерували по масиву за допомогою циклу for, щоб виконати певний код на кожному елементі. Під капотом тут неявно був створений і поглинутий ітератор, але до цього часу ми не звертали уваги на те, як саме це працює.

У прикладі з Блоку коду 13-11 ми відокремлюємо створення ітератора від його використання в циклі for. Коли цикл for викликають з ітератором у v1_iter, кожен елемент у ітераторі використовується одній ітерації циклу, який виводить кожне значення.

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

    let v1_iter = v1.iter();

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

Блок коду 13-11: використання ітератора у циклі for

In languages that don’t have iterators provided by their standard libraries, you would likely write this same functionality by starting a variable at index 0, using that variable to index into the vector to get a value, and incrementing the variable value in a loop until it reached the total number of items in the vector.

Ітератори обробляють всю цю логіку за вас, скорочуючи код, який ви потенційно можете зіпсувати. Ітератори дають вам більше гнучкості для використання тієї ж логіки з різними типами послідовностей, а не лише структурами даних, які ви можете індексувати, такими як вектори. Розгляньмо, як ітератори це роблять.

Трейт Iterator і метод next

Усі ітератори реалізують трейт, що зветься Iterator, визначений у стандартній бібліотеці. Визначення цього трейту виглядає ось так:

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

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

    // методи зі стандартною реалізацією пропущені
}
}

Зверніть увагу, що це визначення використовує новий синтаксис: type Item і Self::Item, які визначають асоційований тип цього трейта. Ми глибше поговоримо про асоційовані типи у Розділі 19. Поки що, все, що вам слід знати - це те, що цей код каже, що реалізація трейту Iterator також вимагає, щоб ви визначили тип Item, і цей тип Item використовується як тип, що повертається методом next. Іншими словами, тип Item буде типом, повернутим з ітератора.

Трейт Iterator потребує від того, хто його реалізовує, визначення лише одного методу: методу next, який повертає за раз один елемент ітератора, обгорнутий у Some і, коли ітерація закінчиться, повертає None.

Ми можемо викликати метод next для ітераторів безпосередньо; Блок коду 13-12 демонструє, які значення повертаються повторюваними викликами next для ітератора, створеного з вектора.

Файл: src/lib.rs

#[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);
    }
}

Блок коду 13-12: виклик методу next для ітератора

Зверніть увагу, що нам потрібно зробити v1_iter мутабельним: виклик методу next для ітератора змінює його внутрішній стан, який використовується для відстеження, де він знаходиться в послідовності. Іншими словами, цей код поглинає, чи використовує, ітератор. Кожен виклик next з'їдає елемент з ітератора. Нам не треба було робити v1_iter мутабельним, коли ми використали його в циклі for, бо цикл взяв володіння v1_iter і зробив його мутабельним за лаштунками.

Також зверніть увагу, що значення, які ми отримуємо від викликів next, є немутабельними посиланнями на значення у векторі. Метод iter створює ітератор по незмінних посиланнях. Якщо ми хочемо створити ітератор, який приймає володіння v1 і повертає значення, що належать нам, ми можемо викликати into_iter замість iter. Аналогічно, якщо ми хочемо ітерувати по мутабельних посиланнях, ми можемо викликати iter_mut замість iter.

Методи, що поглинають ітератор

Трейт Iterator має ряд різних методів з реалізаціями по замовчуванню що надаються стандартною бібліотекою; ви можете дізнатися про ці методи в стандартній документації API для трейта Iterator. Деякі з цих методів викликають у своєму визначенні метод next, чому і необхідно визначити метод next при реалізації трейта Iterator.

Методи, що викликають next, звуться поглинаючими адапторами, бо їх виклик використовує ітератор. Один із прикладів - це метод sum, який бере володіння ітератором і ітерує по елементах, раз за разом викликаючи next, таким чином поглинаючи ітератор. Під час ітерації він додає кожен елемент до поточної загальної суми і повертає загальну суму, коли ітерація завершена. Блок коду 13-13 має тест, що ілюструє використання методу sum:

Файл: src/lib.rs

#[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);
    }
}

Блок коду 13-13: виклик методу sum для отримання загальної суми усіх елементів ітератора

Нам не дозволено використовувати v1_iter після виклику sum, оскільки sum перебирає володіння ітератором, на якому його викликано.

Методи, що створюють інші ітератори

Адаптери ітераторів - це методи, визначені для трейта Iterator, які не поглинають ітератор. Натомість вони створюють інші ітератори, змінюючи певний аспект оригінального ітератора.

Блок коду 13-17 показує приклад виклику метода-адаптора ітератора map, який приймає замикання, яке викличе для кожного елементу під час ітерації. Метод map повертає новий ітератор, який виробляє модифіковані елементи. Замикання створює новий ітератор, у якому кожен елемент вектора буде збільшено на 1:

Файл: src/main.rs

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

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

Блок коду 13-14: Виклик адаптора map для створення нового ітератора

Однак, цей код видає попередження:

$ 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: `#[warn(unused_must_use)]` on by default
  = note: iterators are lazy and do nothing unless consumed

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

Код у Блоці коду 13-14 нічого не робить; замикання, яке ми вказали, ніколи не було викликано. Попередження нагадує нам, чому: адаптори ітераторів ліниві, і нам потрібно поглинути ітератор.

Щоб виправити це попередження і поглинути ітератор, ми використаємо метод collect, який ми використовували у Розділі 12 із env::args у Блоці коду 12-1. Цей метод поглинає ітератор і збирає отримані в результаті значення в колекцію.

У Блоці коду 13-15 ми зібрали результати ітерування по ітератору, повернутому викликом map, у вектор. Цей вектор в результаті міститиме всі елементи оригінального вектора, збільшені на 1.

Файл: src/lib.rs

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]);
}

Блок коду 13-15: виклик методу map для створення нового ітератора і виклик методу collect для поглинання цього нового ітератора і створення вектора

Оскільки map приймає замикання, ми можемо вказати будь-яку операцію, яку хочемо виконати з кожним елементом. Це чудовий приклад того, як замикання дозволяють вам встановити певну поведінку, використовуючи поведінку ітерацій, надану трейтом Iterator.

Ви можете з'єднати багато викликів адаптерів ітераторів для виконання складних дій, щоб це було читабельно. Але оскільки всі ітератори є ледачими, ви маєте викликати один з методів, що поглинають адаптер, щоб отримати результати викликів адаптерів ітераторів.

Використання замикань, що захоплюють своє середовище

Багато адаптерів ітераторів приймають аргументами замикання, і зазвичай замикання, які ми вказуємо аргументами до адаптерів ітераторів будуть замиканнями, що захоплюють своє середовище. Для цього прикладу ми скористаємося методом filter, що приймає замикання. Замикання отримає елемент з ітератора і повертає булеве значення. Якщо замикання повертає true, значення буде включено в ітерації, вироблені filter. Якщо замикання повертає false, значення не буде включено.

У Блоці коду 13-16 ми використовуємо filter із замиканням, яке захоплює змінну shoe_size зі свого середовища для ітерування по колекції екземплярів структур Shoe. Воно поверне лише взуття зазначеного розміру.

Файл: src/lib.rs

#[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")
                },
            ]
        );
    }
}

Блок коду 13-16: використання методу filter із замиканням, що захоплює shoe_size

Функція shoes_in_size приймає параметрами вектор взуття (за володінням) і розмір взуття. Вона повертає вектор, що містить лише взуття зазначеного розміру.

У тілі shoes_in_size ми викликаємо into_iter для створення ітератора, що перебирає володіння вектором. Тоді ми викликаємо filter, щоб адаптувати ітератор у новий ітератор, що містить лише елементи, для яких замикання повертає true.

Замикання захоплює параметр shoe_size із середовища і порівнює значення із розміром кожної пари взуття, лишаючи тільки взуття зазначеного розміру. Нарешті, виклик collect збирає значення, повернуті адаптованим ітератором, у вектор, який функція повертає.

Тест показує, що коли ми викликаємо shoes_in_size, ми отримуємо назад лише взуття, яке має розмір, що дорівнює значенню, яке ми вказали.