一起来学rust|简单的mingrep

Posted HX-Note

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一起来学rust|简单的mingrep相关的知识,希望对你有一定的参考价值。

这是我学习rust起步,意在通过这个小的项目,学习一些基本的rust的思想,熟悉rust的基本语法知识。

2023-5-14,完成于新冠病隔离期间。
是我学习rust起步,意在通过这个小的项目,学习一些基本的rust的思想,熟悉rust的基本语法知识。

学习到的内容总结

  • 关注分离点:把程序的启动放到main,主体逻辑移入库实现
  • 聚合变量,将与某件事情有关的所有变量,聚合起来,同时也赋予其相应的方法
  • 测试驱动的逻辑,先写一个绝对失败的例子,再写一个绝对成功的例子,编写你的代码逻辑,直到成功
  • 使用环境变量,来增加程序的撸棒性
  • 错误处理的两种逻辑:主动与被动
  • 日志分离,普通日志与错误信息提示
  • 迭代器遍历,更加简洁且完整,性能与安全性大幅度提升
  • IO处理,要明白,一切用户输入都是不可信的输入
  • 使用?返回特征对象,用Box来存储动态的特征对象指针
  • 代码分离,对象抽象

Rust知名项目grep替代

https://github.com/BurntSushi/ripgrep

实现的功能

ps:查看cargo参数的方法,cargo --list

  • input:指定文件和待查找的字符串
  • output:返回查找结果

大体的形式

传入,--告诉后面的参数给程序使用而不是cargo

cargo run -- searchstring example-filename.txt

程序接受参数并且处理

//in main.rs

//引入环境包,可以处理传入的参数
use std::env;

fn main() 
    //将内容转换为数组集合
    let args: Vec<String> = env::args().collect();
    //使用 dbg!宏,读取数组内容
    dbg!(args);


尝试一下

