Итераторы в Rust
Итераторы в Rust предоставляют мощный и гибкий способ эффективной обработки данных путем преобразования, фильтрации и агрегации элементов в коллекции.
В отличие от традиционных циклов, итераторы Rust ленивы — то есть они не выполняют никаких действий, пока им явно не указано это делать. Эта ленивость делает итераторы эффективными, поскольку они оценивают элементы только при необходимости, часто объединяя несколько преобразований в один проход по данным.
В этом уроке мы погрузимся в ядро системы итераторов Rust и узнаем, как использовать такие методы, как .map()
, .filter()
и .fold()
(похожие на reduce
), для создания выразительного функционального кода.
Введение в итераторы
Шаблон итератора позволяет вам выполнять задачи над последовательностью элементов по очереди. Итератор обрабатывает логику перехода от одного элемента к другому и определяет, когда последовательность завершена, освобождая вас от реализации этой логики вручную. В Rust итераторы ленивы, то есть они не выполняют никакой работы, пока вы не вызовете методы, которые используют итератор.
Например:
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {}", val);
}
}
В этом коде мы создаем итератор по вектору с помощью метода .iter()
. Затем мы используем цикл for
для итерации по каждому значению в итераторе и выводим его на печать. Шаблон итератора позволяет нам абстрагироваться от логики итерации по вектору, делая наш код более читаемым и выразительным.
Trait Iterator и следующий метод
Trait Iterator — основная черта для итераторов в Rust. Она определяет поведение метода .next()
, который возвращает Option
, содержащий следующее значение в последовательности. Когда итератор завершается, .next()
возвращает None
.
Все итераторы в Rust реализуют черту Iterator, которая определяет один обязательный метод: next
.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
type Item
: Это связанный тип, который определяет тип элементов, которые будет выдавать итератор.next()
: Перемещает итератор вперед и возвращает следующий элемент илиNone
, если итератор исчерпан.
Например:
fn main() {
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);
}
В этом коде мы создаем итератор по вектору и вызываем метод .next()
для получения следующего значения в последовательности. Мы используем макрос assert_eq!
для проверки правильности значений, возвращаемых .next()
.
Основные типы итераторов
Rust предоставляет три основных типа итераторов: потребляющие адаптеры, адаптеры итераторов и сами итераторы. Потребляющие адаптеры берут на себя владение итератором и потребляют его, производя одно значение. Адаптеры итераторов принимают итератор и возвращают новый итератор с другим поведением. Сами итераторы являются базовым признаком для всех итераторов и определяют поведение метода .next()
.
Три основных типа итераторов:
iter()
: Этот метод создает итератор, который заимствует каждый элемент коллекции.into_iter()
: Этот метод создает итератор, который становится владельцем каждого элемента коллекции.iter_mut()
: Этот метод создает итератор, который изменяемо заимствует каждый элемент коллекции.
Давайте рассмотрим назначение, владельца и вариант использования каждого типа итератора:
iter()
- Назначение: Этот метод создает итератор, который заимствует каждый элемент коллекции.
- Право собственности: Исходная коллекция остается нетронутой после вызова
iter()
. - Вариант использования: Используйте
iter()
, когда вы хотите выполнить итерацию по коллекции, не становясь ее владельцем.
Например:
let numbers = vec![1, 2, 3];
for num in numbers.iter() {
println!("{}", num); // Outputs each number
}
println!("after iter: {:?}", numbers); // Outputs: [1, 2, 3]
iter_mut()
- Назначение: Этот метод создает итератор, который изменяемо заимствует каждый элемент коллекции.
- Право собственности: Исходная коллекция остается нетронутой после вызова
iter_mut()
. - Вариант использования: Используйте
iter_mut()
, когда вы хотите выполнить итерацию по коллекции и изменить ее элементы.
Например:
let mut numbers = vec![1, 2, 3];
for num in numbers.iter_mut() {
*num += 1; // Mutates each element by adding 1
println!("{}", num);
}
println!("{:?}", numbers); // Outputs: [2, 3, 4]
into_iter()
- Назначение: Этот метод создает итератор, который становится владельцем каждого элемента коллекции.
- Право собственности: Исходная коллекция потребляется после вызова
into_iter()
. - Вариант использования: Используйте
into_iter()
, когда вы хотите переместить каждый элемент из коллекции.
Например:
let numbers = vec![1, 2, 3];
for num in numbers.into_iter() {
println!("{}", num); // Outputs each number
}
// println!("{:?}", numbers); // Error: `numbers` is no longer accessible
Методы изменения или использования итераторов: .map(), .filter() и .fold()
Rust предоставляет множество методов для изменения или потребления итераторов, что позволяет эффективно преобразовывать, фильтровать и агрегировать элементы. Эти методы называются адаптерами итераторов и объединяются вместе для создания выразительного, функционального кода.
Некоторые распространенные адаптеры итераторов включают в себя:
.map()
: Преобразует каждый элемент в итераторе..filter()
: Фильтрует элементы на основе функции-предиката..fold()
: Объединяет элементы в одно значение.
Давайте рассмотрим каждый из этих методов более подробно:
.map() — Преобразование элементов
Метод .map()
преобразует каждый элемент в итераторе, применяя к нему замыкание. Замыкание принимает элемент в качестве входных данных и возвращает новое значение, которое затем выдается итератором.
let numbers = [1, 2, 3, 4, 5];
let squares: Vec<_> = numbers.iter().map(|&x| x * x).collect();
println!("Map - Squares: {:?}", squares); // Outputs: [1, 4, 9, 16, 25]
В этом коде мы используем метод .map() для возведения в квадрат каждого элемента в массиве чисел. Замыкание |&x| x * x
возводит в квадрат входной x
, а полученные значения собираются в новый вектор.
.filter() — Фильтрующие элементы
Метод .filter()
фильтрует элементы в итераторе на основе функции предиката. Замыкание принимает элемент в качестве входных данных и возвращает логическое значение, указывающее, следует ли включать элемент в выходные данные.
let numbers = [1, 2, 3, 4, 5];
let evens: Vec<_> = numbers.iter().filter(|&x| x % 2 == 0).collect();
println!("Filter - Evens: {:?}", evens); // Outputs: [2, 4]
В этом коде мы используем метод .filter()
для выбора только четных чисел из массива чисел. Замыкание |&x| x % 2 == 0
проверяет, является ли входное число x
четным, и полученные значения собираются в новый вектор.
.fold() — Агрегирование элементов
Метод .fold()
объединяет элементы итератора в одно значение. Он принимает начальное значение и замыкание, которое объединяет текущее значение аккумулятора с каждым элементом итератора. Он похож на функцию reduce
в других языках.
let numbers = [1, 2, 3, 4, 5];
let sum = numbers.iter().fold(0, |acc, &x| acc + x);
println!("Fold - Sum: {}", sum); // Outputs: 15
В этом коде мы используем метод .fold()
для вычисления суммы всех элементов в массиве numbers
. Начальное значение 0 передается в качестве первого аргумента, а замыкание |acc, &x| acc + x
добавляет каждый элемент x
к аккумулятору acc
.
Итоги
Итераторы Rust предоставляют гибкий способ эффективной обработки данных, позволяя выполнять преобразования, фильтрацию и агрегацию по коллекциям.
Основные выводы:
- Ленивость: Итераторы ничего не делают, пока не будут явно использованы.
- Управление владением и изменчивостью: Выбирайте между
iter()
,iter_mut()
иinto_iter()
для точного управления обработкой элементов. - Функциональные методы: Такие методы, как
.map()
,.filter()
и.fold()
, позволяют выполнять выразительные функциональные преобразования.
Итераторы обеспечивают детальный контроль над владением данными и их изменчивостью во время итерации, что делает Rust эффективным и мощным средством обработки коллекций.
Весь код доступен на GitHub.