Преобразование данных с помощью итераторов и замыканий

RustBeginner
Практиковаться сейчас

Введение

Явный цикл for часто оказывается самым понятным способом обработки коллекции. В Rust также есть итераторы — объекты, которые выдают по одному элементу за раз и позволяют объединять операции в понятный конвейер обработки данных.

Вы сохраните рабочий цикл как эталонный вариант, получите тот же результат с помощью iter и sum, создадите небольшое замыкание, преобразуете показания с помощью map, исключите недопустимые показания с помощью filter, а затем объедините все операции. Все изменения выполняются в подготовленном проекте Cargo, поэтому вы сможете сосредоточиться на новых возможностях Rust.

Получение той же суммы с помощью итератора

На этом этапе вы сравните подготовленный цикл for с итератором, который вычисляет ту же сумму.

Проект находится в /home/labex/project/reading-pipeline, а его программа — в src/main.rs. Подготовленная конструкция vec![12, 18, -1, 24, 30] использует макрос vec! как сокращённый способ создать вектор с указанными значениями. По смыслу это эквивалентно созданию пустого вектора и добавлению каждого значения с помощью push, как в предыдущей лабораторной работе по Vec. Существующий цикл заимствует этот вектор с помощью &readings, перебирает элементы типа &i32 и использует *reading, чтобы получить целое число, на которое ссылается эта ссылка.

Перейдите в каталог проекта и запустите исходный вариант:

cd /home/labex/project/reading-pipeline
cargo run --quiet
Loop total: 83

Сумма включает все показания, в том числе -1. Теперь откройте исходный файл:

nano src/main.rs

Замените комментарий Step 1 следующим кодом:

    let iterator_total: i32 = readings.iter().sum();
    println!("Iterator total: {iterator_total}");

readings.iter() создаёт итератор, который заимствует каждый элемент вектора. Он не удаляет элементы и не потребляет сам вектор. Метод sum() последовательно складывает полученные элементы; стандартная библиотека Rust умеет складывать целые числа, полученные через ссылки. Аннотация : i32 указывает тип результата, который должен построить sum.

Сохраните файл с помощью Ctrl+O, нажмите Enter, а затем выйдите с помощью Ctrl+X. Перед запуском проверьте код:

cargo check
cargo run --quiet
Loop total: 83
Iterator total: 83

Одинаковые строки показывают, что цикл и итератор описывают одно и то же вычисление. Вектор остаётся доступным для следующих этапов, потому что iter() только заимствует его.

Преобразование показаний с помощью замыкания и map

На этом этапе вы определите замыкание и примените его к каждому показанию с помощью map.

Замыкание — это небольшая безымянная функция, которую можно сохранить в переменной или напрямую передать адаптеру итератора. В форме |parameter: Type| expression параметры указываются между вертикальными чертами, а результатом функции становится значение выражения. Здесь замыкание для калибровки будет добавлять смещение, равное двум.

Откройте исходный файл:

nano src/main.rs

Замените комментарий Step 2 следующим кодом:

    let add_offset = |reading: i32| reading + 2;
    let adjusted: Vec<i32> = readings.iter().map(|reading| add_offset(*reading)).collect();
    println!("Adjusted all: {adjusted:?}");

map вызывает своё замыкание один раз для каждого элемента итератора и выдаёт возвращаемые значения. Типы проходят такой путь: &i32 из iter() → скопированное значение i32 из *reading → скорректированное значение i32 из add_offset → элементы Vec<i32>, созданные с помощью collect(). Форматировщик :? выводит вектор в удобном для чтения отладочном виде.

Сохраните файл и выйдите из редактора, затем проверьте и запустите программу:

cargo check
cargo run --quiet

Новая последняя строка должна выглядеть так:

Adjusted all: [14, 20, 1, 26, 32]

Даже недопустимое значение -1 было преобразовано в 1: map изменяет элементы, но не решает, какие элементы следует оставить.

Сохранение только допустимых показаний с помощью filter

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

Адаптер filter у итератора вызывает замыкание-предикат для каждого кандидата. Предикат возвращает true, чтобы оставить элемент, или false, чтобы исключить его. Откройте исходный файл:

nano src/main.rs

Замените комментарий Step 3 следующим кодом:

    let valid: Vec<i32> = readings
        .iter()
        .copied()
        .filter(|reading| *reading >= 0)
        .collect();
    println!("Valid only: {valid:?}");

Цепочку методов можно продолжать на отступах после точки. iter() заимствует вектор, а copied() превращает каждый заимствованный элемент &i32 в i32. Это не требует существенных затрат, потому что целые числа реализуют трейт Copy. Затем предикат filter получает временную ссылку на каждый кандидат типа i32, чтобы проверить его, не потребляя значение. Поэтому в сравнении используется *reading. Типы проходят такой путь: &i32 → скопированное значение i32 → сохранённое значение i32 → Vec<i32>.

Сохраните файл и выйдите из редактора, затем запустите программу:

cargo run --quiet

Последняя строка должна выглядеть так:

Valid only: [12, 18, 24, 30]

Отрицательное значение отсутствует, а сохранённые значения остались в исходном порядке.

Создание полного конвейера обработки

На этом этапе вы объедините фильтрацию и преобразование, соберёте обработанные показания и вычислите их сумму.

Адаптеры итераторов работают лениво: filter и map описывают операции, но элементы проходят через них только тогда, когда потребляющая операция запрашивает результат. collect и sum являются потребляющими операциями. Они потребляют конвейер итераторов, но не исходный заимствованный вектор.

Откройте исходный файл:

nano src/main.rs

Замените комментарий Step 4 следующим кодом:

    let valid_adjusted: Vec<i32> = readings
        .iter()
        .copied()
        .filter(|reading| *reading >= 0)
        .map(add_offset)
        .collect();
    let valid_total: i32 = valid_adjusted.iter().sum();
    println!("Valid adjusted: {valid_adjusted:?}");
    println!("Valid adjusted total: {valid_total}");

Сначала конвейер исключает -1, затем map(add_offset) передаёт каждое оставшееся значение i32 замыканию, которое вы определили ранее. Имя замыкания можно передать адаптеру напрямую, если типы его параметров и возвращаемого значения подходят этому адаптеру. Второй итератор заимствует собранный вектор и суммирует его элементы.

Сохраните файл и выйдите из редактора, затем проверьте и запустите программу:

cargo check
cargo run --quiet

Две последние строки должны выглядеть так:

Valid adjusted: [14, 20, 26, 32]
Valid adjusted total: 92

Порядок операций имеет значение: фильтрация перед преобразованием не позволяет недопустимому показанию превратиться в значение, которое выглядит как допустительное положительное число. Исходный вектор readings только заимствовался, поэтому его по-прежнему можно использовать после этого.

Итоги

Вы сравнили явный цикл с итератором, создали и повторно использовали замыкание, преобразовали значения с помощью map, выбрали значения с помощью filter, собрали новый вектор и вычислили сумму с помощью sum. Кроме того, вы сохранили исходный вектор, начиная каждый конвейер с заимствованного итератора.