Улучшение нашего I/O-проекта
Используя новые знания об итераторах, мы можем улучшить I/O-проект из
Главы 12, применяя итераторы для упрощения и уточнения кода. Давайте посмотрим,
как итераторы могут улучшить нашу реализацию функции Config::build и функции
search.
Удаление clone с помощью итератора
В Листинге 12-6 мы добавили код, который принимал срез значений String и создавал
экземпляр структуры Config путём индексации в срезе и клонирования значений,
что позволяло структуре Config владеть этими значениями. В Листинге 13-17
мы воспроизвели реализацию функции Config::build такой, какой она была в
Листинге 12-23.
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
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,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
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
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Config::build из Листинга 12-23В то время мы сказали, что не стоит беспокоиться о неэффективных вызовах clone,
потому что мы удалим их в будущем. Что ж, это время настало!
Нам понадобился clone здесь, потому что параметр args представляет собой срез
с элементами String, но функция build не владеет args. Чтобы вернуть
владение экземпляром Config, нам пришлось клонировать значения полей query и
file_path структуры Config, чтобы экземпляр Config мог владеть своими
значениями.
Используя новые знания об итераторах, мы можем изменить функцию build так,
чтобы она принимала владение итератором в качестве аргумента вместо заимствования
среза. Мы будем использовать функциональность итератора вместо кода, который
проверяет длину среза и индексируется в определённые позиции. Это прояснит, что
делает функция Config::build, поскольку итератор будет обращаться к значениям.
Как только Config::build примет владение итератором и перестанет использовать
операции индексации, которые заимствуют, мы сможем перемещать значения String
из итератора в Config, вместо вызова clone и создания нового выделения памяти.
Прямое использование возвращаемого итератора
Откройте файл src/main.rs вашего I/O-проекта, который должен выглядеть так:
Имя файла: src/main.rs
use std::env;
use std::process;
use minigrep::Config;
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--
if let Err(e) = minigrep::run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
Сначала мы изменим начало функции main, которое было в Листинге 12-24, на код
из Листинга 13-18, который на этот раз использует итератор. Это не скомпилируется,
пока мы не обновим Config::build.
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = minigrep::run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
env::args в Config::buildФункция env::args возвращает итератор! Вместо сбора значений итератора в вектор
и последующей передачи среза в Config::build, теперь мы передаём владение
итератором, возвращаемым из env::args, непосредственно в Config::build.
Далее нам нужно обновить определение Config::build. В файле src/lib.rs вашего
I/O-проекта изменим сигнатуру Config::build так, как показано в Листинге 13-19.
Это всё ещё не скомпилируется, потому что нам нужно обновить тело функции.
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
// --snip--
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,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
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
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Config::build для ожидания итератораДокументация стандартной библиотеки для функции env::args показывает, что тип
возвращаемого итератора — std::env::Args, и этот тип реализует типаж Iterator
и возвращает значения String.
Мы обновили сигнатуру функции Config::build так, чтобы параметр args имел
обобщённый тип с ограничениями типажа impl Iterator<Item = String> вместо
&[String]. Это использование синтаксиса impl Trait, о котором мы говорили в
разделе “Типажи как параметры” Главы 10,
означает, что args может быть любым типом, который реализует типаж Iterator
и возвращает элементы String.
Поскольку мы принимаем владение args и будем изменять args путём итерации
по нему, мы можем добавить ключевое слово mut в спецификацию параметра args,
сделав его изменяемым.
Использование методов типажа Iterator вместо индексации
Далее исправим тело Config::build. Поскольку args реализует типаж Iterator,
мы знаем, что можем вызвать метод next на нём! Листинг 13-20 обновляет код из
Листинга 12-23 для использования метода next.
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
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,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
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
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Config::build для использования методов итератораПомните, что первое значение в возвращаемом значении env::args — это имя
программы. Мы хотим проигнорировать его и перейти к следующему значению, поэтому
сначала вызываем next и ничего не делаем с возвращаемым значением. Затем мы
вызываем next, чтобы получить значение, которое хотим поместить в поле query
структуры Config. Если next возвращает Some, мы используем match для
извлечения значения. Если он возвращает None, это означает, что не было передано
достаточно аргументов, и мы досрочно возвращаем значение Err. Мы делаем то же
самое для значения file_path.
Упрощение кода с помощью адаптеров итератора
Мы также можем воспользоваться итераторами в функции search нашего I/O-проекта,
которая воспроизведена здесь в Листинге 13-21 такой, какой она была в Листинге
12-19:
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
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();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
Ok(())
}
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
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
search из Листинга 12-19Мы можем записать этот код более кратко, используя методы-адаптеры итератора.
Это также позволяет нам избежать создания изменяемого промежуточного вектора
results. Функциональный стиль программирования предпочитает минимизировать
количество изменяемого состояния, чтобы сделать код более понятным. Удаление
изменяемого состояния может позволить в будущем улучшить поиск, сделав его
параллельным, поскольку нам не пришлось бы управлять одновременным доступом к
вектору results. Листинг 13-22 показывает это изменение:
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
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,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
searchНапомним, что цель функции search — вернуть все строки в contents, которые
содержат query. Подобно примеру с filter в Листинге 13-16, этот код использует
адаптер filter для сохранения только тех строк, для которых line.contains(query)
возвращает true. Затем мы собираем совпадающие строки в другой вектор с помощью
collect. Намного проще! Не стесняйтесь сделать то же изменение, используя методы
итератора, в функции search_case_insensitive.
Выбор между циклами или итераторами
Следующий логический вопрос — какой стиль вы должны выбрать в своём собственном коде и почему: оригинальную реализацию из Листинга 13-21 или версию с итераторами из Листинга 13-22. Большинство программистов Rust предпочитают использовать стиль с итераторами. Сначала с ним сложнее освоиться, но как только вы почувствуете различные адаптеры итераторов и поймёте, что они делают, итераторы могут стать легче для понимания. Вместо возни с различными частями цикла и построением новых векторов код фокусируется на высокой цели цикла. Это абстрагирует некоторый обычный код, чтобы было легче увидеть концепции, уникальные для этого кода, такие как условие фильтрации, которое должен пройти каждый элемент итератора.
Но действительно ли эти две реализации эквивалентны? Интуитивное предположение может быть в том, что цикл более низкого уровня будет быстрее. Давайте поговорим о производительности.