env::args` 读取到的参数中第一个就是程序的可执行路径名

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/minigrep`
[src/main.rs:5] args = [
    "target/debug/minigrep",
]
$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/minigrep`
[src/main.rs:5] args = [
    "target/debug/minigrep",
]

不可信输入

这里,选择直接了当,让用户知道自己错了,而不是人为去处理这些不可信输入。

这里有一个思考的点:就是不可信输入的处理想法

所有的用户输入都不可信!不可信!不可信!
重要的话说三遍,我们的命令行程序也是,用户会输入什么你根本就不知道,例如他输入了一个非 Unicode 字符,你能阻止吗?显然不能,但是这种输入会直接让我们的程序崩溃!
原因是当传入的命令行参数包含非 Unicode 字符时, std::env::args 会直接崩溃,如果有这种特殊需求,建议大家使用 std::env::args_os,该方法产生的数组将包含 OsString 类型,而不是之前的 String 类型,前者对于非 Unicode 字符会有更好的处理。
至于为啥我们不用,两个理由,你信哪个:1. 用户爱输入啥输入啥,反正崩溃了,他就知道自己错了 2. args_os 会引入额外的跨平台复杂性

项目创建

cargo new minigrep
     Created binary (application) `minigrep` project
$ cd minigrep

实现基本的功能

存储读取到的参数

给予清晰合理的变量名是一项基本功,咱总不能到处都是 args[1] 、args[2] 这样的糟糕代码吧。

//in main.rs

//引入环境包,可以处理传入的参数
use std::env;

fn main() 
    //将内容转换为数组集合
    let args: Vec<String> = env::args().collect();

    //待搜查字符串
    //存储文件路径
    let query = &args[1];
    let file_path = &args[2];

    println!("Searching jfor ",query);
    println!("In file ",file_path);

    //使用 dbg!宏,读取数组内容
    //dbg!(args);

文件读取

在根目录创建内容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!
传遍整个无聊的沼泽!

代码逻辑

use std::fs;

    //省略

    //读取文件内容,记得异常处理
    let context = fs::read_to_string(file_path)
        .expect("Should have been able to read the file!");
    //捕获变量输出
    print!("With text:\\n context"); 

不足点总结

完美,虽然代码还有很多瑕疵,例如所有内容都在 main 函数,这个不符合软件工程,没有错误处理,功能不完善等。不过没关系,万事开头难,好歹我们成功迈开了第一步。
但凡稍微没那么糟糕的程序,都应该具有代码模块化和错误处理,不然连玩具都谈不上。
梳理我们的代码和目标后,可以整理出大致四个改进点:

  • 单一且庞大的函数。对于 minigrep 程序而言, main 函数当前执行两个任务:解析命令行参数和读取文件。但随着代码的增加,main 函数承载的功能也将快速增加。从软件工程角度来看,一个函数具有的功能越多,越是难以阅读和维护。因此最好的办法是将大的函数拆分成更小的功能单元。
  • 配置变量散乱在各处。还有一点要考虑的是,当前 main 函数中的变量都是独立存在的,这些变量很可能被整个程序所访问,在这个背景下,独立的变量越多,越是难以维护,因此我们还可以将这些用于配置的变量整合到一个结构体中。
  • 细化错误提示。 目前的实现中,我们使用 expect 方法来输出文件读取失败时的错误信息,这个没问题,但是无论任何情况下,都只输出 Should have been able to read the file 这条错误提示信息,显然是有问题的,毕竟文件不存在、无权限等等都是可能的错误,一条大一统的消息无法给予用户更多的提示。
  • 使用错误而不是异常。 假如用户不给任何命令行参数,那我们的程序显然会无情崩溃,原因很简单:index out of bounds,一个数组访问越界的 panic,但问题来了,用户能看懂吗?甚至于未来接收的维护者能看懂吗?因此需要增加合适的错误处理代码,来给予使用者给详细友善的提示。还有就是需要在一个统一的位置来处理所有错误,利人利己!

改进程序(增加模块化和错误处理)

分离main函数思路(启动与逻辑)

如何处理庞大的 main 函数,Rust 社区给出了统一的指导方案:

  • 将程序分割为 main.rs 和 lib.rs,并将程序的逻辑代码移动到后者内
  • 命令行解析属于非常基础的功能,严格来说不算是逻辑代码的一部分,因此还可以放在 main.rs 中

按照这个方案,将我们的代码重新梳理后,可以得出 main 函数应该包含的功能:

  • 解析命令行参数
  • 初始化其它配置
  • 调用 lib.rs 中的 run 函数,以启动逻辑代码的运行
  • 如果 run 返回一个错误,需要对该错误进行处理

这个方案有一个很优雅的名字: 关注点分离(Separation of Concerns)。
简而言之,main.rs 负责启动程序,lib.rs 负责逻辑代码的运行。
从测试的角度而言,这种分离也非常合理: lib.rs 中的主体逻辑代码可以得到简单且充分的测试,至于 main.rs ?确实没办法针对其编写额外的测试代码,但是它的代码也很少啊,很容易就能保证它的正确性。

分离main中命令解析

//in main.rs

fn main() 
    //将内容转换为数组集合
    let args: Vec<String> = env::args().collect();

    //2.0,命令解析
    let (query, file_path) = parse_config(&args);

    //省略


// in main.rs
//解析函数
fn parse_config(args: &[String]) -> (&str,&str) 
   let query = &args[1];
   let file_path = &args[2];
   (query,file_path)

聚合配置变量

配置变量并不适合分散的到处都是,因此使用一个结构体来统一存放是非常好的选择,这样修改后,后续的使用以及未来的代码维护都将更加简单明了

//in main.rs

//引入环境包,可以处理传入的参数
use std::env;
use std::fs;

fn main() 
    //将内容转换为数组集合
    let args: Vec<String> = env::args().collect();

    //待搜查字符串
    //存储文件路径
    //let query = &args[1];
    //let file_path = &args[2];
    //2.0
    let config = parse_config(&args);

    println!("Searching for ",config.query);
    println!("In file ",config.file_path);

    //读取文件内容,记得异常处理
    let context = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file!");
    //捕获变量
    print!("With text:\\ncontext");

    //使用 dbg!宏,读取数组内容
    //dbg!(args);


// in main.rs
//聚合变量
struct Config 
    query:String,
    file_path:String,


// 命令解析
//这里选择clone, 防止所有权带来的问题
fn parse_config(args: &[String]) -> Config 
   let query = args[1].clone();
   let file_path = args[2].clone();
   Config query,file_path


clone 的得与失
在上面的代码中,除了使用 clone ,还有其它办法来达成同样的目的,但 clone 无疑是最简单的方法:直接完整的复制目标数据,无需被所有权、借用等问题所困扰,但是它也有其缺点,那就是有一定的性能损耗。
因此是否使用 clone 更多是一种性能上的权衡,对于上面的使用而言,由于是配置的初始化,因此整个程序只需要执行一次,性能损耗几乎是可以忽略不计的。
总之,判断是否使用 clone:

  • 是否严肃的项目,玩具项目直接用 clone 就行,简单不好吗?
  • 要看所在的代码路径是否是热点路径(hot path),例如执行次数较多的显然就是热点路径,热点路径就值得去使用性能更好的实现方式

上面的代码总感觉差点意思,特别是从OO语言来的!
目标:
通过构造函数来初始化一个 Config 实例,而不是直接通过函数返回实例,典型的,标准库中的 String::new 函数就是一个范例。

//in main.rs

//引入环境包,可以处理传入的参数
use std::env;
use std::fs;

fn main() 
    //将内容转换为数组集合
    let args: Vec<String> = env::args().collect();

    //待搜查字符串
    //存储文件路径
    //let query = &args[1];
    //let file_path = &args[2];
    //2.0
    //let config = parse_config(&args);
    let config = Config::new(&args);

    println!("Searching for ", config.query);
    println!("In file ", config.file_path);

    //读取文件内容,记得异常处理
    let context =
        fs::read_to_string(config.file_path).expect("Should have been able to read the file!");
    //捕获变量
    print!("With text:\\ncontext");

    //使用 dbg!宏,读取数组内容
    //dbg!(args);


// in main.rs
//聚合配置变量
struct Config 
    query: String,
    file_path: String,


// 命令解析
//这里选择clone, 防止所有权带来的问题
/*
fn parse_config(args: &[String]) -> Config 
   let query = args[1].clone();
   let file_path = args[2].clone();
   Config query,file_path

*/
//修改为符合 OO 语言的构造

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


错误处理

如果用户不输入任何命令行参数,我们的程序会怎么样?
结果喜闻乐见,由于 args 数组没有任何元素,因此通过索引访问时,会直接报出数组访问越界的 panic。

想法一:主动调用panic

impl Config 
    fn new(args: &[String]) -> Config 
        //错误处理1:主动调用panic
        if args.len( ) < 3 
            panic!("not enough arguments");
        
        let query = args[1].clone();
        let file_path = args[2].clone();
        Config  query, file_path 
    


用户看到了更为明确的提示,但是还是有一大堆 debug 输出,这些我们其实是不想让用户看到的。这么看来,想要输出对用户友好的信息, panic 是不太适合的,它更适合告知开发者,哪里出现了问题

想法二:返回Result来代替直接panic

new往往不会失败,毕竟新建一个实例没道理失败,对不?因此修改为build` 会更加合适。

