Язык программирования Rust
Скачать 7.02 Mb.
|
Обработка группы элементов с помощью итераторов Шаблон итератора позволяет выполнять некоторые задачи над последовательностью элементов. Итератор отвечает за логику итерации по каждому элементу и определяет, когда последовательность завершилась. Когда вы используете итераторы, вам не нужно переопределять эту логику самостоятельно. В 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 ) -> Vec } #[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 > { 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 >, ) -> Result > { // --snip-- Листинг 13-20: Изменяем тело Config::build так, чтобы использовать методы итератора Помните, что первое значение в возвращаемых данных env::args - это имя программы. Мы хотим проигнорировать его и перейти к следующему значению, поэтому сперва мы вызываем next и ничего не делаем с возвращаемым значением. Затем мы вызываем next , чтобы получить значение, которое мы хотим поместить в поле query в Config Если next возвращает Some , мы используем match для извлечения значения. Если возвращается None , это означает, что было задано недостаточно аргументов, и мы досрочно возвращаем значение Err . То же самое мы делаем для значения filename |