基础语法以及文件操作
Rust
是一门 命令式编程
的语言,并且借鉴了一些 函数式编程
的概念。Rust
围绕于 代码安全
和 稳定性
的概念,这代表没有任何东西的类型可以是 Null
,Rust
不存在 Null
这个概念。同时任何有机会出错的代码都会建议你进行错误处理,导致程序发生错误并且崩溃的机会非常低。Rust
的 内存安全
不使用 垃圾收集器
来实现,而是靠 所有权
和 生存周期
实现的。并且在我看来,Rust
开发者在解决这个问题的同时,解决了所有我在意的问题。
本文仅作于我的学习笔记,不保证完全正确。
安装
要安装 Rust
,官方的 RustUp 就有下载链接以及linux
你需要运行的命令。
要注意的是,如果你电脑没有 VisualStudio
C++ 套件,你需要去安装一套,不然会编译失败。
创建项目
Cargo
是Rust
的包管理器,并且用语创建项目。
1 | cargo new <项目名> |
这会创建一个 Executable
的项目模版,而你也可以创建 Liberary
模块的项目模版。
在当前目录你就能看到src
文件夹里面有一个main.rs
文件,并且带有默认的Hello World
代码。
1 | fn main() { |
如果你来自其他编程语言,你可以很简单的理解println
这个“函数”是把 Hello, world
输出。
但你要知道的是,println
并不是一个function
,而是一个macro
,这些的名字都以!
结尾,并且是会生成一段代码。之所以println
是一个macro
而无法使用函数实现,这是因为rust
函数不可以输入未知数量的参数,而println
可以支持格式化字符串。
同时你还可以看到根目录有一个 Cargo.toml
,这里面是你项目的配置。
1 | [package] |
最后是 target
目录,乍看一下是一堆不知道是什么东西的文件,但是重点在于 target/debug/
目录中有你代码的可执行文件 kvstore
。
如果你运行这个文件,就会输出 Hello, World!
你可以使用一下指令编译:
1 | cargo build |
但是事实上,你一般不会这样编译并且手动执行程序,有个缩短的指令:
1 | cargo run |
如果出现 [error: link.exe not found]{.label .danger} 错误,这代表你没安装 VSC++
。
注意这是在debug
文件夹里面,而还有一个release
文件夹,这是因为这是一个debug
的编译。debug
的时候编译器不会去尝试最优化二进制文件,因此一般编译速度快,但执行速度比较慢。
而release
的时候编译器会尽可能的进行优化,编译时间长,但执行速度快。
有很多人接触rust
的时候都因为没在编译的时候加上--release
而觉得rust
不够快。
代码
在这将构造一个使用key设置和获取value的命令行工具。
获得指令参数
我们可以使用std::env::args()
获取指令行运行的参数。
::
在rust
里面代表下一级的意思,也就是std
里面env
里面的args()
。
1 | let args = std::env::args(); |
在这里,args
是一个iterable
,而一般操作系统的第一个参数为可执行文件的路径
,我们不需要这个,因此可以使用skip(1)
跳过一个项目。
1 | let args = std::env::args().skip(1); |
然后我们要获取第一个参数,我们可以使用next()
获取iterable
的下一个项目:
1 | let key = args.next(); |
但这时候我们就看到ide
标出了args
那行,看上去报错了。
这是因为我们需要让rust
知道args
是可变换的。与很多语言不一样,rust
需要你明确说明这个是mutable
,而大部分主流语言都是相反的,让你去说明哪些元素是不可变换的,在rust
所有元素默认为不可变换。
我们只需要加上mut
即可:
1 | let mut args = std::env::args().skip(1); |
在IDE可以看到,key的类型为Option<String>
,这代表这个key是一个可以是不存在的的字符串,没有字符串就会指向Option<None>
。
如果我们要让程序在没有输入的时候 panic
,我们可以使用unwrap()
来让程序崩溃。
1 | let key = args.next().unwrap(); |
这时候如果你不提供参数,就会崩溃并且看到
called Option::unwrap()
on a None
value
同样的我们也可以获取到第二个参数value
,并且把它们输出看看是否正确。
1 | fn main() { |
如果你想要一个更清楚的panic
信息,你可以使用Option::expect("信息")
而不是Option::unwrap()
。
文件读写
那么这时候我们应该怎么写入文件内容呢?
这时候我们就可以去rust官方文档里面看看,官方文档有个很好记的快捷域名 docs.rs/std
。
搜索之后可以看到 std::fs::File
似乎很合适。
然后再让我们看看 std::fs
是做什么的,看上去有一些很方便的函数。
看上去我们能用 std::fs::write()
来写入字符串到一个文件路径上。
我将会以 <key>\t<value>\n
的方式存储这些对。
那么我们可以通过 std::format!()
获取该字符串。 (这里有个!
因此是macro
)
1 | let contents = std::format!("{}\t{}\n", key, value); |
然后通过 std::fs::write()
来将字符串写入到 kv.db
中。
1 | std::fs::write("kv.db", contents); |
那么现在我们可以试试看:
1 | cargo run -- hello world |
这时候你就会发现有一个警告
this Result
may be an Err
variant, which should be handled
说write()
会返回一个Result
,而这个Result
有可能是错误的,因此应该被处理。
想一下,在写入文件的时候一定能成功吗?这是不一定的,你可能没有权限写入,或者写入失败。
在其他语言里面,你或许可以throw
一个错误,并且在另一个地方catch
到那个错误。
但在 Rust
里面,并没有 throw
错误并且 catch
的概念,如果程序 panic
了,那程序就会崩溃,就是这么简单。
那么我们看看这个 Result
到底是什么东西。
1 | let result = std::fs::write("kv.db", contents); |
从IDE可以看到,result
的类型为 Result<(), Error>
,这代表如果是成功的,就会是 ()
,错误了当然就是错误 Error
。
()
是一个 unit
,是一个空的元组,它的意思类似于其他语言的 void
,也就是不返回任何东西,只是代表成功了。
那么我们要怎么处理这些错误呢?我们可以使用pattern matching
模式匹配。Pattern match
像是其他语言里面的 switch
,但更加完善且强大。
1 | match std::fs::write("kv.db", contents) { |
在这里的意思是,如果为成功 Ok(())
就运行成功的代码块。
而如果失败,就把错误放到 e
里面,运行错误的代码块。
这像是 try catch
,但是 match
不止能用于错误处理,任何地方都可以使用。
但是与其优雅的处理错误,我们也可以像之前参数一样让错误发生时就panic
程序,让程序崩溃。Result
和 Option
一样都有 unwrap()
, 而功能也是一样,当错误发生时 panic
。
1 | std::fs::write("kv.db", contents).unwarp(); |
Structs
我们不希望整个程序都在 main()
里面,而是应该创造更多的抽象概念。做到这一点的第一步就是构造一个 struct
结构体。Rust
不是一个面向对象的语言,没有 class
的概念,你的类型通常就是一个 struct
。struct
本质上就是一堆键值,类型,数值的集合,你可以定义不同键值为不同的类型,并在创建的时候提供数值。impl
是 struct
的实现,先构造 struct
,然后使用 impl
添加函数。
让我们创建一个创建 Database
的 impl
。
1 | struct Database {} |
然后我们可以在 main()
里面调用 Database::new()
来获取实例。
new()
这个名字事实上只是一个惯例,这可以叫任何名字,只是官方就是用的 new()
然后想一下有什么数据结构适合我们需要的键值组合,那就是 Hash map
。
而很方便的, Rust
自带了 Hash Map
的实现。
1 | struct Database { |
在这里的 <String, String>
代表这个 Hash map
的键值都是字符串。
我知道这个程序将会需要多次的 std::collections::HashMap
,而我也不想每次都要写这么长一串,因为我们可以使用 use
导入。
1 | use std::collections::HashMap; |
那么现在有了个Database
,但这个 Map
是空的,我们应该在创建数据库的时候,解析数据库文件的数据,然后把那些数据放进 map
。
因此现在我们需要的步骤如下:
- 读取
kv.db
- 解析该字符串
- 导入
map
里面
读取数据
在上面 std::fs
中我们还可以看到一个函数, std::fs::read
。
但看一下这个函数的返回类型为 Result<Vec<u8>>
,是一个 unsined 8 bit integer
的向量,这明显不是我们要的字符串。
接下来看看 std::fs::read_to_string
,这回返回的类型就是一个字符串了。
我们可以使用之前的 match
,来进行错误处理
1 | let result = match std::fs::read_to_string("kv.db") { |
那么我们要怎么达成一个逻辑,如果成功取出content,失败就把错误返回让上层处理呢?
你可以看到第一行中有一个 let
,这是因为 match
事实上是可以有返回值到上层的。
基本上 Rust
所有东西都是 expression
而不是 statement
。
也就是说我们可以这样做:
1 | let result = match std::fs::read_to_string("kv.db") { |
然后我们需要返回错误,给上层处理。 这时候我们就需要用 Result::Err()
把错误包装起来,并且把返回类型改为 Result<Database, Error>
。
1 | impl Database { |
事实上,因为这个操作非常的常见, Rust
给了一个简化版的处理方案,你只需要加上 ?
就能达到一样的逻辑。
1 | impl Database { |
其实, ?
不止对 Result
有用,以后有遇到再说。
并且因为之前把返回类型改变为了 Result
,我们需要把 Database
也用 Ok()
包装一下。
同时在上面创建数据库的时候,加上 expect()
来让程序数据库出错的时候 panic
。
1 | use std::collections::HashMap; |
解析数据
现在我们获取到了文件的内容,接下来就是把它转换为 HashMap
。
首先第一件事是把行分开,一行就是一对的键值。我们可以使用 result.lines()
获取一个行的 iterator
,并且通过一个 for
循环获取行字符串。
这里的 result.lines()
事实上是懒的 iterator
,在调用的时候并没有已经预先计算好有的元素,而是需要的时候再计算,并不是一个向量或者列表。
然后使用 line.split_once('\t'')
用 \t
分割字符串。
接下来使用 map.insert()
把 kv 加入到 map
里。
1 | impl Database { |
但如果你现在运行,你会发现是不能够编译的。会出现
expected struct String
, found &str
的错误,那么它这个 &str
到底是什么类型呢,为什么 key
, value
的类型是它。
因为没有垃圾收集, Rust
需要一个概念,所有权
和借所有权
。
在 C
或 C++
里面,我们一般是在用完之后调用 free
来从 Heap
释放。
而在 Rust
里面,一般都不会用到 free
这种释放内存的方法,而是每一个内存中的信息都有一个所有者。
1 | let value = args.next().unwrap(); |
例如在这里,这个String
的所有者就是value
。当那个所有者跑出作用域以外的时候,它拥有内存就会被销毁。
在编译的时候,编译器知道什么时候这个 binding
会跑出作用域,并在跑出之后销毁。
1 | use std::collections::HashMap; |
那么理所当然的, map
拥有的那个 HashMap
,理应在第31行的时候跑出作用域并且被销毁,但事实却不是这样。
因为Rust
还有转移所有权的概念,这个 HashMap
被转移进 Database
,这 Database
然后被转移出作用域到第11行,最后在12行的时候跑出作用域。
然后这个 HashMap
就被释放了, HashMap
内的字符串也同样的被释放。
因此在你程序里面,你应该了解这个实例,现在的拥有权在谁手上。
1 | for line in result.lines() { |
那么回到 &str
的问题,result
的类型为 String
的原因是因为那是一个有所有者的字符串,但你在 for line in lines()
中的 line
并不是 String
的原因是那些是 result
的 view
映射。
因此当 line
跑出作用域之后,什么事都不会发生,因为 line
并不是行字符串的所有者,而只是一个指向那一行的指针而已。
因此在31行result
释放了之后, HasmMap
就不能够再使用 line
了。这也是为什么类型为 &str
。
再看看这个错误,现在这个错误的意思就是,
expected struct String
, found &str
我期望的是已经有所有的字符串,而不是字符串切片的 view
。HashMap
需要这个 String
的所有权。
解决这个问题的方法是调用 &str
的 to_owned()
,这会把 view
变成一个实际有所有者的 String
,然后就能成功 insert
了。