Mejora de nuestro Proyecto de E/S

RustRustBeginner
Practicar Ahora

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

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Bienvenido al proyecto Mejora de nuestra E/S. Esta práctica es parte del Libro de Rust. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, exploraremos cómo se pueden utilizar los iteradores para mejorar la implementación de la función Config::build y la función search en el proyecto de E/S del Capítulo 12.

Mejora de nuestro proyecto de E/S

Con estos nuevos conocimientos sobre los iteradores, podemos mejorar el proyecto de E/S del Capítulo 12 utilizando iteradores para hacer que los lugares del código sean más claros y concisos. Veamos cómo los iteradores pueden mejorar nuestra implementación de la función Config::build y la función search.

Eliminando una clonación usando un iterador

En la Lista 12-6, agregamos código que tomó una porción de valores de String y creó una instancia de la estructura Config mediante la indexación en la porción y la clonación de los valores, lo que permite que la estructura Config sea dueña de esos valores. En la Lista 13-17, reproducimos la implementación de la función Config::build tal como estaba en la Lista 12-23.

Nombre de archivo: 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,
        })
    }
}

Lista 13-17: Reproducción de la función Config::build de la Lista 12-23

En aquel momento, dijimos que no nos preocupáramos por las llamadas ineficientes a clone porque las eliminaremos en el futuro. Bueno, ¡ese momento es ahora!

Necesitamos clone aquí porque tenemos una porción con elementos de String en el parámetro args, pero la función build no es dueña de args. Para devolver la propiedad de una instancia de Config, tuvimos que clonar los valores de los campos query y filename de Config para que la instancia de Config pueda ser dueña de sus valores.

Con nuestro nuevo conocimiento sobre iteradores, podemos cambiar la función build para tomar la propiedad de un iterador como argumento en lugar de prestar una porción. Usaremos la funcionalidad del iterador en lugar del código que verifica la longitud de la porción e indexa en ubicaciones específicas. Esto clarificará lo que hace la función Config::build porque el iterador accederá a los valores.

Una vez que Config::build tome la propiedad del iterador y deje de usar operaciones de indexación que presten, podemos mover los valores de String desde el iterador a Config en lugar de llamar a clone y hacer una nueva asignación.

Usando directamente el iterador devuelto

Abra el archivo src/main.rs de su proyecto de E/S, que debería verse así:

Nombre de archivo: 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--
}

Primero, cambiaremos el comienzo de la función main que teníamos en la Lista 12-24 al código de la Lista 13-18, que esta vez utiliza un iterador. Esto no se compilará hasta que actualicemos Config::build también.

Nombre de archivo: src/main.rs

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

    --snip--
}

Lista 13-18: Pasando el valor devuelto de env::args a Config::build

La función env::args devuelve un iterador ¡Qué sorpresa! En lugar de recopilar los valores del iterador en un vector y luego pasar una porción a Config::build, ahora estamos pasando la propiedad del iterador devuelto por env::args a Config::build directamente.

A continuación, necesitamos actualizar la definición de Config::build. En el archivo src/lib.rs de su proyecto de E/S, cambiemos la firma de Config::build para que se vea como en la Lista 13-19. Esto todavía no se compilará, porque necesitamos actualizar el cuerpo de la función.

Nombre de archivo: src/lib.rs

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

Lista 13-19: Actualizando la firma de Config::build para esperar un iterador

La documentación de la biblioteca estándar para la función env::args muestra que el tipo del iterador que devuelve es std::env::Args, y ese tipo implementa el trato Iterator y devuelve valores de String.

Hemos actualizado la firma de la función Config::build para que el parámetro args tenga un tipo genérico con los límites de trato impl Iterator<Item = String> en lugar de &[String]. Este uso de la sintaxis impl Trait que discutimos en "Tratos como parámetros" significa que args puede ser cualquier tipo que implemente el tipo Iterator y devuelva elementos de String.

Debido a que estamos tomando la propiedad de args y vamos a mutar args iterando sobre él, podemos agregar la palabra clave mut en la especificación del parámetro args para que sea mutable.

Usando métodos del trato Iterator en lugar de indexación

A continuación, corregiremos el cuerpo de Config::build. Debido a que args implementa el trato Iterator, sabemos que podemos llamar al método next en él. La Lista 13-20 actualiza el código de la Lista 12-23 para usar el método next.

Nombre de archivo: 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,
        })
    }
}

Lista 13-20: Cambiando el cuerpo de Config::build para usar métodos de iterador

Recuerde que el primer valor en el valor devuelto de env::args es el nombre del programa. Queremos ignorarlo y pasar al siguiente valor, por lo que primero llamamos a next y no hacemos nada con el valor devuelto. Luego llamamos a next para obtener el valor que queremos poner en el campo query de Config. Si next devuelve Some, usamos una match para extraer el valor. Si devuelve None, significa que no se dieron suficientes argumentos y retornamos temprano con un valor Err. Hacemos lo mismo para el valor filename.

Haciendo el código más claro con adaptadores de iterador

También podemos aprovechar los iteradores en la función search de nuestro proyecto de E/S, que se reproduce aquí en la Lista 13-21 tal como estaba en la Lista 12-19.

Nombre de archivo: 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
}

Lista 13-21: La implementación de la función search de la Lista 12-19

Podemos escribir este código de manera más concisa utilizando métodos de adaptadores de iterador. Hacer esto 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. Eliminar el estado mutable podría permitir una mejora futura para que la búsqueda se realice en paralelo porque no tendríamos que manejar el acceso concurrente al vector results. La Lista 13-22 muestra este cambio.

Nombre de archivo: src/lib.rs

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

Lista 13-22: Usando métodos de adaptadores de iterador en la implementación de la función search

Recuerde que el propósito de la función search es devolver todas las líneas en contents que contienen la query. Similar al ejemplo de filter en la Lista 13-16, este código utiliza el adaptador filter para conservar solo las líneas para las cuales line.contains(query) devuelve true. Luego recolectamos las líneas coincidentes en otro vector con collect. ¡Mucho más simple! Siéntase libre de hacer el mismo cambio para usar métodos de iterador en la función search_case_insensitive también.

Elegir entre bucles e iteradores

La siguiente pregunta lógica es cuál estilo debe elegir en su propio código y por qué: la implementación original en la Lista 13-21 o la versión que utiliza iteradores en la Lista 13-22. La mayoría de los programadores de Rust prefieren usar el estilo de iterador. Es un poco más difícil dominar al principio, pero una vez que empieza a familiarizarse con los diversos adaptadores de iterador y lo que hacen, los iteradores pueden resultar más fáciles de entender. En lugar de preocuparse por los diversos aspectos del bucle y la creación de nuevos vectores, el código se centra en el objetivo general del bucle. Esto abstrae algunos de los códigos cotidianos, por lo que es más fácil ver los conceptos que son únicos de este código, como la condición de filtrado que debe pasar cada elemento del iterador.

Pero, ¿son las dos implementaciones realmente equivalentes? La suposición intuitiva podría ser que el bucle de bajo nivel será más rápido. Hablemos de rendimiento.

Resumen

¡Felicidades! Has completado el laboratorio de Mejora de nuestro Proyecto de E/S. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.