Melhorando Nosso Projeto de I/O

Beginner

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

Introdução

Bem-vindo ao Projeto Melhorando Nosso I/O. Este laboratório faz parte do Livro de Rust. Você pode praticar suas habilidades em Rust no LabEx.

Neste laboratório, exploraremos como iteradores podem ser usados para melhorar a implementação da função Config::build e da função search no projeto I/O do Capítulo 12.

Melhorando Nosso Projeto I/O

Com este novo conhecimento sobre iteradores, podemos melhorar o projeto I/O no Capítulo 12, usando iteradores para tornar os locais no código mais claros e concisos. Vamos analisar como os iteradores podem melhorar nossa implementação da função Config::build e da função search.

Removendo um clone usando um iterador

Na Listagem 12-6, adicionamos código que pegava uma fatia de valores String e criava uma instância da struct Config indexando na fatia e clonando os valores, permitindo que a struct Config fosse proprietária desses valores. Na Listagem 13-17, reproduzimos a implementação da função Config::build como estava na Listagem 12-23.

Nome do arquivo: 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,
        })
    }
}

Listagem 13-17: Reprodução da função Config::build da Listagem 12-23

Na época, dissemos para não se preocupar com as chamadas clone ineficientes porque as removeríamos no futuro. Bem, essa hora é agora!

Precisávamos de clone aqui porque temos uma fatia com elementos String no parâmetro args, mas a função build não é proprietária de args. Para retornar a propriedade de uma instância Config, tivemos que clonar os valores dos campos query e filename de Config para que a instância Config pudesse ser proprietária de seus valores.

Com nosso novo conhecimento sobre iteradores, podemos alterar a função build para assumir a propriedade de um iterador como seu argumento, em vez de emprestar uma fatia. Usaremos a funcionalidade do iterador em vez do código que verifica o comprimento da fatia e indexa em locais específicos. Isso esclarecerá o que a função Config::build está fazendo, porque o iterador acessará os valores.

Assim que Config::build assumir a propriedade do iterador e parar de usar operações de indexação que emprestam, podemos mover os valores String do iterador para Config em vez de chamar clone e fazer uma nova alocação.

Usando o Iterador Retornado Diretamente

Abra o arquivo src/main.rs do seu projeto I/O, que deve ter esta aparência:

Nome do arquivo: 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--
}

Primeiro, vamos alterar o início da função main que tínhamos na Listagem 12-24 para o código na Listagem 13-18, que desta vez usa um iterador. Isso não compilará até que também atualizemos Config::build.

Nome do arquivo: src/main.rs

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

    --snip--
}

Listagem 13-18: Passando o valor de retorno de env::args para Config::build

A função env::args retorna um iterador! Em vez de coletar os valores do iterador em um vetor e, em seguida, passar uma fatia para Config::build, agora estamos passando a propriedade do iterador retornado de env::args para Config::build diretamente.

Em seguida, precisamos atualizar a definição de Config::build. No arquivo src/lib.rs do seu projeto I/O, vamos alterar a assinatura de Config::build para se parecer com a Listagem 13-19. Isso ainda não compilará, porque precisamos atualizar o corpo da função.

Nome do arquivo: src/lib.rs

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

Listagem 13-19: Atualizando a assinatura de Config::build para esperar um iterador

A documentação da biblioteca padrão para a função env::args mostra que o tipo do iterador que ela retorna é std::env::Args, e esse tipo implementa o trait Iterator e retorna valores String.

Atualizamos a assinatura da função Config::build para que o parâmetro args tenha um tipo genérico com as restrições de trait impl Iterator<Item = String> em vez de &[String]. Este uso da sintaxe impl Trait que discutimos em "Traits como Parâmetros" significa que args pode ser qualquer tipo que implemente o tipo Iterator e retorne itens String.

Como estamos assumindo a propriedade de args e vamos mutar args iterando sobre ele, podemos adicionar a palavra-chave mut na especificação do parâmetro args para torná-lo mutável.

Usando Métodos de Trait de Iterador em Vez de Indexação

Em seguida, corrigiremos o corpo de Config::build. Como args implementa o trait Iterator, sabemos que podemos chamar o método next nele! A Listagem 13-20 atualiza o código da Listagem 12-23 para usar o método next.

Nome do arquivo: 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,
        })
    }
}

Listagem 13-20: Alterando o corpo de Config::build para usar métodos de iterador

Lembre-se de que o primeiro valor no valor de retorno de env::args é o nome do programa. Queremos ignorá-lo e ir para o próximo valor, então primeiro chamamos next e não fazemos nada com o valor de retorno. Em seguida, chamamos next para obter o valor que queremos colocar no campo query de Config. Se next retornar Some, usamos um match para extrair o valor. Se retornar None, significa que não foram fornecidos argumentos suficientes e retornamos antecipadamente com um valor Err. Fazemos a mesma coisa para o valor filename.

Tornando o Código Mais Claro com Adaptadores de Iterador

Também podemos tirar proveito de iteradores na função search em nosso projeto I/O, que é reproduzida aqui na Listagem 13-21 como estava na Listagem 12-19.

Nome do arquivo: 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
}

Listagem 13-21: A implementação da função search da Listagem 12-19

Podemos escrever este código de uma forma mais concisa usando métodos de adaptador de iterador. Fazer isso também nos permite evitar ter um vetor results mutável intermediário. O estilo de programação funcional prefere minimizar a quantidade de estado mutável para tornar o código mais claro. Remover o estado mutável pode possibilitar uma aprimoramento futuro para fazer a busca acontecer em paralelo, porque não precisaríamos gerenciar o acesso concorrente ao vetor results. A Listagem 13-22 mostra essa alteração.

Nome do arquivo: src/lib.rs

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

Listagem 13-22: Usando métodos de adaptador de iterador na implementação da função search

Lembre-se de que o objetivo da função search é retornar todas as linhas em contents que contêm o query. Semelhante ao exemplo filter na Listagem 13-16, este código usa o adaptador filter para manter apenas as linhas para as quais line.contains(query) retorna true. Em seguida, coletamos as linhas correspondentes em outro vetor com collect. Muito mais simples! Sinta-se à vontade para fazer a mesma alteração para usar métodos de iterador na função search_case_insensitive também.

Escolhendo entre Loops e Iteradores

A próxima pergunta lógica é qual estilo você deve escolher em seu próprio código e por quê: a implementação original na Listagem 13-21 ou a versão usando iteradores na Listagem 13-22. A maioria dos programadores Rust prefere usar o estilo de iterador. É um pouco mais difícil de entender no início, mas assim que você se familiarizar com os vários adaptadores de iterador e o que eles fazem, os iteradores podem ser mais fáceis de entender. Em vez de mexer com as várias partes do loop e construir novos vetores, o código se concentra no objetivo de alto nível do loop. Isso abstrai parte do código comum, tornando mais fácil ver os conceitos que são exclusivos deste código, como a condição de filtragem que cada elemento no iterador deve passar.

Mas as duas implementações são realmente equivalentes? A suposição intuitiva pode ser que o loop de nível inferior será mais rápido. Vamos falar sobre desempenho.

Resumo

Parabéns! Você concluiu o laboratório Melhorando Nosso Projeto de I/O. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.