[Aprenda o Rust Project Combat juntos] Minigrep do projeto IO de linha de comando - módulo de otimização de refatoração e tratamento de erros


prefácio

Após as duas primeiras seções, nosso minigrep pode abrir com sucesso o arquivo de texto especificado e ler seu conteúdo.

Considerando que mais funções serão adicionadas ao nosso programa posteriormente, alguns problemas de procedimento aparecerão. Por exemplo, temos usado expect para enviar mensagens de erro, mas não podemos saber como o erro ocorreu. Existem muitos motivos para o erro, como o arquivo não existe. , ou falta de permissões e outros problemas, precisamos refatorar o projeto para otimizar os módulos do projeto e o tratamento de erros.


1. Finalidade da tarefa

Atualmente, pode haver quatro problemas no projeto, que afetarão os procedimentos de acompanhamento.

  1. Main agora apenas analisa os parâmetros e abre o arquivo, o que não é problema para uma função pequena, mas conforme as funções do software continuam a crescer, a função se torna mais complicada e difícil de depurar e modificar, e não é propícia ao Read, então é preciso separar múltiplas funções, cada função é responsável por uma função.
  2. query e filename são variáveis ​​de configuração no programa, e o conteúdo é usado para executar a lógica do programa. À medida que a função principal se torna complexa, haverá mais variáveis ​​e será difícil entender o significado de cada variável. Portanto, organize as variáveis ​​de configuração em uma estrutura que deixe claro o propósito da variável.
  3. Se o arquivo não abrir, ele sempre avisará Algo deu errado ao ler o arquivo, mas há muitas situações em que o arquivo não abre, como o arquivo não existe, não há permissão de arquivo e assim por diante. Portanto, devemos tentar fornecer informações detalhadas sobre o erro.
  4. Sabemos que o programa tem dois parâmetros, mas se outros não souberem passar dois parâmetros, o Rust reportará um erro, e nosso programa não é robusto o suficiente. Considere juntar o tratamento de erros e otimizar os prompts de erro.

Para fazer isso, precisamos refatorar nosso projeto.

2. Divisão do projeto

Grandes divisões de projetos na comunidade Rust têm um princípio comum,

  1. Divida o programa em main.rs e lib.rs e coloque a lógica do programa em lib.rs.
  2. Quando a lógica de análise da linha de comando é relativamente pequena, ela pode ser mantida em main.rs.
  3. Quando a análise da linha de comando começa a ficar complicada, ela também é extraída de main.rs para lib.rs.

Após as etapas acima, a função principal deve ser,

  • Chamar lógica de análise de linha de comando com valores de parâmetro
  • definir qualquer outra configuração
  • Chame a função run em lib.rs
  • Se executar retornar um erro, trate o erro

O objetivo acima é perceber que main e rs são especializados em processar a operação do programa, e lib.rs lida com a lógica das funções.

3. Refatore o projeto

Em seguida, seguiremos os princípios acima para dividir o projeto.

Extrair analisador de parâmetros

Crie uma nova função parse_config, que é usada especialmente para dividir os parâmetros obtidos e retornar a consulta e o nome do arquivo

fn parse_config(args: &[String]) -> (&str, &str) {
    
    
    let query = &args[1];
    let filename = &args[2];

    (query, filename)
}

Em seguida, usamos parse_config na função principal para obter os parâmetros exigidos pelo programa. As duas linhas de código comentadas abaixo são usadas para obter a consulta e o nome do arquivo antes. Comentamos e adicionamos o acima para obter os parâmetros let (query, filename) = parse_config(&args);.

fn main() {
    
    
    let args: Vec<String> = env::args().collect();
    println!("{:#?}", args);
    let (query, filename) = parse_config(&args);
    // let query = &args[1];
    // let filename = &args[2];
// 其他代码
}

Este método parece um pouco exagerado agora, mas não é, isso lhe dará grande comodidade ao localizar o problema.

Exportar configuração autônoma

