0%

Rust小试牛刀

Rust小试牛刀

Rust的书已经看完2本了,看程序基本上能看懂一些了(生命周期这块还是迷迷糊糊),但是觉得多少还是要自己写一下程序,实际应用一下才能学会。正好今天找了一个练手的小项目,初步尝试一下

背景

N年之前给媳妇写过一个小程序,处理Excel数据的,大致就是从A.xlsx读取数据,分组合并并计算,然后追加写入到B.xlsx,但是其中有个字典关系的映射,以前是维护在代码中的,每次修改字典都要重新编译。
加上当时程序是.Net写的,WinForm程序,依赖Windows和VisualStudio这个组合,最近今年工作基本就没用过Windows,电脑也换成了M1的Mac,给她处理这个程序要安装个arm版Windows不说,还要安装VisualStudio,本就不富裕的硬盘,又被拖走了几十个G。
想着既然学了Rust,加上为了处理这个程序,索性就用Rust重写一份吧。

准备工作

  1. Rust的安装就不多说什么了,按照官网的脚本执行一下就搞定了
  2. 编辑器我用的是VisualCode,配合rust-analyzer插件,debug就是直接用CodeLLDB。
  3. 因为要解析xlsx文件,在https://lib.rs/search?q=xlsx查了一圈,最后决定用umya-spreadsheet="0.8.3"
  4. 因为有个需要配置的字典,考虑json和toml,最后决定用toml,因为对我媳妇而已,这个更直接一些,最终选择toml="0.5.0"

开工

cargo new一个项目,添加好依赖,开整,只是逻辑的话,其实很简单,按照.Net的逻辑直接迁移过来就行了,不过有一说一,写的过程中还是挺多麻烦的,光看书以为自己掌握的东西,coding的时候却干着急想不起来怎么写,最后还是依赖搜索引擎解决。
另外还有一点就是关于mod,直到程序写完了,也没太get这个玩意和目录的关系怎么搞。所以最终的项目还是基于一个main.rs实现的。
逻辑啥的,就不具体写这里了,记录一下自己开发过程中记得住记不住的东西吧。

结构体

读取数据之后肯定不能直接用个元祖或者Map传来传去,怎么也得有个结构体来组装数据,为了隔离开,我这里还是用mod来进行一下简单的区分。

1
2
3
4
5
6
7
8
9
pub mod model {
#[derive(Debug, Clone)]
pub struct ProductModel {
pub field1: String,
pub field2: String,
pub field3: i32,
pub type_: String,
}
}

别看就上面几个字段,就这个type_还折腾我一会,因为type是关键字,如果确实要用type做这个字段名称,就要用r#type, 还有就是从mod到struce到field,都要pub,否则访问不了(开始在不同目录,后来放到一个文件了,也没调整了,这块后面再补习一下吧)。

xlsx处理lib-umya_spreadsheet

rust处理Excel的lib本来就不是很多,还有很多是只支持读取不支持写入或者只支持写入不支持读取的,好不容易找到了这么一个既支持读又支持写入的,搜了一圈,除了官方文档,几乎没有任何资料。我处理xlsx的时候基本上是按照行进行处理,一行封装成一个model,所以需要知道当前xlsx的当前sheet有多少行数据,然后for循环遍历,组合数据。然而就找一共有多少行这个api就一个一个尝试,官方文档并没有说这块内容,网上也没有这个使用教程,饶了一大圈,最终找到了这个api sheet.get_highest_row() , 不管怎么说,兜了一大圈,这个总算是解决了。
然后就是下一个问题,写入api需要传一个book对象,因为第二个xlsx实际上是我要追加数据,不是新建一个excel,凭着感觉走,是先用read的api创建mut对象,修改之后吧read的book放到write中,执行回写,验证了一下,hmmm没猜错,这样基本上这个lib的使用问题得到了解决。

for循环

for循环应该是一门语言中最基本的部分了,依靠代码提示,这个没出太大问题,vec和map的循环,都顺利进行,但是拿到一个sheet有多少行之后,我要遍历的时候发现不会了,然后又是搜索引擎走一圈,找到了这个最简单的语法

1
2
3
for i in 1..sheet.get_highest_row()+1 {
....
}

唉,就如实说,光看书是真的不行,还是得coding才能掌握的说。

集合

本来以为集合是个简单的东西,没想到这上面也废了不少劲。简单总结一下:

  • Vec 的添加元素用push
  • HashSet的添加元素用insert
  • HashMap的添加键值对也用insert
  • HashMap的遍历调用iter然后for循环,得到的是一个元素,key是.0,value是.1
  • 迭代得到的元素是引用,如果要放入其他集合,我选择的是clone一份,直接解引用,经常出现所有权问题,暂时也没想到更好的办法。

