Главная страница
Навигация по странице:

  • Листинг 13-10: Создание итератора

  • Листинг 13-11: Использование итератора в цикле

  • Типаж

  • Листинг 13-12: Вызов метода next итератора

  • Методы, которые потребляют итератор

  • Листинг 13-13: Вызов метода sum для получения суммы всех элементов в итераторе

  • Методы, которые создают другие итераторы

  • Листинг 13-14: Вызов адаптера итератора map для создания нового итератора

  • Листинг 13-15: Вызов метода map для создания нового итератора, а затем вызов метода collect для потребления нового итератора и создания вектора

  • Использование замыканий, которые захватывают переменные окружения

  • Листинг 13-16. Использование метода filter с замыканием, фиксирующим

  • Улучшение проекта ввода/вывода

  • Удаление метода

  • Листинг 13-17: Репродукция функции Config::build из листинга 12-23

  • Использование возвращённого итератора напрямую

  • Листинг 13-18: Передача возвращаемого значения из env::args в

  • Листинг 13-19: Обновление сигнатуры Config::build для определения итератора как ожидаемого параметра

  • Использование методов типажа

  • Листинг 13-20: Изменяем тело Config::build так, чтобы использовать методы итератора

  • Язык программирования Rust


    Скачать 7.02 Mb.
    НазваниеЯзык программирования Rust
    Дата12.04.2023
    Размер7.02 Mb.
    Формат файлаpdf
    Имя файлаThe Rust Programming Language_ru.pdf
    ТипУчебник
    #1056301
    страница35 из 62
    1   ...   31   32   33   34   35   36   37   38   ...   62
    Обработка группы элементов с помощью итераторов
    Шаблон итератора позволяет выполнять некоторые задачи над последовательностью элементов. Итератор отвечает за логику итерации по каждому элементу и определяет,
    когда последовательность завершилась. Когда вы используете итераторы, вам не нужно переопределять эту логику самостоятельно.
    В Rust итераторы являются lazy, то есть они не производят никакого эффекта, пока вы не вызовете методы, которые потребляют итератор, чтобы использовать его. Например, код в листинге 13-10 создаёт итератор элементов вектора v1
    , вызывая метод iter
    ,
    определённый для
    Vec
    . Сам по себе этот код не делает ничего полезного.
    Листинг 13-10: Создание итератора
    Итератор хранится в переменной v1_iter
    . Создав итератор, мы можем использовать его различными способами. В листинге 3-5 главы 3 мы выполняли итерацию по массиву с помощью цикла for для выполнения какого-то кода над каждым из его элементов. Под капотом это неявно создавало, а затем потребляло итератор, но до сих пор мы не касались того, как именно это работает.
    В примере в листинге 13-11 мы отделяем создание итератора от его использования в цикле for
    . Когда цикл for вызывается используя итератор v1_iter
    , то для каждого элемента итератора отводится одна итерация цикла, в ходе которой выводится каждое значение.
    Листинг 13-11: Использование итератора в цикле
    for
    В языках, которые не имеют итераторов в стандартной библиотеке, вы, вероятно,
    написали бы эту же функцию следующим образом: взять переменную со значением 0,
    использовать её для индексации вектора, чтобы получить значение, и увеличивать её
    значение в цикле, пока не будет достигнуто общее количество элементов в векторе.
    Итераторы делают все эти шаги за вас, сокращая повторяющийся код, который вы потенциально могли бы испортить. Итераторы дают вам больше гибкости для использования одной и той же логики с различными типами последовательностей, а не let v1 = vec!
    [
    1
    ,
    2
    ,
    3
    ]; let v1_iter = v1.iter(); let v1 = vec!
    [
    1
    ,
    2
    ,
    3
    ]; let v1_iter = v1.iter(); for val in v1_iter { println!
    (
    "Got: {}"
    , val);
    }
    только со структурами данных, которые можно индексировать, типа векторов. Давайте посмотрим как итераторы это делают.
    Типаж Iterator и метод next
    Все итераторы реализуют типаж
    Iterator
    , который определён в стандартной библиотеке. Его определение выглядит так:
    Обратите внимание данное объявление использует новый синтаксис: type Item и
    Self::Item
    , которые определяют ассоциированный тип (associated type) с этим типажом.
    Мы подробнее поговорим о ассоциированных типах в главе 19. Сейчас вам нужно знать,
    что этот код требует от реализаций типажа
    Iterator определить требуемый им тип
    Item и данный тип
    Item используется в методе next
    . Другими словами, тип
    Item будет являться типом элемента, который возвращает итератор.
    Типаж
    Iterator требует, чтобы разработчики определяли только один метод: метод next
    , который возвращает один элемент итератора за раз обёрнутый в вариант
    Some и
    когда итерация завершена, возвращает
    None
    Мы можем вызывать метод next у итераторов напрямую; в листинге 13-12 показано,
    какие значения возвращаются при повторных вызовах next у итератора, созданного из вектора.
    Файл: src/lib.rs
    Листинг 13-12: Вызов метода
    next
    итератора
    pub trait
    Iterator
    { type
    Item
    ; fn next
    (&
    mut self
    ) ->
    Option
    ;
    // methods with default implementations elided
    }
    #[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 итератора изменяет внутреннее состояние итератора, которое итератор использует для отслеживания того, где он находится в последовательности. Другими словами, этот код потребляет (consumes) или использует итератор. Каждый вызов 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
    Листинг 13-13: Вызов метода
    sum
    для получения суммы всех элементов в итераторе
    #[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-17 показан пример вызова метода адаптера итератора map
    , который принимает замыкание и вызывает его для каждого элемента по мере итерации элементов. Метод map возвращает новый итератор, который создаёт изменённые элементы. Замыкание здесь создаёт новый итератор, в котором каждый элемент из вектора будет увеличен на 1:
    Файл: src/main.rs
    Листинг 13-14: Вызов адаптера итератора
    map
    для создания нового итератора
    Однако этот код выдаёт предупреждение:
    Код в листинге 13-14 ничего не делает; указанное нами замыкание никогда не вызывается. Предупреждение напоминает нам, почему: адаптеры итераторов ленивы, и здесь нам нужно использовать итератор.
    Чтобы устранить это предупреждение и использовать итератор, мы воспользуемся методом collect
    , который мы использовали в главе 12 с env::args в листинге 12-1. Этот метод потребляет итератор и собирает полученные значения в тип данных collection.
    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-15 мы собираем в вектор результаты итерирования по итератору, который возвращается в результате вызова map
    . Этот вектор в итоге будет содержать каждый элемент исходного вектора, увеличенный на 1.
    Файл: src/main.rs
    Листинг 13-15: Вызов метода
    map
    для создания нового итератора, а затем вызов метода
    collect
    для
    потребления нового итератора и создания вектора
    Поскольку map принимает замыкание, мы можем указать любую операцию, которую хотим выполнить с каждым элементом. Это отличный пример того, как замыкания позволяют настраивать какое-то поведение при повторном использовании итерационного поведения, предоставляемого типажом
    Iterator
    Вы можете выстроить цепочку из нескольких вызовов адаптеров итератора для выполнения сложных действий в удобочитаемом виде. Но поскольку все итераторы являются "ленивыми", для получения результатов вызовов адаптеров итератора необходимо вызвать один из методов потребляющего адаптера.
    Использование замыканий, которые захватывают переменные
    окружения
    Многие адаптеры итераторов принимают замыкания в качестве аргументов, и обычно замыкания, которые мы будем указывать в качестве аргументов адаптерам итераторов,
    это замыкания, которые фиксируют своё окружение.
    В этом примере мы будем использовать метод filter
    , который принимает замыкание.
    Замыкание получает элемент из итератора и возвращает bool
    . Если замыкание возвращает true
    , значение будет включено в итерацию, создаваемую filter
    . Если замыкание возвращает false
    , значение не будет включено.
    В листинге 13-16 мы используем filter с замыканием, которое захватывает переменную shoe_size из своего окружения для итерации по коллекции экземпляров структуры
    Shoe
    . Он будет возвращать обувь только указанного размера.
    Файл: src/lib.rs 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-16. Использование метода
    filter
    с замыканием, фиксирующим
    shoe_size
    Функция shoes_in_size принимает в качестве параметров вектор с экземплярами обуви и размер обуви, а возвращает вектор, содержащий только обувь указанного размера.
    В теле shoes_in_my_size мы вызываем into_iter чтобы создать итератор, который становится владельцем вектора. Затем мы вызываем filter
    , чтобы превратить этот
    #[derive(PartialEq, Debug)]
    struct
    Shoe
    { size: u32
    , style:
    String
    ,
    } fn shoes_in_size
    (shoes:
    Vec
    , shoe_size: u32
    ) ->
    Vec
    { 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"
    )
    },
    ]
    );
    }
    }
    итератор в другой, который содержит только элементы, для которых замыкание возвращает true
    Замыкание захватывает параметр shoe_size из окружения и сравнивает его с размером каждой пары обуви, оставляя только обувь указанного размера. Наконец, вызов collect собирает значения, возвращаемые адаптированным итератором, в вектор,
    возвращаемый функцией.
    Тест показывает, что когда мы вызываем shoes_in_my_size
    , мы возвращаем только туфли, размер которых совпадает с указанным нами значением.

    Улучшение проекта ввода/вывода
    Вооружившись полученными знаниями об итераторах, мы можем улучшить реализацию работы с вводом-выводом в проекте главы 12, применяя итераторы для того, чтобы сделать некоторые места в коде более понятными и лаконичными. Давайте рассмотрим,
    как итераторы могут улучшить нашу реализацию функции
    Config::build и функции search
    Удаление метода clone используя итератор
    В листинге 12-6 мы добавили код, который принимает срез значений
    String и создаёт экземпляр структуры
    Config путём индексации среза и клонирования значений,
    позволяя структуре
    Config владеть этими значениями. В листинге 13-17 мы воспроизвели реализацию функции
    Config::build
    , как это было в листинге 12-23:
    Файл: src/lib.rs
    Листинг 13-17: Репродукция функции
    Config::build
    из листинга 12-23
    Ранее мы говорили, что не стоит беспокоиться о неэффективных вызовах clone
    , потому что мы удалим их в будущем. Ну что же, это время пришло!
    Здесь нам понадобился clone
    , потому что у нас есть срез с элементами
    String в
    параметре args
    , но функция build не владеет args
    . Чтобы вернуть владение экземпляру
    Config
    , нам пришлось клонировать значения полей query и filename из
    Config
    , чтобы экземпляр
    Config мог владеть их значениями.
    Благодаря нашим новым знаниям об итераторах мы можем изменить функцию build
    ,
    чтобы вместо заимствования среза она принимала в качестве аргумента итератор. Мы impl
    Config { pub fn build
    (args: &[
    String
    ]) ->
    Result
    'static str
    > { if args.len() <
    3
    { return
    Err
    (
    "not enough arguments"
    );
    } let query = args[
    1
    ].clone(); let file_path = args[
    2
    ].clone(); let ignore_case = env::var(
    "IGNORE_CASE"
    ).is_ok();
    Ok
    (Config { query, file_path, ignore_case,
    })
    }
    }
    будем использовать функциональность итератора вместо кода, который проверяет длину среза и обращается по индексу к определённым значениям. Это позволит лучше понять, что делает функция
    Config::build
    , поскольку итератор будет обращаться к значениям.
    Как только
    Config::build получит в своё распоряжение итератор и перестанет использовать операции индексирования с заимствованием, мы сможем переместить значения
    String из итератора в
    Config вместо того, чтобы вызывать clone и создавать новое выделение памяти.
    Использование возвращённого итератора напрямую
    Откройте файл src/main.rs проекта ввода-вывода, который должен выглядеть следующим образом:
    Файл: src/main.rs
    Сначала мы изменим начало функции main
    , которая была в листинге 12-24, на код в листинге 13-18, который теперь использует итератор. Это не будет компилироваться,
    пока мы не обновим
    Config::build
    Файл: src/main.rs
    Листинг 13-18: Передача возвращаемого значения из
    env::args
    в
    Config::build
    Функция env::args возвращает итератор! Вместо того чтобы собирать значения итератора в вектор и затем передавать срез в
    Config::build
    , теперь мы передаём владение итератором, возвращённым из env::args в
    Config::build напрямую.
    fn main
    () { let args:
    Vec
    <
    String
    > = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!(
    "Problem parsing arguments: {err}"
    ); process::exit(
    1
    );
    });
    // --snip--
    } fn main
    () { let config = Config::build(env::args()).unwrap_or_else(|err| { eprintln!(
    "Problem parsing arguments: {err}"
    ); process::exit(
    1
    );
    });
    // --snip--
    }

    Далее нам нужно обновить определение
    Config::build
    . В файле src/lib.rs вашего проекта ввода-вывода изменим сигнатуру
    Config::build так, чтобы она выглядела как в листинге 13-19. Это все ещё не скомпилируется, потому что нам нужно обновить тело функции.
    Файл: src/lib.rs
    Листинг 13-19: Обновление сигнатуры
    Config::build
    для определения итератора как ожидаемого
    параметра
    Документация стандартной библиотеки для функции env::args показывает, что тип возвращаемого ею итератора - std::env::Args
    , и этот тип реализует признак
    Iterator и
    возвращает значения
    String
    Мы обновили сигнатуру функции
    Config::build
    , чтобы параметр args имел универсальный тип ограниченный трейтом impl Iterator
    вместо
    &
    [String]
    . Такое использование синтаксиса impl Trait
    , который мы обсуждали в разделе " Трейты как параметры"
    главы 10, означает, что args может быть любым типом,
    реализующим тип
    Iterator и возвращающим элементы
    String
    Поскольку мы владеем args и будем изменять args в процессе итерации над ним, мы можем добавить ключевое слово mut в спецификацию параметра args
    , чтобы сделать его изменяемым.
    Использование методов типажа Iterator вместо индексов
    Далее мы подправим содержимое
    Config::build
    . Поскольку args реализует признак
    Iterator
    , мы знаем, что можем вызвать у него метод next
    ! В листинге 13-20 код из листинга 12-23 обновлён для использования метода next
    :
    Файл: src/lib.rs impl
    Config { pub fn build
    ( mut args: impl
    Iterator
    String
    >,
    ) ->
    Result
    'static str
    > {
    // --snip--

    Листинг 13-20: Изменяем тело
    Config::build
    так, чтобы использовать методы итератора
    Помните, что первое значение в возвращаемых данных env::args
    - это имя программы.
    Мы хотим проигнорировать его и перейти к следующему значению, поэтому сперва мы вызываем next и ничего не делаем с возвращаемым значением. Затем мы вызываем next
    , чтобы получить значение, которое мы хотим поместить в поле query в
    Config
    Если next возвращает
    Some
    , мы используем match для извлечения значения. Если возвращается
    None
    , это означает, что было задано недостаточно аргументов, и мы досрочно возвращаем значение
    Err
    . То же самое мы делаем для значения filename
    1   ...   31   32   33   34   35   36   37   38   ...   62


    написать администратору сайта