Обробка Послідовностей Елементів з Ітераторами
Шаблон ітератора дозволяє вам виконати певну задачу з послідовністю елементів по черзі. Ітератор відповідає за логіку ітерації по елементах і визначення, коли послідовність закінчується. Коли ви користуєтесь ітераторами, вам не треба самостійно реалізовувати цю логіку.
У Rust ітератори є лінивими, тобто вони нічого не роблять до того моменту, коли ви викличете метод, що поглине ітератор і використає його. Наприклад, код у Блоці коду 13-10 створює ітератор по елементах вектора v1
, викликавши метод iter
, визначений для Vec<T>
. Цей код як такий не робить нічого корисного.
fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); }
Ітератор зберігається у змінній 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); } }
У мовах, які не мають у своїх стандартних бібліотеках ітераторів, ви, ймовірно, опишете такий функціонал, почавши зі змінної для індексу 0, використовуючи цю змінну для індексування вектора, щоб отримати значення, та збільшуючи значення змінної в циклі, поки вона не досягне загальної кількості елементів у векторі.
Ітератори обробляють всю цю логіку за вас, скорочуючи код, який ви потенційно можете зіпсувати. Ітератори дають вам більше гнучкості для використання тієї ж логіки з різними типами послідовностей, а не лише структурами даних, які ви можете індексувати, такими як вектори. Розгляньмо, як ітератори це роблять.
Трейт 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);
}
}
Зверніть увагу, що нам потрібно зробити 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);
}
}
Нам не дозволено використовувати v1_iter
після виклику sum
, оскільки sum
перебирає володіння ітератором, на якому його викликано.
Методи, що Створюють Інші Ітератори
Адаптери ітераторів - це методи, визначені для трейта Iterator
, які не поглинають ітератор. Натомість вони створюють інші ітератори, змінюючи певний аспект оригінального ітератора.
Блок коду 13-14 показує приклад виклику метода-адаптора ітератора map
, який приймає замикання, яке викличе для кожного елементу під час ітерації. Метод map
повертає новий ітератор, який виробляє модифіковані елементи. Замикання створює новий ітератор, у якому кожен елемент вектора буде збільшено на 1:
Файл: src/main.rs
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); }
Однак, цей код видає попередження:
$ 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]); }
Оскільки map
приймає замикання, ми можемо вказати будь-яку операцію, яку хочемо виконати з кожним елементом. Це чудовий приклад того, як замикання дозволяють вам встановити певну поведінку, використовуючи поведінку ітерацій, надану трейтом Iterator
.
Ви можете з'єднати багато викликів адаптерів ітераторів для виконання складних дій, щоб це було читабельно. Але оскільки всі ітератори є ледачими, ви маєте викликати один з методів, що поглинають адаптер, щоб отримати результати викликів адаптерів ітераторів.
Використання Замикань, що Захоплюють Своє Середовище
Багато адаптерів ітераторів приймають аргументами замикання, і зазвичай замикання, які ми вказуємо аргументами до адаптерів ітераторів будуть замиканнями, що захоплюють своє середовище.
Для цього прикладу ми скористаємося методом filter
, що приймає замикання. Замикання отримає елемент з ітератора і повертає bool
. Якщо замикання повертає 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")
},
]
);
}
}
Функція shoes_in_size
приймає володіння вектором взуття і розмір взуття. Вона повертає вектор, що містить лише взуття зазначеного розміру.
У тілі shoes_in_size
ми викликаємо into_iter
для створення ітератора, що приймає володіння вектором. Тоді ми викликаємо filter
, щоб адаптувати ітератор у новий ітератор, що містить лише елементи, для яких замикання повертає true
.
Замикання захоплює параметр shoe_size
із середовища і порівнює значення із розміром кожної пари взуття, лишаючи тільки взуття зазначеного розміру. Нарешті, виклик collect
збирає значення, повернуті адаптованим ітератором, у вектор, який функція повертає.
Тест показує, що коли ми викликаємо shoes_in_size
, ми отримуємо назад лише взуття, яке має розмір, що дорівнює вказаному значенню.