//in main.rs

//引入环境包,可以处理传入的参数
use std::env;
use std::fs;
use std::process;

fn main() 
    //将内容转换为数组集合
    let args: Vec<String> = env::args().collect();

    //待搜查字符串
    //存储文件路径
    //let query = &args[1];
    //let file_path = &args[2];
    //2.0
    //let config = parse_config(&args);
    //更加友好的错误处理
    let config = Config::build(&args).unwrap_or_else(|err| 
        println!("Problem parsing arguments: err");
        process::exit(1);
    );

    println!("Searching for ", config.query);
    println!("In file ", config.file_path);

    //读取文件内容,记得异常处理
    let context =
        fs::read_to_string(config.file_path).expect("Should have been able to read the file!");
    //捕获变量
    print!("With text:\\ncontext");

    //使用 dbg!宏,读取数组内容
    //dbg!(args);


// in main.rs
//聚合配置变量
struct Config 
    query: String,
    file_path: String,


// 命令解析
//这里选择clone, 防止所有权带来的问题
/*
fn parse_config(args: &[String]) -> Config 
   let query = args[1].clone();
   let file_path = args[2].clone();
   Config query,file_path

*/
//修改为符合 OO 语言的构造

impl Config                                   //标注生命周期,让闭包可以处理
    fn build(args: &[String]) -> Result<Config, &\'static str> 
        //错误处理1:主动调用panic
        /*
        if args.len( ) < 3 
            panic!("not enough arguments");
        
        */
        //错误处理2:返回Result来直接代替panic
        if args.len() < 3 
            return Err("not enough arguments");
        
        let query = args[1].clone();
        let file_path = args[2].clone();
        Ok(Config  query, file_path )
    

  • 当 Result 包含错误时,我们不再调用 panic 让程序崩溃,而是通过 process::exit(1) 来终结进程,其中 1 是一个信号值(事实上非 0 值都可以),通知调用我们程序的进程,程序是因为错误而退出的。
  • unwrap_or_else 是定义在 Result<T,E> 上的常用方法,如果 Result 是 Ok,那该方法就类似 unwrap:返回 Ok 内部的值;如果是 Err,就调用闭包中的自定义代码对错误进行进一步处理

分离主体逻辑

继续精简 main 函数,那就是将主体逻辑( 例如业务逻辑 )从 main 中分离出去,这样 main 函数就保留主流程调用,非常简洁。只负责解析命令。

注意:这里因为config直接由主体逻辑负责了,所以采用所有权进行转移。


fn main() 
    //将内容转换为数组集合
    let args: Vec<String> = env::args().collect();

    //待搜查字符串
    //存储文件路径
    //let query = &args[1];
    //let file_path = &args[2];
    //2.0
    //let config = parse_config(&args);
    //更加友好的错误处理
    let config = Config::build(&args).unwrap_or_else(|err| 
        println!("Problem parsing arguments: err");
        process::exit(1);
    );

    println!("Searching for ", config.query);
    println!("In file ", config.file_path);

    run(config)

    //使用 dbg!宏,读取数组内容
    //dbg!(args);


