Обробка серії елементів за допомогою iterator
Шаблон iterator дає змогу виконувати певне завдання над послідовністю елементів по черзі. За логіку проходження по кожному елементу та визначення моменту, коли послідовність завершилася, відповідає iterator. Коли ви використовуєте iterator, вам не потрібно реалізовувати цю логіку самостійно.
У Rust iterator-и є ледачими, тобто вони не мають жодного ефекту, доки ви не викличете методи, які споживають iterator, щоб використати його. Наприклад, код у Listing 13-10 створює iterator над елементами у векторі v1, викликаючи метод iter, визначений на Vec<T>. Сам по собі цей код нічого корисного не робить.
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
}
Iterator зберігається у змінній v1_iter. Після того як ми створили iterator, ми можемо використовувати його різними способами. У Listing 3-5 ми проходили по масиву за допомогою циклу for, щоб виконати певний код над кожним його елементом. Під капотом це неявно створювало, а потім споживало iterator, але до цього моменту ми не заглиблювалися в те, як саме це працює.
У прикладі в Listing 13-11 ми відокремлюємо створення iterator від використання iterator у циклі for. Коли цикл for викликається з використанням iterator у v1_iter, кожен елемент у iterator використовується в одній ітерації циклу, яка виводить кожне значення.
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {val}");
}
}
У мовах, які не мають iterator-ів, наданих їхніми стандартними бібліотеками, ви, ймовірно, реалізували б цю саму функціональність, започаткувавши змінну з індексу 0, використовуючи цю змінну для індексування у векторі, щоб отримати значення, і збільшуючи значення змінної в циклі, доки воно не досягне загальної кількості елементів у векторі.
Iterator-и виконують усю цю логіку за вас, скорочуючи повторюваний код, у якому ви потенційно могли б помилитися. Iterator-и дають вам більше гнучкості для використання тієї самої логіки з багатьма різними видами послідовностей, а не лише зі структурами даних, у які можна індексуватися, як-от вектори. Розгляньмо, як iterator-и це роблять.
Трейт Iterator і метод next
Усі iterator-и реалізують трейт під назвою Iterator, який визначено у стандартній бібліотеці. Визначення трейт має такий вигляд:
#![allow(unused)]
fn main() {
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// methods with default implementations elided
}
}
Зверніть увагу, що це визначення використовує новий синтаксис: type Item і Self::Item, які визначають асоційований тип із цим трейт. Ми детально поговоримо про асоційовані типи в Chapter 20. Наразі вам потрібно знати лише те, що цей код означає: реалізація трейт Iterator вимагає, щоб ви також визначили тип Item, і цей тип Item використовується в типі повернення методу next. Іншими словами, тип Item буде типом, що повертається з iterator.
Трейт Iterator вимагає від реалізаторів визначити лише один метод: метод next, який повертає один елемент iterator за раз, загорнутий у Some, а коли ітерація завершена, повертає None.
Ми можемо викликати метод next на iterator безпосередньо; Listing 13-12 демонструє, які значення повертаються з повторних викликів next на iterator, створеному з вектора.
#[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 на iterator змінює внутрішній стан, який iterator використовує, щоб відстежувати, де він перебуває в послідовності. Іншими словами, цей код споживає iterator, або використовує його цілком. Кожен виклик next забирає один елемент з iterator. Нам не потрібно було робити v1_iter змінною, коли ми використовували цикл for, тому що цикл взяв v1_iter у власність і зробив його змінним приховано.
Також зверніть увагу, що значення, які ми отримуємо з викликів next, є незмінними посиланнями на значення у векторі. Метод iter створює iterator за незмінними посиланнями. Якщо ми хочемо створити iterator, який бере вектор v1 у власність і повертає власні значення, ми можемо викликати into_iter замість iter. Аналогічно, якщо ми хочемо ітерувати за змінними посиланнями, ми можемо викликати iter_mut замість iter.
Методи, що споживають iterator
Трейт Iterator має низку різних методів із реалізаціями за замовчуванням, наданими стандартною бібліотекою; ви можете дізнатися про ці методи, переглянувши документацію API стандартної бібліотеки для трейт Iterator. Деякі з цих методів викликають метод next у своєму визначенні, тому від вас і вимагається реалізувати метод next, коли ви реалізуєте трейт Iterator.
Методи, що викликають next, називаються адаптерами, що споживають, тому що їхній виклик використовує iterator цілком. Один із прикладів — метод sum, який бере iterator у власність і проходить крізь елементи, неодноразово викликаючи next, таким чином споживаючи iterator. Під час ітерації він додає кожен елемент до поточної суми та повертає цю суму, коли ітерація завершена. Listing 13-13 містить тест, що ілюструє використання методу 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);
}
}
Ми не маємо права використовувати v1_iter після виклику sum, тому що sum бере у власність iterator, на якому ми його викликаємо.
Методи, що створюють інші iterator
Адаптери iterator — це методи, визначені на трейт Iterator, які не споживають iterator. Натомість вони створюють різні iterator-и, змінюючи певний аспект оригінального iterator.
Listing 13-14 показує приклад виклику методу-адаптера iterator map, який приймає замикання, що викликається для кожного елемента під час проходження по елементах. Метод map повертає новий iterator, який видає змінені елементи. Замикання тут створює новий iterator, у якому кожен елемент з вектора буде збільшено на 1.
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: 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`
Код у Listing 13-14 нічого не робить; замикання, яке ми вказали, ніколи не викликається. Попередження нагадує нам, чому: адаптери iterator є ледачими, і тут нам потрібно спожити iterator.
Щоб виправити це попередження і спожити iterator, ми використаємо метод collect, який ми застосовували з env::args у Listing 12-1. Цей метод споживає iterator і збирає отримані значення в тип даних колекції.
У Listing 13-15 ми збираємо результати ітерації по iterator, що повертається з виклику map, у вектор. Цей вектор зрештою міститиме кожен елемент з оригінального вектора, збільшений на 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]);
}
Оскільки map приймає замикання, ми можемо вказати будь-яку операцію, яку хочемо виконати над кожним елементом. Це чудовий приклад того, як замикання дають змогу налаштувати певну поведінку, повторно використовуючи поведінку ітерації, яку надає трейт Iterator.
Ви можете ланцюжком викликати кілька адаптерів iterator, щоб виконувати складні дії у зрозумілий спосіб. Але оскільки всі iterator-и є ледачими, вам потрібно викликати один із методів адаптера, що споживає, щоб отримати результати з викликів адаптерів iterator.
Замикання, що захоплюють своє середовище
Багато адаптерів iterator приймають замикання як аргументи, і зазвичай замикання, які ми вказуватимемо як аргументи адаптерів iterator, будуть замиканнями, що захоплюють своє середовище.
Для цього прикладу ми використаємо метод filter, який приймає замикання. Замикання отримує елемент з iterator і повертає bool. Якщо замикання повертає true, значення буде включено в ітерацію, створену filter. Якщо замикання повертає false, значення не буде включено.
У Listing 13-16 ми використовуємо filter із замиканням, яке захоплює змінну shoe_size зі свого середовища, щоб ітерувати по колекції екземплярів структури Shoe. Воно поверне лише взуття вказаного розміру.
#[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, щоб створити iterator, який бере вектор у власність. Потім ми викликаємо filter, щоб адаптувати цей iterator у новий iterator, який містить лише елементи, для яких замикання повертає true.
Замикання захоплює параметр shoe_size із середовища та порівнює значення з розміром кожної пари взуття, залишаючи лише взуття вказаного розміру. Нарешті, виклик collect збирає значення, повернуті адаптованим iterator, у вектор, який повертається функцією.
Тест показує, що коли ми викликаємо shoes_in_size, то отримуємо назад лише взуття, яке має той самий розмір, що й вказане нами значення.