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

Beginner

This tutorial is from open-source community. Access the source code

Введение

Добро пожаловать в Проект по улучшению ввода-вывода. Этот лаба является частью Книги о Rust. Вы можете практиковать свои навыки Rust в LabEx.

В этом лабе мы исследуем, как итераторы можно использовать для улучшения реализации функции Config::build и функции search в проекте по вводу-выводу из главы 12.

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

С этими новыми знаниями о итераторах мы можем улучшить проект по вводу-выводу из главы 12, используя итераторы, чтобы сделать места в коде более понятными и компактными. Посмотрим, как итераторы могут улучшить нашу реализацию функции Config::build и функции search.

Удаление клонирования с использованием итератора

В листинге 12-6 мы добавили код, который получал срез значений типа String и создавал экземпляр структуры Config, обращаясь по индексу к срезу и клонируя значения, чтобы структура Config могла владеть этими значениями. В листинге 13-17 мы再现了 Config::build функции, как она была в листинге 12-23.

Имя файла: src/lib.rs

impl Config {
    pub fn build(
        args: &[String]
    ) -> Result<Config, &'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,
        })
    }
}

Листинг 13-17:再现 Config::build функции из листинга 12-23

Тогда мы говорили, не беспокоиться о неэффективных вызовах clone, потому что мы уберём их в будущем. Ну, вот и настало время!

Мы тут нуждавались в clone, потому что в параметре args у нас есть срез с элементами типа String, но функция build не владеет args. Чтобы вернуть владение экземпляром Config, нам пришлось клонировать значения из полей query и filename структуры Config, чтобы экземпляр Config мог владеть своими значениями.

С нашими новыми знаниями о итераторах мы можем изменить функцию build, чтобы она принимала в качестве аргумента владение итератором вместо взятия поимки среза. Мы будем использовать функциональность итератора вместо кода, который проверяет длину среза и обращается по индексу к конкретным местам. Это уточнит, что делает функция Config::build, потому что итератор будет получать доступ к значениям.

Как только Config::build получит владение итератором и перестанет использовать операции индексирования, которые берут поимку, мы сможем переместить значения типа String из итератора в Config, вместо вызова clone и создания нового выделения памяти.

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

Откройте файл src/main.rs вашего проекта по вводу-выводу, который должен выглядеть так:

Имя файла: src/main.rs

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--
}

Сначала мы изменим начало функции main, которое мы имели в листинге 12-24, на код из листинга 13-18, который на этот раз использует итератор. Это не скомпилируется, пока мы не обновим Config::build также.

Имя файла: src/main.rs

fn main() {
    let config =
        Config::build(env::args()).unwrap_or_else(|err| {
            eprintln!("Problem parsing arguments: {err}");
            process::exit(1);
        });

    --snip--
}

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

Функция env::args возвращает итератор! Вместо сбора значений итератора в вектор и последующей передачи среза в Config::build, теперь мы напрямую передаём владение итератором, возвращаемым из env::args, в Config::build.

Далее, нам нужно обновить определение Config::build. В файле src/lib.rs вашего проекта по вводу-выводу давайте изменим сигнатуру Config::build так, чтобы она выглядела как в листинге 13-19. Это по-прежнему не скомпилируется, потому что нам нужно обновить тело функции.

Имя файла: src/lib.rs

impl Config {
    pub fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        --snip--

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

Документация по стандартной библиотеке для функции env::args показывает, что тип итератора, который она возвращает, это std::env::Args, и этот тип реализует трейт Iterator и возвращает значения типа String.

Мы обновили сигнатуру функции Config::build, так что параметр args имеет обобщённый тип с ограничениями трейтов impl Iterator<Item = String> вместо &[String]. Это использование синтаксиса impl Trait, о котором мы говорили в разделе "Трейты в качестве параметров", означает, что 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<Item = String>,
    ) -> Result<Config, &'static str> {
        args.next();

        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a query string"),
        };

        let file_path = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file path"),
        };

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

Листинг 13-20: Изменение тела функции Config::build для использования методов итератора

Помните, что первое значение в возвращаемом значении env::args — это имя программы. Мы хотим игнорировать это и перейти к следующему значению, поэтому сначала мы вызываем next и ничего не делаем с возвращаемым значением. Затем мы вызываем next, чтобы получить значение, которое мы хотим поместить в поле query структуры Config. Если next возвращает Some, мы используем match для извлечения значения. Если оно возвращает None, это означает, что передано недостаточно аргументов, и мы выходим ранним, возвращая значение Err. Мы делаем то же самое для значения filename.

Улучшение ясности кода с использованием адаптеров итераторов

Мы также можем воспользоваться итераторами в функции search нашего проекта по вводу-выводу, которая представлена здесь в листинге 13-21, как она была в листинге 12-19.

Имя файла: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

Листинг 13-21: Реализация функции search из листинга 12-19

Мы можем написать этот код более компактно, используя методы-адаптеры итераторов. Это также позволяет избежать использования изменяемого промежуточного вектора results. Функциональное программирование предпочитает минимизировать количество изменяемого состояния, чтобы сделать код более понятным. Удаление изменяемого состояния может привести к будущему улучшению, которое позволит выполнять поиск параллельно, так как нам не придётся управлять одновременным доступом к вектору results. Листинг 13-22 показывает это изменение.

Имя файла: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    contents
     .lines()
     .filter(|line| line.contains(query))
     .collect()
}

Листинг 13-22: Использование методов-адаптеров итераторов в реализации функции search

Помните, что цель функции search — вернуть все строки в contents, которые содержат query. Подобно примеру с filter в листинге 13-16, этот код использует адаптер filter, чтобы оставить только те строки, для которых line.contains(query) возвращает true. Затем мы собираем соответствующие строки в другой вектор с помощью collect. Насколько проще! Не стесняйтесь сделать то же самое для функции search_case_insensitive, чтобы использовать методы итераторов.

Выбор между циклами и итераторами

Следующий логичный вопрос — какой стиль выбрать в своем коде и почему: исходная реализация из листинга 13-21 или версия с использованием итераторов из листинга 13-22. Большинство программистов на Rust предпочитают использовать стиль с итераторами. Сначала его немного сложнее освоить, но一旦 вы привыкнете к различным адаптерам итераторов и понимаете, что они делают, итераторы могут быть легче понять. Вместо того чтобы мучаться с различными аспектами циклов и созданием новых векторов, код сосредотачивается на высокоуровневом целевом назначении цикла. Это позволяет убрать часть обыденного кода, так что легче увидеть концепции, уникальные для этого кода, такие как условие фильтрации, которое должен проходить каждый элемент итератора.

Но являются ли эти две реализации действительно эквивалентными? Интуитивное предположение может быть, что более низкоуровневый цикл будет работать быстрее. Давайте поговорим о производительности.

Резюме

Поздравляем! Вы завершили лабораторную работу "Улучшение нашего проекта по вводу-выводу". Вы можете практиковаться в других лабораторных работах в LabEx, чтобы улучшить свои навыки.