//直接所有权转移
fn run(config: Config) 
    //读取文件内容,记得异常处理
    let context =
        fs::read_to_string(config.file_path).expect("Should have been able to read the file!");
    //捕获变量
    print!("With text:\\ncontext");


使用?和特征对象来返回错误

run` 函数没有错误处理,因为在文章开头我们提到过,错误处理最好统一在一个地方完成,这样极其有利于后续的代码维护。

//in main.rs

//引入环境包,可以处理传入的参数
use std::env;
use std::error::Error;
use std::fs;
use std::process;

fn main() 
    //将内容转换为数组集合
    let args: Vec<String> = env::args().collect();

    //待搜查字符串
    //存储文件路径
    //let query = &args[1];
    //let file_path = &args[2];
    //2.0
    //let config = parse_config(&args);
    //更加友好的错误处理
    let config = Config::build(&args).unwrap_or_else(|err| 
        println!("Problem parsing arguments: err");
        process::exit(1);
    );

    println!("Searching for ", config.query);
    println!("In file ", config.file_path);
    
    //因为只需要匹配一个,用if let 进行匹配错误,不用match
    if let Err(e) = run(config) 
        println!("Application error: e");
        process::exit(1);
    

    //使用 dbg!宏,读取数组内容
    //dbg!(args);


//直接所有权转移
//Error是特征对象,引入包,dyn类型,动态,智能指针
fn run(config: Config) -> Result<(), Box<dyn Error>>
    //读取文件内容,记得异常处理
    //使用 ? 将otherError转换为returnError
    let context =
        fs::read_to_string(config.file_path)?;
    //捕获变量
    print!("With text:\\ncontext");

    Ok(())


// in main.rs
//聚合配置变量
struct Config 
    query: String,
    file_path: String,


// 命令解析
//这里选择clone, 防止所有权带来的问题
/*
fn parse_config(args: &[String]) -> Config 
   let query = args[1].clone();
   let file_path = args[2].clone();
   Config query,file_path

*/
//修改为符合 OO 语言的构造

impl Config 
    fn build(args: &[String]) -> Result<Config, &\'static str> 
        //错误处理1:主动调用panic
        /*
        if args.len( ) < 3 
            panic!("not enough arguments");
        
        */
        //错误处理2:返回Result来直接代替panic
        if args.len() < 3 
            return Err("not enough arguments");
        
        let query = args[1].clone();
        let file_path = args[2].clone();
        Ok(Config  query, file_path )
    


  • Result<T,E>的要求,因此使用了Ok(())返回一个单元类型()。最重要的是 Box, 如果按照顺序学到这里,大家应该知道这是一个Error 的特征对象(为了使用 Error,我们通过 use std::error::Error; 进行了引入),它表示函数返回一个类型,该类型实现了 Error 特征,这样我们就无需指定具体的错误类型,否则你还需要查看。
  • ?传播界的大明星
  • 最后,用if let处理返回的错误,我们并不关注 run 返回的 Ok 值,因此只需要用 if let 去匹配是否存在错误即可。

分离逻辑代码到库包中

首先,创建一个 src/lib.rs 文件,然后将所有的非 main 函数都移动到其中。

记得:分离之后,要对库文件里的各个代码标记上权限等。

libc.rs

//引入到库包中,非main的代码

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


//聚合配置变量
pub struct Config 
    pub query: String,
    pub file_path: String,


// 命令解析
//这里选择clone, 防止所有权带来的问题
/*
fn parse_config(args: &[String]) -> Config 
   let query = args[1].clone();
   let file_path = args[2].clone();
   Config query,file_path

*/
//修改为符合 OO 语言的构造

impl Config 
    pub fn build(args: &[String]) -> Result<Config, &\'static str> 
        //错误处理1:主动调用panic
        /*
        if args.len( ) < 3 
            panic!("not enough arguments");
        
        */
        //错误处理2:返回Result来直接代替panic
        if args.len() < 3 
            return Err("not enough arguments");
        
        let query = args[1].clone();
        let file_path = args[2].clone();
        Ok(Config  query, file_path )
    


//直接所有权转移
//Error是特征对象,引入包,dyn类型,动态,智能指针
pub fn run(config: Config) -> Result<(), Box<dyn Error>>
    //读取文件内容,记得异常处理
    //使用 ? 将otherError转换为returnError
    let context =
        fs::read_to_string(config.file_path)?;
    //捕获变量
    print!("With text:\\ncontext");

    Ok(())

main.rs

//in main.rs

//引入环境包,可以处理传入的参数
use std::env;
use std::process;

use minigrep::Config;

fn main() 
    //将内容转换为数组集合
    let args: Vec<String> = env::args().collect();

    //待搜查字符串
    //存储文件路径
    //let query = &args[1];
    //let file_path = &args[2];
    //2.0
    //let config = parse_config(&args);
    //更加友好的错误处理
    let config = Config::build(&args).unwrap_or_else(|err| 
        println!("Problem parsing arguments: err");
        process::exit(1);
    );

    println!("Searching for ", config.query);
    println!("In file ", config.file_path);
    
    //因为只需要匹配一个,用if let 进行匹配错误,不用match
    if let Err(e) = minigrep::run(config) 
        println!("Application error: e");
        process::exit(1);
    

    //使用 dbg!宏,读取数组内容
    //dbg!(args);

这里的 mingrep::run 的调用,以及 Config 的引入,跟使用其它第三方包已经没有任何区别,也意味着我们成功的将逻辑代码放置到一个独立的库包中,其它包只要引入和调用就行。

这里,lib.rs就是库,库的名就是项目名。本身也是一个包。

测试驱动开发

我们需要先编写一些测试代码,也是最近颇为流行的测试驱动开发模式(TDD, Test Driven Development):

  1. 编写一个注定失败的测试,并且失败的原因和你指定的一样
  2. 编写一个成功的测试
  3. 编写你的逻辑代码,直到通过测试

注定失败的用例

pub fn search<\'a >(query: &str,contents: &\'a str) -> Vec<&\'a str> 
    vec![]


#[cfg(test)]
mod tests 
    use super::*;

    #[test]
    fn one_result( ) 
        let query = "duct";
        let contents = "\\
        Rust:safe, fast, productive.
        Pick three.
        ";

        assert_eq!(vec!["safe,fast,productive."],search(query,contents));
    


$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished test [unoptimized + debuginfo] target(s) in 0.97s
     Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 1 test
test tests::one_result ... FAILED

failures:

---- tests::one_result stdout ----
thread \'main\' panicked at \'assertion failed: `(left == right)`
  left: `["safe, fast, productive."]`,
 right: `[]`\', src/lib.rs:44:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::one_result

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

注定成功的用例

pub fn search<\'a >(query: &str,contents: &\'a str) -> Vec<&\'a str> 
    //vec![] //注定失败的用例
    let mut result = Vec::new();
    for line in contents.lines() 
        if line.contains(query) 
            result.push(line);
        
    
    result

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished test [unoptimized + debuginfo] target(s) in 1.22s
     Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 1 test
test tests::one_result ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests minigrep

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

在run函数中调用search函数

//直接所有权转移
//Error是特征对象,引入包,dyn类型,动态,智能指针
pub fn run(config: Config) -> Result<(), Box<dyn Error>> 
    //读取文件内容,记得异常处理
    //使用 ? 将otherError转换为returnError
    let contents = fs::read_to_string(config.file_path)?;
    //捕获变量
    //print!("With text:\\ncontents");

    for line in search(&config.query, &contents) 
        println!("line");
    

    Ok(())

$ cargo run -- frog poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/minigrep frog poem.txt`
How public, like a frog

