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

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

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

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

Filename: 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 получает во владение итератор и не использует доступ по индексу. Мы можем переместить знанчения String из итератора в Config.

Использование итератора возвращаемого функцией 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:

Filename: 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:

Filename: 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, мы используем его для в описании входных данных.

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

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

Filename: 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:

Filename: 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 демонстрирует изменения:

Filename: 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 выбирают второй вариант. Хотя, конечно, новичку может этот стиль показаться сложнее для понимания, но чем больше у Вас будет опыта работы с итераторами-адапторами, тем легче будет их использовать. Вместо циклов и промежуточных переменных лучше использовать итераторы-адаптеры.

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