Em seguida, continue melhorando o parse_config. Essa função retorna um tipo de tupla de dados. Para abstrair corretamente os parâmetros e trazer conveniência para a manutenção, extraímos o valor de retorno do parâmetro para torná-lo visível.

Crie uma nova estrutura Config, os campos dentro são nossos parâmetros,

struct Config {
    
    
    query: String,
    filename: String,
}

Em seguida, modifique a função parse_config,

fn parse_config(args: &[String]) -> Config {
    
    
    let query = args[1].clone();
    let filename = args[2].clone();

    Config {
    
     query, filename }
}

Aqui utilizamos o método clone para copiar os dados completos dos parâmetros, de forma que a instância do Config seja independente, tornando o código mais direto, pois não há necessidade de gerenciar o ciclo de vida da referência, mas consumirá mais tempo e memória do que armazenar a referência de dados de string. Nesse caso, vale a pena sacrificar uma pequena quantidade de desempenho em troca de simplicidade.

Em seguida, transformaremos o Config novamente. Quando usarmos a biblioteca padrão, usaremos new para criar instâncias. Para atender aos nossos hábitos de programação, escreveremos um construtor para ele. Primeiro, renomearemos a função parse_config para new e, em seguida, mova-o para impl.

impl Config {
    
    
    fn new(args: &[String]) -> Config {
    
    
        let query = args[1].clone();
        let filename = args[2].clone();

        Config {
    
     query, filename }
    }
}

Em seguida, modifique a chamada em main, para que possamos usar a configuração para adicionar pontos para chamar no futuro.

// 其他代码
	let config = Config::new(&args);
    // let query = &args[1];
    // let filename = &args[2];
// 其他代码

Otimize o tratamento de erros

Quando o número de parâmetros recebidos pelo programa não for igual a 2, nosso programa relatará um erro e a mensagem de erro será

index out of bounds: the len is 1 but the index is 1

Esse tipo de erro é um erro de programa incompreensível para o usuário. Por esse motivo, julgamos o número de parâmetros ao ler os parâmetros e otimizamos os erros aqui, para que as pessoas possam ver intuitivamente quais são os erros.

O número de parâmetros é julgado no novo de Config,

    // 其他代码
    fn new(args: &[String]) -> Config {
    
    
        if args.len() < 3 {
    
    
            panic!("参数个数不足");
        }
        // 其他代码

Se o pânico for retornado aqui, o programa sairá diretamente. Esse tipo de mensagem de erro é realmente óbvio, mas não é o melhor, porque também exibirá algumas informações de depuração, o que não é amigável o suficiente para os usuários, então consideramos usar Result , fazemos as seguintes alterações no impl

impl Config {
    
    
    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();

        Ok(Config {
    
     query, filename })
    }
}

Agora modifique a função principal

use std::process;
fn main() {
    
    
    let args: Vec<String> = env::args().collect();
    let config = Config::new(&args).unwrap_or_else(|err| {
    
    
        println!("参数拆分错误: {}", err);
        process::exit(1);
    });

    // 其他代码

Agora que testamos os erros, essa mensagem de erro é muito específica.

unwrap_or_else, que é definido em Result<T, E> da biblioteca padrão. Use unwrap_or_else para fazer algum tratamento de erro personalizado sem pânico. Quando Result é Ok, este método se comporta como unwrap: ele retorna o valor agrupado dentro de Ok. Porém, quando seu valor é Err, o método chama um encerramento, que é uma função anônima que definimos e passamos como parâmetro para unwrap_or_else.

Usamos o processo na biblioteca padrão para manipular a saída do programa, importar std::process e, em seguida, chamar process::exit, e o código de status recebido interromperá o programa imediatamente e o número passado para ele será usado como o código de status de saída.

extrair arquivo de leitura

Vamos extrair a parte da leitura do arquivo e virar função run, passar no config correspondente, e ler o arquivo

fn run(config: Config) {
    
    
    let contents = fs::read_to_string(config.filename)
        .expect("读取文件失败");

    println!("文件内容:\n{}", contents);
}

Para tornar o prompt de erro mais amigável, continue a modificar a execução para retornar Result.

Aqui fazemos três modificações óbvias. Primeiro, altere o tipo de retorno da função run para Result<(), Box<dyn Error>>. Anteriormente esta função retornava o tipo de unidade (), agora ainda permanece como o valor de retorno quando Ok.

Para os tipos de erro, é utilizado o objeto trait Box<dyn Error>, que será explicado mais adiante. Tudo o que você precisa saber Box<dyn Error>é que a função retornará um tipo que implementa o trait Error, mas você não precisa especificar o tipo do valor que será retornado.

A segunda alteração é remover a chamada para esperar e substituí-la por ?. Em vez de panic! em caso de erro, ? retorna o valor do erro da função e permite que o responsável pela chamada o trate.

A terceira modificação é que a função agora retorna um valor Ok em caso de sucesso.

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    
    
    let contents = fs::read_to_string(config.filename)?;
    println!("文件内容:\n{}", contents);
    Ok(())
}

