zoukankan      html  css  js  c++  java
  • [易学易懂系列|rustlang语言|零基础|快速入门|(24)|实战2:命令行工具minigrep(1)]

    [易学易懂系列|rustlang语言|零基础|快速入门|(24)|实战2:命令行工具minigrep(1)]

    项目实战

    实战2:命令行工具minigrep

    有了昨天的基础,我们今天来开始另一个稍微有点复杂的项目。

    简单来说,就是开发一个我们自己的grep (globally search a regular expression and print)

    首先用命令生成一个工程:

    cargo new minigrep
    

    然后在工程目录minigrep下新建一个文件:poem.txt,文件的内容如下 :

    I'm nobody! Who are you?
    Are you nobody, too?
    Then there's a pair of us - don't tell!
    They'd banish us, you know.
    
    How dreary to be somebody!
    How public, like a frog
    To tell your name the livelong day
    To an admiring bog!
    

    在工程目录下的src/main.rs文件中,填入以下代码:

    use std::env;
    use std::fs;
    fn main() {
        let args: Vec<String> = env::args().collect();//从命令行环境得到用户输入的参数
    
        let query = &args[1];//参数1
        let filename = &args[2];//参数2
    
        println!("Searching for {}", query);//打印参数1
        println!("In file {}", filename);//打印参数1
    
        let contents = fs::read_to_string(filename).expect("Something went wrong reading the file");//以参数2为路径,读取文件内容
    
        println!("With text:
    {}", contents);//打印内容 
    }
    
    

    我们现在用命令:

    cargo run the poem.txt
    

    结果将打印如下 信息:

    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
         Running `targetdebugminigrep.exe the poem.txt`
    Searching for the
    In file poem.txt
    With text:
    I'm nobody! Who are you?
    Are you nobody, too?
    Then there's a pair of us - don't tell!
    They'd banish us, you know.
    
    How dreary to be somebody!
    How public, like a frog
    To tell your name the livelong day
    To an admiring bog!
    

    成功了!

    我们成功地从文件poem.txt中读取了相关内容信息。

    好现在我们来重构一下代码!

    为什么要重构代码?

    因为我们的很多逻辑都写在一个main函数上,这是一个不好的现象,如果功能越多,代码也越多,最后可以出现一个超长的main函数。可读性和可维护性都很差!

    所以从一开始,我们就要考虑代码的可读性和可维护性,这是最佳实践!

    好,我们开始重构吧!

    根据单一职责设计原则,一个方法只负责一个职责。

    那我们就应该把main方法里的负责处理参数读取和参数搜索的逻辑,把它们分别抽离开来,放在单独的方法中。

    我们先把处理参数读取的逻辑抽出来,如下:

    use std::env;
    use std::fs;
    fn main() {
        let args: Vec<String> = env::args().collect();
    
        let (query, filename) = parseConfig(&args);//抽离参数读取与解析的逻辑
    
        let contents = fs::read_to_string(filename).expect("Something went wrong reading the file");
    
        println!("With text:
    {}", contents);
    }
    
    //参数读取与解析的逻辑
    fn parseConfig(args: &[String]) -> (&str, &str) {
        println!("args len is  {}", args.len());
        println!("args  is  {:?}", args);
        let query = &args[1];
        let filename = &args[2];
    
        println!("Searching for {}", query);
        println!("In file {}", filename);
        (query, filename)
    }
    
    

    同样,我们用命令:

    cargo run the poem.txt
    

    运行后的结果为:

    args len is  3
    args  is  [".\target\debug\minigrep.exe", "the", "poem.txt"]
    Searching for the
    In file poem.txt
    With text:
    I'm nobody! Who are you?
    Are you nobody, too?
    Then there's a pair of us - don't tell!
    They'd banish us, you know.
    
    How dreary to be somebody!
    How public, like a frog
    To tell your name the livelong day
    To an admiring bog!
    

    很好,我们的程序正常运行。

    我们再来看看把元组用一个结构体来替换,把相关参数属性放在一起,更简洁直观。

    开始吧:

    use std::env;
    use std::fs;
    fn main() {
        let args: Vec<String> = env::args().collect();
    
        let config: Config = parseConfig(&args);
    
        let contents =
            fs::read_to_string(config.filename).expect("Something went wrong reading the file");
    
        println!("With text:
    {}", contents);
    }
    //结构体Config用来封装参数属性
    struct Config {
        query: String,
        filename: String,
    }
    //参数读取与解析的逻辑
    fn parseConfig(args: &[String]) -> Config {
        println!("args len is  {}", args.len());
        println!("args  is  {:?}", args);
        let query = args[1].clone();//这里直接用clone方法得到一个参数string的拷贝
        let filename = args[2].clone();//这里直接用clone方法得到一个参数string的拷贝
    
        println!("Searching for {}", query);
        println!("In file {}", filename);
        Config { query, filename }//返回结构体
    }
    
    

    同样,我们用命令:

    cargo run the poem.txt
    

    运行后的结果跟原来一样,重构成功!

    有同学会问,这里为什么用clone呢?

    不会有性能问题吗?

    因为简单!

    我们先保持简单,让程序能跑起来,以后再考虑性能的问题。(事实上,这里的性能只损失很少一部分。)

    很好!

    能否再优化重构一下:参数读取与解析代码?

    可以的。

    我们看如下 代码:

    use std::env;
    use std::fs;
    //主函数,程序入口 
    fn main() {
        let args: Vec<String> = env::args().collect();
    
        let config: Config = Config::new(&args);//直接调用Config构造函数
    
        let contents =
            fs::read_to_string(config.filename).expect("Something went wrong reading the file");
    
        println!("With text:
    {}", contents);
    }
    //结构体Config用来封装参数属性
    struct Config {
        query: String,
        filename: String,
    }
    //为结构体实现一个构造器,其主要功能也是读取和解析参数
    impl Config {
        fn new(args: &[String]) -> Config {
            let query = args[1].clone();
            let filename = args[2].clone();
    
            Config { query, filename }
        }
    }
    //参数读取与解析的逻辑,现在可以删除了!
    fn parseConfig(args: &[String]) -> Config {
        println!("args len is  {}", args.len());
        println!("args  is  {:?}", args);
        let query = args[1].clone();//这里直接用clone方法得到一个参数string的拷贝
        let filename = args[2].clone();//这里直接用clone方法得到一个参数string的拷贝
    
        println!("Searching for {}", query);
        println!("In file {}", filename);
        Config { query, filename }//返回结构体
    }
    
    

    同样,我们用命令:

    cargo run the poem.txt
    

    运行后的结果跟原来一样,重构成功!

    这时,我们的函数:parseConfig,可以退休了。

    好吧,直接把它删除!

    同样,我们用命令:

    cargo run the poem.txt
    

    运行后的结果跟原来一样,删除成功!

    这里为什么,每执行一次小版本的重构,都要跑一次代码呢?

    因为可以保证,每次重构都是很小一步,可以避免错误!如果重构失败,也容易回退代码。

    好吧,我们继续重构。

    我们现在考虑一下错误处理。

    比如,我们现在直接用命令:cargo run 运行代码,会报错:

    $ cargo run
       Compiling minigrep v0.1.0 (file:///projects/minigrep)
        Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
         Running `target/debug/minigrep`
    thread 'main' panicked at 'index out of bounds: the len is 1
    but the index is 1', src/main.rs:25:21
    note: Run with `RUST_BACKTRACE=1` for a backtrace.
    

    好,我们现在在代码上加上错误处理的逻辑,修改Config的构造函数:

    //为结构体实现一个构造器,其主要功能也是读取和解析参数
    impl Config {
        fn new(args: &[String]) -> Config {
            if args.len() < 3 {
                panic!("参数个数不够!not enough arguments");//增加错误处理
            }
            let query = args[1].clone();
            let filename = args[2].clone();
    
            Config { query, filename }
        }
    }
    
    

    直接用命令:cargo run 运行代码,会报错,但错误信息明确多了:

    Finished dev [unoptimized + debuginfo] target(s) in 0.96s
         Running `targetdebugminigrep.exe`
    thread 'main' panicked at '参数个数不够!not enough arguments', srcmain.rs:23:13
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
    

    能否更优雅地处理错误信息?

    可以,我们可以用Result,代码如下:

    use std::env;
    use std::fs;
    use std::process;
    //主函数,程序入口
    fn main() {
        let args: Vec<String> = env::args().collect();
    
        // let config: Config = Config::new(&args);
        let config = Config::new(&args).unwrap_or_else(|err| {
            println!("Problem parsing arguments: {}", err);
            process::exit(1);
        });
    
        let contents =
            fs::read_to_string(config.filename).expect("Something went wrong reading the file");
    
        println!("With text:
    {}", contents);
    }
    //结构体Config用来封装参数属性
    struct Config {
        query: String,
        filename: String,
    }
    
    //为结构体实现一个构造器,其主要功能也是读取和解析参数
    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 })
        }
    }
    
    

    直接用命令:cargo run 运行代码,会报错,错误信息一样:

    Finished dev [unoptimized + debuginfo] target(s) in 0.96s
         Running `targetdebugminigrep.exe`
    thread 'main' panicked at '参数个数不够!not enough arguments', srcmain.rs:23:13
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
    

    错误处理代码,重构成功!

    我们再来跑一下正确的流程。

    我们用命令:

    cargo run the poem.txt
    

    运行后的结果跟原来一样,很好!

    现在我们再来重构一下main函数的对参数的处理逻辑,代码如下:

    use std::env;
    use std::fs;
    use std::process;
    //主函数,程序入口
    fn main() {
        let args: Vec<String> = env::args().collect();
    
        // let config: Config = Config::new(&args);
        let config = Config::new(&args).unwrap_or_else(|err| {
            println!("Problem parsing arguments: {}", err);
            process::exit(1);
        });
    
        // let contents =
        //     fs::read_to_string(config.filename).expect("Something went wrong reading the file");
    
        // println!("With text:
    {}", contents)
        run(config);//重构从文件中读取内容的业务逻辑
    }
    //结构体Config用来封装参数属性
    struct Config {
        query: String,
        filename: String,
    }
    
    //为结构体实现一个构造器,其主要功能也是读取和解析参数
    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 })
        }
    }
    //重构从文件中读取内容的业务逻辑
    fn run(config: Config) {
        let contents = fs::read_to_string(config.filename)
            .expect("从文件中读取内容时出错!Something went wrong reading the file");
    
        println!("With text:
    {}", contents);
    }
    
    

    我们用命令:

    cargo run the poem.txt
    

    运行后的结果跟原来一样,重构成功!

    我们再来让run方法有返回值,这样,主程序更好处理,看代码:

    use std::env;
    use std::error::Error;
    use std::fs;
    use std::process;
    //主函数,程序入口
    fn main() {
        let args: Vec<String> = env::args().collect();
    
        // let config: Config = Config::new(&args);
        let config = Config::new(&args).unwrap_or_else(|err| {
            println!("Problem parsing arguments: {}", err);
            process::exit(1);
        });
    
        if let Err(e) = run(config) {
            //根据处理结果返回值 来处理,如果有错误,则打印信息,并直接退出当前程序
            println!("Application error: {}", e);
    
            process::exit(1);
        }
    }
    //结构体Config用来封装参数属性
    struct Config {
        query: String,
        filename: String,
    }
    
    //为结构体实现一个构造器,其主要功能也是读取和解析参数
    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 })
        }
    }
    //重构从文件中读取内容的业务逻辑
    fn run(config: Config) -> Result<(), Box<dyn Error>> {
        let contents = fs::read_to_string(config.filename)?;
    
        println!("With text:
    {}", contents);
    
        Ok(())
    }
    
    

    我们用命令:

    cargo run the poem.txt
    

    运行后的结果跟原来一样,重构成功!

    非常好!

    现在我们再来重构,把main函数,所有业务逻辑迁移到lib.rs文件。

    我们来看看怎么重构。

    首先,我们先在工程目录下的目录src下创建另一个文件lib.rs,并把main函数相关代码写入进去:

    use std::error::Error;
    use std::fs;
    
    //结构体Config用来封装参数属性
    pub struct Config {
        query: String,
        filename: String,
    }
    
    //为结构体实现一个构造器,其主要功能也是读取和解析参数
    impl Config {
        pub 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 })
        }
    }
    //重构从文件中读取内容的业务逻辑
    pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
        let contents = fs::read_to_string(config.filename)?;
    
        println!("With text:
    {}", contents);
    
        Ok(())
    }
    
    

    这时src/main.rs的代码更新为如下:

    use minigrep::run;
    use minigrep::Config;
    
    use std::env;
    use std::process;
    //主函数,程序入口
    fn main() {
        let args: Vec<String> = env::args().collect();
    
        // let config: Config = Config::new(&args);
        let config = Config::new(&args).unwrap_or_else(|err| {
            println!("Problem parsing arguments: {}", err);
            process::exit(1);
        });
    
        if let Err(e) = run(config) {
            //根据处理结果返回值 来处理,如果有错误,则打印信息,并直接退出当前程序
            println!("Application error: {}", e);
    
            process::exit(1);
        }
    }
    
    

    我们用命令:

    cargo run the poem.txt
    

    运行后的结果跟原来一样,重构成功!

    完美!

    我们已经有一个能运行的基本程序框架。

    以上,希望对你有用。

    如果遇到什么问题,欢迎加入:rust新手群,在这里我可以提供一些简单的帮助,加微信:360369487,注明:博客园+rust
    

    参考文章:

    https://doc.rust-lang.org/stable/book/ch12-00-an-io-project.html

  • 相关阅读:
    结对项目开始
    团队项目开始
    个人项目总结
    python面向对象
    OSI七层模型
    Django中的orm的惰性机制
    Python装饰器
    python运算符
    python元祖和列表
    Sencha Touch 1.x 快速入门 第三章 布局(2) Card布局
  • 原文地址:https://www.cnblogs.com/gyc567/p/12067149.html
Copyright © 2011-2022 走看看