Con este nuevo conocimiento sobre iteradores, podemos mejorar el proyecto I/O
en el Capítulo 12 usando iteradores para hacer que los lugares en el código
sean más claros y concisos. Veamos cómo los iterators pueden mejorar nuestra
implementación de la función Config::build y la función search.
En el Listado 12-6, agregamos código que tomó un slice de valores String y
creó una instancia del struct Config indexando en el slice y clonando
los valores, permitiendo que el struct Config posea esos valores. En el
Listado 13-17, hemos reproducido la implementación de la función Config::build
tal como estaba en el Listado 12-23:
Filename: src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pubstructConfig {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pubfnbuild(args: &[String]) -> Result<Config, &'staticstr> {
if args.len() < 3 {
returnErr("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,
})
}
}
pubfnrun(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(())
}
pubfnsearch<'a>(query: &str, contents: &'astr) -> Vec<&'astr> {
letmut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pubfnsearch_case_insensitive<'a>(
query: &str,
contents: &'astr,
) -> Vec<&'astr> {
let query = query.to_lowercase();
letmut 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]fncase_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]fncase_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)
);
}
}
Listing 13-17: Reproducción de la función Config::build
del Listing 12-23
En ese momento, dijimos que no nos preocupáramos por las llamadas ineficientes
a clone porque las eliminaríamos en el futuro. ¡Bueno, ese momento es ahora!
Necesitábamos clone aquí porque tenemos un slice con elementos String en el
parámetro args, pero la función build no posee args. Para retornar la
propiedad de una instancia de Config, tuvimos que clonar los valores de los
campos query y file_path de Config para que la instancia de Config
pueda poseer sus valores.
Con nuestro nuevo conocimiento sobre iteradores, podemos cambiar la función
build para tomar propiedad de un iterator como su argumento en lugar de
tomar prestado un slice. Usaremos la funcionalidad del iterator en lugar del
código que verifica la longitud del slice e indexa en ubicaciones específicas.
Esto aclarará lo que la función Config::build está haciendo porque el
iterator accederá a los valores.
Una vez que Config::build tome ownership del iterator y deje de usar
operaciones de indexación que toman borrowing, podemos mover los valores
String del iterator dentro de Config en lugar de llamar a clone y hacer
una nueva asignación.
Abre tu proyecto I/O en src/main.rs, el cual debería verse así:
Filename: src/main.rs
use std::env;
use std::process;
use minigrep::Config;
fnmain() {
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--ifletErr(e) = minigrep::run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
Primero cambiaremos el inicio de la función main que teníamos en el Listado
12-24 al código del Listado 13-18, el cual esta vez usa un iterator. Esto no
compilará hasta que actualicemos Config::build también.
Filename: src/main.rs
use std::env;
use std::process;
use minigrep::Config;
fnmain() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--ifletErr(e) = minigrep::run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
Listing 13-18: Pasando el valor de retorno de env::args
a Config::build
¡La función env::args retorna un iterator! En lugar de recolectar los valores
del iterator en un vector y luego pasar un slice a Config::build, ahora
estamos pasando ownership del iterator retornado por env::args directamente a
Config::build.
Luego, necesitamos actualizar la definición de Config::build. En el archivo
src/lib.rs de tu proyecto I/O, cambiemos la firma de Config::build para que
se vea como el Listado 13-19. Esto aún no compilará porque necesitamos
actualizar el cuerpo de la función.
Filename: src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pubstructConfig {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pubfnbuild(
mut args: implIterator<Item = String>,
) -> Result<Config, &'staticstr> {
// --snip--if args.len() < 3 {
returnErr("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,
})
}
}
pubfnrun(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(())
}
pubfnsearch<'a>(query: &str, contents: &'astr) -> Vec<&'astr> {
letmut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pubfnsearch_case_insensitive<'a>(
query: &str,
contents: &'astr,
) -> Vec<&'astr> {
let query = query.to_lowercase();
letmut 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]fncase_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]fncase_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)
);
}
}
Listing 13-19: Actualizando la firma de Config::build
para esperar un iterator
La documentación de la biblioteca estándar para la función env::args muestra
que el tipo del iterator que retorna es std::env::Args, y que ese tipo
implementa el trait Iterator y retorna valores String.
Hemos actualizado la firma de la función Config::build para que el parámetro
args tenga un tipo genérico con los trait bounds
impl Iterator<Item = String> en lugar de &[String]. Este uso de la sintaxis
impl Trait que discutimos en la sección “Traits como parámetros”
del Capítulo 10 significa que `args` puede ser cualquier tipo
que implemente el trait Iterator y retorne items String.
Debido a que estamos tomando ownership de args y estaremos mutando args
por iterarlo, podemos agregar la palabra clave mut en la especificación del
parámetro args para hacerlo mutable.
Luego, necesitamos actualizar el cuerpo de Config::build para usar los
métodos del trait Iterator en lugar de indexar en el slice. En el Listado
13-20 hemos actualizado el código del Listado 12-23 para usar el método next:
Filename: src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pubstructConfig {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pubfnbuild(
mut args: implIterator<Item = String>,
) -> Result<Config, &'staticstr> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => returnErr("Didn't get a query string"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => returnErr("Didn't get a file path"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
pubfnrun(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(())
}
pubfnsearch<'a>(query: &str, contents: &'astr) -> Vec<&'astr> {
letmut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pubfnsearch_case_insensitive<'a>(
query: &str,
contents: &'astr,
) -> Vec<&'astr> {
let query = query.to_lowercase();
letmut 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]fncase_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]fncase_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)
);
}
}
Listing 13-20: Cambiando el cuerpo de Config::build para
usar métodos de iterators
Recuerda que el primer valor en el valor de retorno de env::args es el nombre
del programa. Queremos ignorar eso y llegar al siguiente valor, así que
primero llamamos a next y no hacemos nada con el valor de retorno. Segundo,
llamamos a next para obtener el valor que queremos poner en el campo query
de Config. Si next retorna un Some, usamos un match para extraer el
valor. Si retorna None, significa que no se dieron suficientes argumentos y
retornamos temprano con un valor Err. Hacemos lo mismo para el valor
file_path.
También podemos aprovechar los iterators en la función search de nuestro
proyecto I/O, el cual se reproduce aquí en el Listado 13-21 como estaba en el
Listado 12-19:
Filename: src/lib.rs
use std::error::Error;
use std::fs;
pubstructConfig {
pub query: String,
pub file_path: String,
}
impl Config {
pubfnbuild(args: &[String]) -> Result<Config, &'staticstr> {
if args.len() < 3 {
returnErr("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pubfnrun(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
Ok(())
}
pubfnsearch<'a>(query: &str, contents: &'astr) -> Vec<&'astr> {
letmut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]mod tests {
use super::*;
#[test]fnone_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
Listing 13-21: La implementación de la función search
del Listing 12-19
Podemos escribir este código de una manera más concisa usando los métodos
adaptor del iterator. Hacerlo también nos permite evitar tener un vector
intermedio mutable results. El estilo de programación funcional prefiere
minimizar la cantidad de estado mutable para hacer el código más claro. Remover
el estado mutable podría permitir una mejora futura para hacer que la búsqueda
ocurra en paralelo, porque no tendríamos que manejar el acceso concurrente al
vector results. El Listado 13-22 muestra este cambio:
Filename: src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pubstructConfig {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pubfnbuild(
mut args: implIterator<Item = String>,
) -> Result<Config, &'staticstr> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => returnErr("Didn't get a query string"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => returnErr("Didn't get a file path"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
pubfnrun(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(())
}
pubfnsearch<'a>(query: &str, contents: &'astr) -> Vec<&'astr> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
pubfnsearch_case_insensitive<'a>(
query: &str,
contents: &'astr,
) -> Vec<&'astr> {
let query = query.to_lowercase();
letmut 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]fncase_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]fncase_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)
);
}
}
Listing 13-22: Utilizando método iterator adaptor en la
implementación de la función search
Recuerda que el propósito de la función search es retornar todas las líneas
en contents que contengan query. Similar al ejemplo de filter en el
Listado 13-16, este código usa el adaptador filter para mantener solo las
líneas que retornan true para line.contains(query). Luego recolectamos las
líneas que coinciden en otro vector con collect. ¡Mucho más simple! Siéntete
libre de hacer el mismo cambio para usar los métodos del iterator en la función
search_case_insensitive también.
La siguiente pregunta lógica es qué estilo deberías escoger en tu propio código
y por qué: la implementación original en el Listado 13-21 o la versión usando
iterators en el Listado 13-22. La mayoría de los programadores Rust prefieren
usar el estilo de iterators. Es un poco más difícil de entender al principio,
pero una vez que obtienes una idea de los varios adaptadores de iterators y lo
que hacen, los iterators pueden ser más fáciles de entender. En lugar de
manipular los varios bits de los loops y construir nuevos vectores, el código
se enfoca en el objetivo de alto nivel del loop. Esto abstrae un poco del
código común para que sea más fácil ver los conceptos que son únicos a este
código, como la condición de filtrado que cada elemento en el iterator debe
pasar.
¿Pero son las dos implementaciones realmente equivalentes? La suposición
intuitiva podría ser que el loop más bajo nivel será más rápido. Hablemos de
performance.