Então trate esse erro no main, pois aqui só se preocupa com o erro, então use if let para manipulá-lo.

fn main() {
    
    
    // 其他代码
    if let Err(e) = run(config) {
    
    
        println!("程序运行出错: {}", e);
        process::exit(1);
    }
}

Divida o código em caixas

Crie um novo arquivo lib.rs, mova Config e execute em main para src/lib.rs,

注意As funções e estruturas em lib.rs devem ser modificadas com a palavra-chave pub

use std::fs;
use std::error::Error;

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    
    
    let contents = fs::read_to_string(config.filename)?;
    println!("文件内容:\n{}", contents);
    Ok(())
}
pub struct Config {
    
    
    query: String,
    filename: String,
}

impl Config {
    
    
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
    
    
        if args.len() < 3 {
    
    
            return Err("参数个数不足");
        }
        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config {
    
     query, filename })
    }
}

Em seguida, modifique main.rs, principalmente adicionando use minigrep::Config, para que, ao usá-lo, você possa usar minigrep para chamar run em lib.rs e também chamar Config diretamente nele.

use std::{
    
    env, process};
use minigrep::Config;
fn main() {
    
    
    let args: Vec<String> = env::args().collect();
    println!("{:#?}", args);
    let config = Config::new(&args).unwrap_or_else(|err| {
    
    
        println!("参数拆分错误: {}", err);
        process::exit(1);
    });
    if let Err(e) = minigrep::run(config) {
    
    
        println!("程序运行出错: {}", e);
        process::exit(1);
    }
}

Resumir

Através desta seção, você aprendeu como dividir projetos, como produzir erros de forma elegante e dividir projetos em caixas. Embora a carga de trabalho nesta seção seja pesada, os benefícios para o desenvolvimento subsequente também são muito grandes, estabelecendo as bases para o sucesso futuro.

código completo

principal.rs

use std::{
    
    env, process};
use minigrep::Config;
fn main() {
    
    
    let args: Vec<String> = env::args().collect();
    println!("{:#?}", args);
    let config = Config::new(&args).unwrap_or_else(|err| {
    
    
        println!("参数拆分错误: {}", err);
        process::exit(1);
    });
    if let Err(e) = minigrep::run(config) {
    
    
        println!("程序运行出错: {}", e);
        process::exit(1);
    }
}

lib.rs

use std::fs;
use std::error::Error;

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    
    
    let contents = fs::read_to_string(config.filename)?;
    println!("文件内容:\n{}", contents);
    Ok(())
}
pub struct Config {
    
    
    query: String,
    filename: String,
}

impl Config {
    
    
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
    
    
        if args.len() < 3 {
    
    
            return Err("参数个数不足");
        }
        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config {
    
     query, filename })
    }
}

Acho que você gosta

Origin blog.csdn.net/weixin_47754149/article/details/125730175
Recomendado
Clasificación