Улучшение нашего проекта работы с системой ввода вывода (I/O Project)

Теперь, когда мы изучили возможности замыканий и итераторов мы можем улучшить код проекта, который мы реализовывали в Главе 12. Мы сделаем код более кратким и ясным. Мы улучшим код функций Config::new и search.

Замена функции clone с помощью итератора

В коде (12-6) мы, получив срез строк и создав экземпляр структуры Config, клонировали значения, чтобы передать их в поля структуры. Продемонстрируем этот код:

Файл: src/lib.rs

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

        Ok(Config { query, filename, case_sensitive })
    }
}

Код 13-24: реализация функции Config::new из Главы 12

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

Причина использования метода clone является необходимость получить возможность полям экземпляра структуры владеть данными (в данном случае строковыми значениями).

Используя полученные знания об итераторах, мы можем изменить функцию new, таким образом, чтобы она стала владельцем итератора в качестве аргумента вместо заимствования среза. Мы будем использовать функциональность итератора вместо того кода, в котором мы проверяем длину среза и используем индекс в некоторых местах (для доступа к элементам среза). Это упростит понимание того, что делает функция Config::new, так как итератор позаботится о доступе к значениям.

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

Использование итератора возвращаемого функцией env::args

В файле src/main.rs проекта Главы 12 изменим содержание функции main:

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    // ...snip...
}

На код примера 13-25:

Файл: src/main.rs

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

    // ...snip...
}

Код 13-25: удаление переменной args и направление результата вызова функции env::args непосредственно в функцию Config::new

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

Далее, нам необходимо внести изменения в функцию Config::new в файле src/lib.rs:

Файл: src/lib.rs

impl Config {
    pub fn new(args: std::env::Args) -> Result<Config, &'static str> {
        // ...snip...

Код 13-26: изменение описания функции Config::new

Согласно документации для стандартной библиотеки, функция env::args возвращает итератор std::env::Args, мы обновили описание (сигнатуру) функции Config::new так, чтобы параметр args имел тип std::env::Args вместо &[String].

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

Далее мы вносим изменения в содержание функции Config::new. Т.к. std::env::Args является итератором, т.е. реализует типаж Iterator, то он может использовать все методы данного типажа:

Файл: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
# use std::env;
#
# struct Config {
#     query: String,
#     filename: String,
#     case_sensitive: bool,
# }
#
impl Config {
    pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
    	args.next();

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

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

        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

        Ok(Config {
            query, filename, case_sensitive
        })
    }
}
#}

Код 13-27: Новое содержание функции Config::new

Обратите внимание, что первым элементом аргументов является имя программы, поэтому, в данном случае, оно должно быть проигнорировано с помощью функции next. Следующий вызов функции next вернет значение query, а последующий filename.

Упрощаем код с помощью итераторов-адаптеров (Iterator Adaptors)

Следующая функция, которую мы можем улучшиться в нашем проекте - это search, приведённая тут, в таком же виде как это было в конце Главы 12:

Файл: 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-28: реализация функции search в Главе 12

Мы можем сократить код этой функции благодаря использованию итераторов-адаптеров. Также дополнительным плюсом этого решения станет удаление промежуточной переменной results. Функциональный стиль программирования рекомендует минимизацию количества изменяемых состояний. Это делает код устойчивым от ошибок. Удаление возможности изменять вектор даст нам в будущем возможность реализовать параллельный поиск. Код с изменениями 13-29 демонстрирует изменения:

Файл: src/lib.rs

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

Код 13-29: использование методов итератора-адаптера

Напомним, что целью функции search является возвращение всех строк из текста contents, в которой содержится query. Функция filter решает задачу поиска, а collect формирование вектора. Код стал проще, не правда ли?! Пожалуйста, самостоятельно реализуйте подобное улучшение в функции search_case_insensitive.

Существует следующий логический вопрос. Какой стиль выбрать в своем коде: исходная реализация в листинге 13-28 или версия с использованием итераторов в листинге 13-29. Большинство программистов Rust выбирают второй вариант. Хотя, конечно, новичку может этот стиль показаться сложнее для понимания, но чем больше у Вас будет опыта работы с итераторами-адаптерами, тем легче будет их использовать. Вместо циклов и промежуточных переменных лучше использовать итераторы-адаптеры.

Но действительно ли эти конструкции равнозначны. Это вызывает сомнение. Рассуждения по поводу производительности мы продолжим в следующей секции этой главы.