使用环境变量来增强程序

在编译时候控制,如栈展开,大小写敏感

RUST_BACKTRACE=1 cargo run
IGNORE_CASE=1 cargo run -- to poem.txt

首先写一个注定失败

很简单,函数不要实现就好

写一个成功的案例

//大小写敏感
pub fn search<\'a >(query: &str,contents: &\'a str) -> Vec<&\'a str> 
    //vec![] //注定失败的用例
    let mut result = Vec::new();
    for line in contents.lines() 
        if line.contains(query) 
            result.push(line);
        
    
    result


//大小写不敏感
pub fn search_case_insensitive<\'a >(query: &str,contents: &\'a str) -> Vec<&\'a str> 
    let query = query.to_lowercase();
    let mut result = Vec::new();

    for line in contents.lines() 
        if line.to_lowercase().contains(&query) 
            result.push(line);
        
    

    result


#[cfg(test)]
mod tests 
    use super::*;
    
    #[test] //标注为test
    fn case_sensitive( ) 
        let query = "duct";
        let contents = "\\
Rust:
safe, fast, productive.
Pick three.
Duct tape.
";

        assert_eq!(vec!["safe, fast, productive."],search(query,contents));
    
     
    #[test]
    fn case_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));
    

环境控制大写与内部流程控制

//直接所有权转移
//Error是特征对象,引入包,dyn类型,动态,智能指针
pub fn run(config: Config) -> Result<(), Box<dyn Error>> 
    //读取文件内容,记得异常处理
    //使用 ? 将otherError转换为returnError
    let contents = fs::read_to_string(config.file_path)?;
    //捕获变量
    //print!("With text:\\ncontents");

    let results = if config.ignore_case 
        search_case_insensitive(&config.query, &contents) 
     else 
        search(&config.query, &contents)
    ;

    for line in results 
        println!("line");
    

    Ok(())