字符串 (String和&str)

说真的,字符串这玩意还挺绕的,String和&str,就这两个来来回回的转换,我这也是偷懒了,自己接收也好,使用也罢,基本都用String了,得到了&str基本都转换成String,然后在传递了,最起码没有所有权问题了,偷个懒,后面在慢慢找更好的适配方法。

字符串 (字符串拼接)

我是怎么也没有想到,一个字符串拼接竟然愁住了我。
我从HashSet的iter遍历得到的item,类型是&String,我就想直接后面拼接个字符串,然后判断在另外一个集合中是否存在,然后这个追加愁住了我。

1
2
3
4
let name =element+"流";
报错:
cannot add `&str` to `&std::string::String`
string concatenation requires an owned `String` on the leftrustcClick for full compiler diagnostic
1
2
3
4
 let name = element.add("流");
报错:
cannot move out of `* element ` which is behind a shared reference
move occurs because `* element ` has type `std::string::String`, which does not implement the `Copy` traitrustcClick for full compiler diagnostic
1
2
3
4
5
6
let name =*element.add("流");
报错:
the size for values of type `str` cannot be known at compilation time
the trait `Sized` is not implemented for `str`
all local variables must have a statically known size
unsized locals are gated as an unstable feature

我自己尝试出来正确方法

1
let name = String::from(element).add("流");

再来看另外一个, 对于元组的引用,也是通过clone来解决的。。

1
2
3
4
5
6
7
 let mut map: HashMap<String, Vec<Model>> = HashMap::new();
...
for entry in map.iter() {
let sheet_name = entry.0.clone().add("流");
let sheet = book.get_sheet_by_name_mut(&sheet_name).unwrap();
...
}

全局配置

这个程序因为有个配置文件,运行过程中需要使用,开始想着声明一个全局变量就行了,准备放到dict这个mod里面,存储当然就用HashMap,然后就出现了问题

1
2
3
4
5
6
pub mod dict {
static mut map:HashMap<String,String> = HashMap::new();
}
报错:
cannot call non-const fn `HashMap::<std::string::String, std::string::String>::new` in statics
calls in statics are limited to constant functions, tuple structs and tuple variantsrustcClick for full compiler diagnostic

既然需要HashMap必须是const,我就试一下

1
2
3
const mut map:HashMap<String,String> = HashMap::new();
报错:
const globals cannot be mutable

不可变?那我怎么insert配置文件啊?先试试看非mut的

1
2
3
4
const map:HashMap<String,String> = HashMap::new();
报错:
cannot call non-const fn `HashMap::<std::string::String, std::string::String>::new` in constants
calls in constants are limited to constant functions, tuple structs and tuple variantsrustcClick for full compiler diagnostic

给我整不会了,看来HashMap不能全局使用了,从网上又查了一圈,hmm找到方法了,用lazy_static,具体原理还没看,但是尝试了一下,确实解决了我的问题。最终代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pub mod dict {
use std::{collections::HashMap, fs::File, io::Read, sync::Mutex};

pub fn get_product_name(product_name: String) -> String {
if let Some(value) = CONFIG_MAP.lock().unwrap().get(&product_name) {
return value.to_owned();
}
return product_name;
}
lazy_static! {
static ref CONFIG_MAP: Mutex<HashMap<String, String>> = {
let mut file = File::open("./data/cfg.toml").unwrap();
let mut data = String::new();
file.read_to_string(&mut data).unwrap();
let obj: HashMap<String, HashMap<String, String>> = toml::from_str(&data).unwrap();
let dict_map = obj.get("product_dict").unwrap();
Mutex::new(dict_map.clone())
};
}
}

数据类型转换

  • f64-> i32 (引申一下,应该数字类型转换都可以这么搞)
    • let a = num as i32
  • String -> i32 (引申一下,字符串到数字类型)
    • let a = String::from("13").parse::<i32>().unwrap();

总结

这个程序算是第一次正式用rust实现一个完整的功能的程序,说简单吧,也用到不少东西,说复杂吧,实际上就是数据格式转换+xlsx基本操作,后面还会继续用rust实现一些小的功能之类的,主要还是方便一下自己吧,这次代码量也就150+行,不过嘞,写了快一天。
编程这东西,还是不能停留在看,要实际coding才能掌握,后面如果要写一些系统底层的东西,相比现在可要复杂太多了,自己给自己加加油打打气吧~