Итераторы

Последнее обновление: 18.05.2024

Итераторы играют важную роль в стандартной библиотеке языка Rust при работе с коллекциями. В частности, итераторы позволяют перебрать объект, например, получить из массива элементы. По своей сути итератор — это объект, который генерирует последовательность значений. И эту последовательность можно удобно использовать в циклах или других операциях. Стандартная библиотека Rust предоставляет широкий спектр итераторов, адаптированных для разных типов данных, включая векторы, строки, хеш-таблицы и многое другое. Кроме того, мы сами можем создавать собственные итераторы в соответствии с конкретными требованиями.

Итератор поставляется с набором методов, которые позволяют нам просматривать коллекцию элементов один за другим, из которых отметим прежде всего метод next(), который позволяет извлечь следующий элемент в последовательности, оборачивая его в значение перечисления Option. Если последовательность действительно имеет элемент, то метод возвратит его в виде Some(item), где item - это извлекаемый элемент. Но если мы достигли конца последовательности, то есть элементов больше нет, то метод возвращает None.

Реализация своего итератора

Возможность итерации по объекту предоставляет специальный трейт - Iterator:

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // остальные методы
}

Item представляет ассоциированный тип, который будет применяться в качестве типа генерируемых итератором элементов.

Метод next() собственно генерирует значения типа Item и возвращает их в виде Option<Self::Item>. Рассмотрим простейший пример реализации итератора и его метода next():

struct Counter {
    count: u32
}

impl Iterator for Counter {

    type Item = u32; 

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 10 {
            self.count += 1;
            Some(self.count)   
        } else {
            None
        }
    }
}

fn main() {
    let mut counter = Counter { count: 0 };
    for number in &mut counter {
        println!("Counter: {}", number);
    }
}

Здесь определена структура Counter, которая в переменной count хранит некоторое значение. Структура Counter применяет трейт Iterator. В качестве ассоциированного типа Item применяется тип u32 (то есть 32-разрядные целые числа без знака) - это тип возвращаемых итератором значений.

Реализация метода next() просто увеличивает значение count и возвращает это значение в виде Some(self.count). Допустим, в данном случае нам надо возвратить 10 чисел. Когда мы возвратим все числа, то, чтобы показать, что перебор закончен, возвращаем None. В итоге мы получим следующий консольный вывод:

Counter: 1
Counter: 2
Counter: 3
Counter: 4
Counter: 5
Counter: 6
Counter: 7
Counter: 8
Counter: 9
Counter: 10

Другой пример - определим генератор простых чисел:

struct PrimeGenerator { 

    current: u32
}

impl Iterator for PrimeGenerator { 

    type Item = u32;
    fn next(&mut self) -> Option<Self::Item> {

        loop {
            self.current += 1;
            if is_prime(self.current) {
                return Some(self.current);
            }
        }
    }
}


fn is_prime(num: u32) -> bool {

    if num <= 1 { return false; }

    for i in 2..(num / 2 + 1) {
        if num % i == 0 { return false; }
    }
    true
}

fn main() { 

    let prime_sequence = PrimeGenerator { current: 1 };

    for prime in prime_sequence.take(10) {
        println!("{}", prime);
    }
}

Здесь для типа PrimeGenerator реализуем итератор таким образом, чтобы в методе next() в бесконечном цикле возвращались простые числа. Поскольку этот генератор бесконечен, то в функции main при переборе чисел с помощью функции take() берем только 10 первых простых чисел. Консольный вывод:

2
3
5
7
11
13
17
19
23
29

Кроме трейта Iterator следует отметить еще один тип трейта итератора - IntoIterator. Он позволяет рассматривать типы как итеративные, доступные для перебора. Ключевой его компонент - метод into_iter(), который возвращает итератор для определенного типа. Реализация IntoIterator позволяет использовать их в цикле for. Простейший пример:

struct MyCollection {

    data: Vec<i32> // условные данные
}

impl MyCollection {

    // определяем функцию-конструктор
    fn new() -> Self { MyCollection { data: Vec::new() } }

    // Добавляем в коллекцию
    fn add(&mut self, value: i32) {

        self.data.push(value);
    }
}

// реализуем трейт IntoIterator
impl IntoIterator for MyCollection {

    type Item = i32;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {

        self.data.into_iter()
    }
}

fn main() {

    let mut collection = MyCollection::new();

    collection.add(2);
    collection.add(4);
    collection.add(8);

    for item in collection { // перебираем коллекцию

        println!("Item: {}", item);
    }
}

Здесь мы определяем структуру MyCollection, которая содержит данные в виде вектора типа Vec<i32>. То есть MyCollection будет выступать своего рода контейнером для набора целых чисел типа i32. Далее реализуем для структуры MyCollection метод new(), который отвечает за создание нового экземпляра MyCollection с пустым вектором. И также реализуем метод add(), который добавляет числа в вектор.

Затем реализуем трейт IntoIterator для нашей структуры MyCollection. В реализации указываем ассоциированные типы: Item представляет тип элементов в нашей коллекции (в данном случае i32), а IntoIter указывает, как создать итератор для нашей коллекции. Метод into_iter() определяет, как нашу коллекцию можно использовать в качестве итератора, позволяя нам перебирать ее содержимое. В функции main создаем экземпляр MyCollection, добавляем в него несколько целых чисел, а затем используем цикл for для перебора коллекции. Консольный вывод:

Item: 2
Item: 4
Item: 8
Помощь сайту
Юмани:
410011174743222
Номер карты:
4048415020898850