//聚合配置变量
pub struct Config 
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,


重定向错误信息的输出

无论 debug 还是 error 类型,都是通过 println! 宏输出到终端的标准输出( stdout ),但是对于程序来说,错误信息更适合输出到标准错误输出(stderr)。
用户就可以选择将普通的日志类信息输出到日志文件 1,然后将错误信息输出到日志文件 2,甚至还可以输出到终端命令行。

结论:分离错误信息

如果错信息输出到标准输出,那么它们将跟普通的日志信息混在一起,难以分辨,因此我们需要将错误信息进行单独输出。

标准错误输出stderr

将错误信息和日志信息,在终端输出
其他内容,在新的文件中出现
很简单,将改为eprintln即可

fn main() 
    //将内容转换为数组集合
    let args: Vec<String> = env::args().collect();

    //待搜查字符串
    //存储文件路径
    //let query = &args[1];
    //let file_path = &args[2];
    //2.0
    //let config = parse_config(&args);
    //更加友好的错误处理
    let config = Config::build(&args).unwrap_or_else(|err| 
        eprintln!("Problem parsing arguments: err");
        process::exit(1);
    );

    //println!("Searching for ", config.query);
    //println!("In file ", config.file_path);
    
    //因为只需要匹配一个,用if let 进行匹配错误,不用match
    if let Err(e) = minigrep::run(config) 
        eprintln!("Application error: e");
        process::exit(1);
    

    //使用 dbg!宏,读取数组内容
    //dbg!(args);

完结+迭代器修改

一些后续不再使用的,自己传入迭代器
用迭代器去处理遍历,让语言变得更rusty

main.rs

//in main.rs

//引入环境包,可以处理传入的参数
use std::env;
use std::process;

use minigrep::Config;

fn main() 
    //将内容转换为数组集合
    //let args: Vec<String> = env::args().collect();

    //待搜查字符串
    //存储文件路径
    //let query = &args[1];
    //let file_path = &args[2];
    //2.0
    //let config = parse_config(&args);
    //更加友好的错误处理

    //这里直接传入迭代器即可
    let config = Config::build(env::args()).unwrap_or_else(|err| 
        eprintln!("Problem parsing arguments: err");
        process::exit(1);
    );

    //println!("Searching for ", config.query);
    //println!("In file ", config.file_path);
    
    //因为只需要匹配一个,用if let 进行匹配错误,不用match
    if let Err(e) = minigrep::run(config) 
        eprintln!("Application error: e");
        process::exit(1);
    

    //使用 dbg!宏,读取数组内容
    //dbg!(args);

lib.rs

//引入到库包中,非main的代码

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


//聚合配置变量
pub struct Config 
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,


// 命令解析
//这里选择clone, 防止所有权带来的问题
/*
fn parse_config(args: &[String]) -> Config 
   let query = args[1].clone();
   let file_path = args[2].clone();
   Config query,file_path

*/
//修改为符合 OO 语言的构造

impl Config 
    //由于修改了clone,所以这里要修改
    //而且移除索引
    pub fn build(mut args : impl Iterator<Item = String>) -> Result<Config, &\'static str> 
        //错误处理1:主动调用panic
        /*
        if args.len( ) < 3 
            panic!("not enough arguments");
        
        */
        //错误处理2:返回Result来直接代替panic
        /* 
        if args.len() < 3 
            return Err("not enough arguments");
        
        */

        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();
        
        //let query = args[1].clone();
        //let file_path = args[2].clone();

        //klet ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config  query, file_path , ignore_case)
    


//直接所有权转移
//Error是特征对象,引入包,dyn类型,动态,智能指针
pub fn run(config: Config) -> Result<(), Box<dyn Error>> 
    //读取文件内容,记得异常处理
    //使用 ? 将otherError转换为returnError
    let contents = fs::read_to_string(config.file_path)?;
    //捕获变量
    //print!("With text:\\ncontents");

    let results = if config.ignore_case 
        search_case_insensitive(&config.query, &contents) 
     else 
        search(&config.query, &contents)
    ;

    for line in results 
        println!("line");
    

    Ok(())


//大小写敏感
pub fn search<\'a >(query: &str,contents: &\'a str) -> Vec<&\'a str> 
    //vec![] //注定失败的用例
    /* 
    let mut result = Vec::new();
    for line in contents.lines() 
        if line.contains(query) 
            result.push(line);
        
    
    result
    */
    contents
        .lines()
        .filter(|line| line.contains(query))
        .collect()


//大小写不敏感
pub fn search_case_insensitive<\'a >(query: &str,contents: &\'a str) -> Vec<&\'a str> 
    let query = query.to_lowercase();
    /*
    let mut result = Vec::new();

    for line in contents.lines() 
        if line.to_lowercase().contains(&query) 
            result.push(line);
        
    
    result
    */
    contents
        .lines()
        .filter(|line| line.to_lowercase().contains(query.as_str()))
        .collect()


#[cfg(test)]
mod tests 
    use super::*;
    
    #[test] //标注为test
    fn case_sensitive( ) 
        let query = "duct";
        let contents = "\\
Rust:
safe, fast, productive.
Pick three.
Duct tape.
";

        assert_eq!(vec!["safe, fast, productive."],search(query,contents));
    
     
    #[test]
    fn case_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));
    


测试数据

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!
传遍整个无聊的沼泽!

通过欧拉计划学Rust(第1~6题)

最近想学习Libra数字货币的MOVE语言,发现它是用Rust编写的,看来想准确理解MOVE的机制,还需要对Rust有深刻的理解,所以开始了Rust的快速入门学习。

看了一下网上有关Rust的介绍,都说它的学习曲线相当陡峭,曾一度被其吓着,后来发现Rust借鉴了Haskell等函数式编程语言的优点,而我以前专门学习过Haskell,经过一段时间的入门学习,我现在已经喜欢上这门神奇的语言。

入门资料我用官方的《The Rust Programming Language》,非常权威,配合着《Rust by example》这本书一起学习,效果非常不错。

学习任何一项技能最怕没有反馈,尤其是学英语、学编程的时候,一定要“用”,学习编程时有一个非常有用的网站,它就是“欧拉计划”,网址:
https://projecteuler.net

这个网站提供了几百道由易到难的数学问题,你可以用任何办法去解决它,当然主要还得靠编程,但编程语言不限,已经有Java、C#、Python、Lisp、Haskell等各种解法,当然直接用google搜索答案就没意思了。

学习Rust最好先把基本的语法和特性看过一遍,然后就可以动手解题了,解题的过程就是学习、试错、再学习、掌握和巩固的过程,学习进度会大大加快。

环境准备

在Windows下安装,用官网上的rustup直接默认安装即可。

安装完成之后,就有了《The Rust Programming Language》这本书的离线HTML版本,直接用命令打开:

rustup doc --book

还要会使用强大的包管理器:cargo

这个cargo好用的另人发指,建项目、编译、运行都用用它:

cargo new euler1
cd euler1
cargo build
cargo run

第一题

问题描述:

1000以内(不含1000)的所有被3或5整除的整数之和。

直接上答案:

let mut sum = 0;
for i in 1..1000 
    if i % 3 == 0 || i % 5 == 0 
        sum += i;
    

println!("", sum);

mut关键字(mutable的缩写)是Rust的一大特色,所有变量默认为不可变的,如果想可变,需要mut关键字,否则在 sum += i 时会报编译错误。

println! 后面有一个叹号,表示这是一个宏,Rust里的宏也是非常非常强大!现在还不到了解的时候。

学过Python的列表推导(List Comprehension)语法的感觉这种题完全可以用一行语句搞定,Rust中需要用到filter()和sum()函数。

// 为了阅读,分成多行
println!(
    "",
    (1..1000).filter(|x| x % 3 == 0 || x % 5 == 0)
             .sum::<u32>() 
);

.. 这个语法糖表示一个范围,需要注意最后不包括1000,如果想包含1000,需要这样写:(1..=1000)

filter里面的|x|定义了一个闭包函数,关于闭包,又是一个复杂的主题。

sum::() 是一个范型函数,这种两个冒号的语法让我好不适应。

还可以用fold()函数,是这样写的:

println!(
    "",
    (1..1000)
        .filter(|x| x % 3 == 0 || x % 5 == 0)
        .fold(0, |s, a| s + a)
);

想把这些数全部打印出来:

println!(
    ":?",
    (1..1000)
        .filter(|x| x % 3 == 0 || x % 5 == 0)
        .collect::<Vec<u32>>()
);
// [3, 5, 6, 9, 10, 12, ... 999]

第二题

问题描述:

400万之内所有偶数的斐波那契数字之和。

算法并不难,这里的数列以[1, 2]开始,后面每个数是前面2个数字之和:

let mut fib = vec![1, 2];

let mut i = 2; // 已经有2个元素
let mut sum = 2; 
loop 
    let c = fib[i - 1] + fib[i - 2];
    if c >= 4_000_000 
        break;
    
    fib.push(c); 
    if c % 2 == 0 
        sum += c;
    
    i += 1;

println!("", sum);

这里没有使用函数式编程,大量使用了mut,无限循环用loop语法。

rust中关于整数的表示提供了多种数据类型,默认的整数类型是i32,默认浮点类型是f64。

数字类型中比较有特点的是可以用‘_‘分隔符,让数字更容易读一些,还可以把u32, i64等类型作为后缀来指明类型。

let 赋值语句与其它语言也不一样,还可以改变其类型,这个特性为隐藏shadowing。

let x = 500u16;
let x = x + 1;
let x = 4_000_000_u64;
let x = "slb";

fib是一个向量,相当于其它语言里的数组、列表。vec! 宏可以进行初始化任务。
这一行:

let mut fib = vec![1, 2];

与下面三行等价:

let mut fib = Vec::new();
fib.push(1);
fib.push(2);

push()函数用于给列表增加一个元素。

第三题

问题描述:
找出整数600851475143的最大素数因子。

素数就是只能被1和本身整除的数,首先定义一个函数is_prime(),用于判断是否为素数:

fn is_prime(num: u64) -> bool 
    for i in 2..(num / 2 + 1) 
        if num % i == 0 
            return false;
        
    
    true

Rust是强类型语言,看到函数定义里的 -> bool,让我想起了Haskell的语法。

函数最后一行的true孤零零的,没有分号,让人感觉很奇怪。Rust是一个基于表达式的语言,一个语句块的最后是一个表达式,当然也可以用return true;

现在可以查找最大的素数因子了:

let big_num = 600851475143;
for i in (2..=big_num).rev() 
    if big_num % i == 0 && is_prime(i) 
        println!("", i);
        break;
    

程序编译没问题,但几分钟也运行不出来结果,试着把数字调小一点,比如:600851,不到1秒出来结果,看来程序的效率太差了,主要是需要大量的判断素数的运算量,需要优化。

尝试把大数进行素数因子分解,并且把素因子记录下来进行比较,效率得到大幅提升,不到1秒得出结果。

let mut big_num = 600851475143;
let mut max_prime_factor = 2;

while big_num >= 2 
    for i in 2..=big_num 
        if big_num % i == 0 && is_prime(i) 
            big_num /= i;
            if i > max_prime_factor  
                max_prime_factor = i;
                break;
            
        
    

println!("", max_prime_factor);

第四题

问题描述:

求两个3位数之积最大的回文数。

所谓回文数,就是两边读都一样的数,比如:698896。

先写一个判断回文数的函数:

fn is_palindromic(n: u64) -> bool 
    let s = n.to_string();
    s.chars().rev().collect::<String>() == s

我把数字转换成字符串,再把字符串反序,如果与原字符串一样,则是回文数。

Rust中字符串的反序操作好奇怪,竟然不是s.rev(),我是google找到的那个代码片段。

剩下的逻辑并不复杂,用两重循环可以快速搞定。

let mut max = 0;
for x in 100..=999 
    for y in 100..=999 
        let prod = x * y;
        if is_palindromic(prod) && prod > max 
            max = prod;
            // println!(" x  = ", x, y, prod);
        
    

println!("", max);

我一开始以为只要反序搜索就可以快速找到答案,但找到的数并不是最大,你能发现问题之所在吗?不过,从这个错误代码中,我学会了双重循环如何跳出外层循环的语法。真是没有白走的弯路。

// 错误代码
'outer: for x in (100..=999).rev() 
    for y in (100..=999).rev() 
        let prod = x * y;
        if is_palindromic(prod) 
            println!(" x  = ", x, y, prod);
            break 'outer;
        
    

第五题

问题描述:

找出能够被1, 2, 3, ..., 20整除的最小整数。

代码逻辑很简单,一个一个尝试整除,找到后跳出最外层循环。

let mut x = 2 * 3 * 5 * 7;
'outer: loop 
    for f in 2..=20 
        if x % f != 0 break;
        if f == 20 
            println!("", x);
            break 'outer;
        
    
    x += 2;

如果你感觉程序运行效率不够高,可以用下面这个命令行运行,差别还是非常大的,感觉与C程序的效率相媲美:

cargo run --release

第六题

问题描述:
求1到100自然数的“和的平方”与“平方和”的差。

用普通的过程式编程方法,这题太简单,但要尝试一下函数式编程思路,代码可以异常简洁。

let sum_of_squares = (1..=100).map(|x| x*x).sum::<u32>();
let sum = (1..=100).sum::<u32>();
println!("", sum * sum - sum_of_squares);

另外还有一种使用fold()函数的写法,理解起来更困难一些:

let sum_of_squares = (1..=100).fold(0, |s, n| s + n * n);
let sum = (1..=100).fold(0, |s, n| s + n);
println!("", sum * sum - sum_of_squares);

以上是关于一起来学rust|简单的mingrep的主要内容,如果未能解决你的问题,请参考以下文章

Rust为什么我建议你学一下 Rust | Rust 初探

Rust 实现简单区块链

通过欧拉计划学Rust(第1~6题)

用欧拉计划学Rust语言(第17~21题)

只学过 C 语言适合学 Rust 吗?

Rust机器学习之tch-rs