0.基本操作
1.使用Clion开发Rust
需要在系统的path里添加clion的bin路径,然后就可以通过命令行使用clion .来打开项目了。
2.使用cargo创建库项目
cargo new add –lib
3.打印地址
1 | fn main() { |
1 rust简介
1.1 基本特性
特性
运行时速度快
内存安全
并发
rust是一门安全的语言,表现在类型安全和内存安全(横向对比c/c++),同时性能也很好,因为没有GC(对比java),同时在设计的时候就考虑了多核处理器,支持并发,火狐公司的一个内核就是用rust写的,全并发执行。
rust采的命名方法是:蛇形命名法,也就是字母小写单词之间加下划线
rust的命令有rustc和rustup,rustc后面的c的意思是编译器。
1.2 命令行+vscode构建项目
步骤
- mkdir hello_rust创建工程目录
- cd h*进入目录
- code .用vscode打开该工程
1 | 常用的windows cmd命令 |
1.3 编写hello world程序
- fn表示函数声明
- rust采用的缩进不是tab,而是四个空格
- println!是rust的宏,也就是rust micro
- rust是预先编译的语言,也就是先编译好,然后生成二进制文件,可直接交给别人使用,而无需rust环境
- rustc只适用于简单的rust文件,用cargo
1.4 cargo创建工程
cargo是rust的创建及包管理工具
rust里代码的包称为crate
1 | windows cmd |
cargo.toml文件
1 | 1.toml是cargo的配置文件 |
顶层目录可放置的信息
1 | 1.README文件 |
当没有用cargo创建工程时,可以直接把文件拷贝到src下,然后再在顶层目录下编写一个cargo.toml文件即可
cargo.lock
1 | 1.负责追寻项目依赖的准确版本 |
使用cargo运行项目
1 | 1.cargo run会编译当前工程的main.rs及相关文件,然后再运行生成的exe文件 |
cargo check调试检查项目
一般在开发的时候都是用的这个命令来进行检查调试,因为更快。
只有要生成文件的时候才会使用run/build指令
如果要发布的话,使用cargo build --release,这样编译的时候时间更久,会进行优化,提高编译出来的程序的性能
2 rust基本语法
2.1 获取控制台输入
1.输入
std里提供了一个io,也就是标准输入输出,然后io里有一个关联函数叫stdin,是io里关于输入输出的输入那一部分,会返回一个句柄。然后stdin里有一个方法是read_line,读取命令行中的一行,这个方法可能抛出异常,所以该有一个except函数。
read_line会返回一个io::Result类型,也就是枚举类型,有两个值,一个是OK,另一个是Err,如果返回Err的话,就会中断当前程序,执行except那一部分。
1 | std::io::stdin.read_line(&mut string).except("exception message"); |
关联函数类似于java中的静态方法。
rust会有一些默认的preclude的类型,预导入。要想使用其它的类型,需要使用use引入外部包。
use std::io表示引入了标准库下的io这个包,该包下的所有类型都可以通过io::xxx来使用。
**io::stdin()**会返回一个句柄。句柄就是一个指针,可以是一个数或者其它内容,将OS或者数据库的某块内存关联起来,这是百度上的解释。下面是官方文档中的解释,可以结合参考。
1 | A handle to the standard input stream of a process. |
2.输出
println!宏
1 | println!("这是一个数字:{}", number); |
{}中的就是number。
2.2 添加外部依赖包rand(修改toml文件)
在cargo.toml中添加bin结点。
1 | [package] |
然后cargo就会自动下载依赖包了。
为什么会自动下载包?
其实,是因为打开了rust server,这样就会自动去扫描toml里的依赖,检查版本并且及时下载对应的版本。
如果我们关闭这个server,那么更新toml中的依赖,工程文件中的包并不会更新,因为工程文件会去lock文件中去找到并使用对应的版本。这个时候不仅要修改toml,还要进行生级,也就是输入指令:cargo update。
但事实上使用这个指令需要换源,因为直接用的话,会提示超时,因为下载的源好像是github,需要用steam++加速或者换源。
cargo.lock文件是一个版本锁,锁住当前项目使用的包的版本。就算更新了toml文件,包也不会更新,除非使用cargo update
这里引入了一个Rng,是一个trait。若想使用类型A,A实现了trait B,那么需要同时use trait和类型。
这里就是引入了Rng和thread_rng()随机数生成器类型。
2.3 使用枚举进行比较
进行比较需要用到std下的cmp中的Ordering这个枚举类型。
1 | use std::cmp::Ordering; |
进行枚举时需要注意一下两个问题:
- 类型一致
- 使用时大小等都得写全
1 | let num1: i32 = 1; |
2.4 处理异常
前面我们使用的是except方法来处理异常,如parse将字符串转换成数字时,会返回一个Result,根据这个类型是OK还是Err来判断是否执行except中的内容。如果Result中判定为Err的话,会直接中断当前程序,然后程序结束(崩溃)。这样我们的程序一遇到非法输入就崩溃,并不健壮。
所以这里我们用了上一节中的match模式匹配来处理这个问题。如果是OK的话,就执行ok的代码块,如果是Rrr的话再做相应的应对措施(如提示用户重新输入)
1 | let num: u32 = match num.trim().parse() { |
下面是完整的猜数字代码:
1 | use rand::{Rng, thread_rng}; |
3 变量及控制流
3.1变量
3.1.1 不可变变量
使用let关键字声明,将等号右边的值绑定到等号左侧。
1 | let num = -1; |
3.1.2 可变变量
mutable,可变化的,还是使用let声明
1 | let mut num = 1; |
3.1.3 常量
常量用const声明(constant的意思),常量必须显式声明数据类型,无法自动推断。
1 | const MAX_LEN: u8 = 64; |
其实工程中大多数都是不可变类型的变量。
3.2 shadow机制
变量可以被隐藏。如下:
1 | let string = "ssss"; |
此机制是为了避免以下情况:
1 | String name_string = "jack"; |
隐藏变量在在作用域外被定义,并绑定值后,在另一个作用域中被shadow后,出了此作用域,隐藏的变量会恢复。
1 | fn main() { |
ps:shadow机制的实际意义是,重新创建了一个新的变量,这个变量可以有新的值,而它的意义就是既能够进行变量名称的复用,也能够不增加新的变量。
3.3 标量类型
rust是静态数据类型,在编译时就需要所有变量的具体数据类型。并且rust提供了类型推断机制,根据值的类型和具体的使用情况,可以推断出变量的类型。若是不能推断出,编译器便会报错,需要我们给出更多的信息(一般是需要显示声明了就)。如下:
无法推断出字符串会转换成什么类型。所以需要我们显示声明:
3.3.1 整数
整数的类型:
其中isize和usize和机器的位数有关,一般不用。
整数的字面量表示:
1 | let adr = 0x1234_5678u64//无符号64位16进制数 |
整数的默认类型一般是i32,比较快。
下面是整数溢出的情况:
- 调试模式下:会发生panic(恐慌)
- 发布模式:不发生panic,选择环绕操作,即256=0
3.3.2 浮点
两种类型:
- f64
- f32
一般是采用f64
3.3.3 bool类型
true或者false。
占用大小一个Byte
为什么不用一个bit?因为如果用一位的话不利于存储,会产生内存碎片。
3.3.4 字符类型
char
1 | let yeye = '👴'; |
3.4 复合类型
3.4.1 元组Tuple
每个位置对应一个类型,类型不必相同。
1 | let tp = (1, "sss", "S"); |
元组赋值
1 | let tp = (1, "sss", "S"); |
3.4.2 数组
和其他语言类似
1 | let a = [1, 2, 3, 4]; |
3.4.5 Vector
大小可变,用的更多。
3.5 函数
函数声明,函数名,参数列表,返回值。这是声明函数的全过程。
代码块里的最后一行没加分号,代表是返回值。
1 | fn main() { |
或者这样也行:
1 | fn add(x: i32, y: i32) -> i32 { |
3.6 if-else
第一种用法:
1 | let cdt = true; |
第二种:
1 | let cdt = true; |
如果ifelse嵌套太多,使用模式匹配吧。
1 | let cdt = false; |
3.7 循环
3.7.1 loop
1 | let mut count = 0; |
3.7.2 while
1 | let mut count = 0; |
3.7.3 for - each
1 | let arr = [1, 2, 3, 4, 5]; |
1 | let arr = [1, 2, 3, 4, 5]; |
60s倒计时
1 | for e in (1..61).rev() { |
4 所有权
rust采用所有权系统来管理内存。并且,是在编译时检查,这样就不会减慢程序运行的速度。无运行时开销。
4.1 栈内存与堆内存
- 堆栈:LIFO,last in first out,后进先出
- 堆:OS给用户在heap上找到一块足够大的区域,标记为在用,然后返回给用户。这就是在堆上分配内存。
堆是通过分配来得到内存,而栈不同,栈是直接将数据存放到那一个格子就行了,不需要分配。
栈上分配的内存是固定不变的,如数组。而堆上分配的内存可以动态变化,也就是可变数组,像C语言里的动态内存分配,就是在堆上分配空间,然后返回一个指针给用户(malloc函数返回指针)。而这个指针由于是固定大小,所以可以存到栈上去。
在堆上分配空间更慢,因为OS需要找到一块足够大的空间。而在栈上就比较快了,因为这个空间肯定在栈的顶端。
在堆上访问数据也慢,因为需要通过指针寻址来访问,是间接访问,需要跳转,从栈->堆,比较慢;而从栈上访问数据就不一样了,因为是栈->栈,所以快。
4.2 所有权规则
简化:在一个时间内,每个值有且只有一个变量,并且当所有者超出作用域时,所有者及其值将被删除。
4.3 初识String
之前的标量数据类型都是存储在stack上的,一旦离开作用域就会被弹出。
而String是一种存放在heap上的数据类型。
String可以代表std中复杂的数据类型,或者是我们自己创建的数据类型。
在程序运行中,有两种字符串:
- 字符串字面量:是不可变的。在程序运行之前,即在编译期间,就可以知道其内容了,所以直接硬编码到可执行文件中了。所以在运行期间就不需要额外的内存了,高效。
1 | let s = "hello"; //hello就是一个字符串字面量 |
- String类型:可变的,如获取用户的输入是,是不可预知的,用的就是String。String是在运行期间才会在heap上分配内存,通过from函数向OS申请内存。然后变变量超出作用域后,救会通过drop函数回收内存(自动的)。
1 | let mut s = String::from("hello"); //从字符串字面量创建一个String类型 |
下面解释一段程序:
1 | let s1 = String::from("hello"); |
第一行向heap申请了一块内存。具体是如下:在堆上申请了一块空间,存放hello字符数组,然后返回这个字符数组的三个信息:起始地址,长度,容量。返回给s1接收。
第二行是将s1的指针考培给了s2,包括heap指针,len和capacity。然后按照常规的思路,s1 s2都离开作用域时,都会进行drop回收内存。这样一块heap内存被回收了两次,是不安全的。
为了解决这个,在将s1指针拷贝给了s2后,也就是MOVE操作后,s1的内容被废弃,再次调用将出现报错。然后s1s2离开作用域后,只有s2指向的heap会被drop掉。
这样无疑更安全,也不会在堆上重新分配空间。
以上其实是一种浅拷贝,然是由于s1时失效了,于是创建了新的术语叫MOVE。
- 浅拷贝 – MOVE移动
- 深拷贝 – CLONE克隆
Rust所有的操作都是廉价的浅拷贝操作,不会开辟新的heap内存,除非是这样要求的。
下面的深拷贝,即克隆的操作。
而在栈上进行的MOVE,先前声明的变量就不会失效。
1 | let x = 1; //useful |
可以用下面两个概念来解释:
- Copy trait(复制特性):实现了Copy trait的数据结构,在赋值后旧的变量仍然有效。
- Drop trait(回收特性):实现了Drop trait的数据结构,不能再实现Copy trait
4.4 函数与所有权
将值窜给函数,要么会发生移动(Move),要么发生复制(Copy)
- copy trait的数据类型被传入时(i32):传进去的时副本,在函数结束的时候,副本会被弹出stack
- drop trait数据类型被传入时(String):传进去后,旧的变量丧失所有权,回收时不再使用drop清理heap内存。传进去的数据获得所有权,在函数结束时弹出堆栈并且使用drop回收堆内存
如果想即使用所有权,还能返回回来的话,可以使用元组
1 | fn main() { |
4.5 引用
引用:引用数据的值而不使用其所有权。&符号表示
引用分类:
- 不可变引用:不能修改指向堆上的数据
- 可变引用:可以修改指向堆上的数据
- 悬空引用:引用指向的数据已经被释放,而引用依然有效(Rust在编译期杜绝了这个问题)
有以下规则:
- 一个作用域内只能有一个可变引用
- 一个作用域内可以有多个不可变引用
- 同一个作用域内可变引用与不可变引用不能同时存在
可以看到:先声明了俩不可变的引用,然后声明了一个可变引用。如果不对不可变引用做操作的话,不会报错。如果在声明了可变引用后,还对不可变引用进行操作,这样就会报错。
4.5 切片
1 | fn main() { |
上面的代码是获取第一个空格所在的位置。
bug:当字符串被清空了后,得到的index不会发生改变,也就是同步性的问题。要保证:在字符串改变的同时,这个index也会同步改变。这很困难。
rust可以采用切片解决这个问题。
1 | fn main() { |
因为函数里面采用了字符串切片,也就是不可变引用,所以修改时报错。
注意:字符串切片仅仅针对UTF-8的字符,两字节的汉字会报错。
还做了一个优化,把字符串引用修改成为了字符串切片,这样就能同时接收两种类型了(&String -> &str)。如字符串字面量(&str)和String类型。
数组也可以切片,和上面类似。
5 结构
5.1 结构定义
三类:
- struct:普通结构
- tuple struct:元组结构,当你想给元组起名字的时候,使用它。
- Unit - like - struct:无任何字段的结构
普通结构:
1 | struct User { |
元组结构
1 | fn main() { |
5.2 结构实例
1 |
|
Rust中有类似与java的toString方法,或者说是trait,但是默认没有实现,所以我们就用的是debug特性。
- #[derive(Debug)]:实现debug trait
- {:?}:输出结构的信息,不换行
- {:#?}:输出结构的信息,换行
5.3 struct方法
两种:
- 方法:方法用impl定义的块去实现,需要传递它本身(或本身的引用)
- 关联函数:不需要传递本身,只是跟这个结构有关联,类似静态函数。
1 |
|
6 枚举与模式匹配
6.1 定义枚举
rust的枚举很强大。可以自定义枚举并存储数据,不需要消耗额外的结构体。
1 | enum IpAddrKind { |
枚举也可以定义方法。与结构体相同使用impl
1 | enum IpAddrKind { |
6.2 Option枚举
1 | enum Option { |
1 | fn main() { |
6.3 match
math允许一个值与一系列的模式进行匹配,并执行匹配上的代码。
1 | enum Week { |
上面通过枚举存储了数据(String),并且通过模式匹配,将数据与statement绑定,重现了数据。
match必须列举所有的值进行匹配,若值太多了,使用_来代表其他的。
1 | enum Week { |
6.4 if-let语法糖
1 | enum Week { |
等价于:
1 | enum Week { |
7 package crate module
7.1 定义
自顶向下:
Package:通过cargo可以创建一个新的包,位于最顶层。
Crate:cargo创建完包后,下面的.rs文件,有的会生成binary二进制文件(main.rs就是默认创建的binary文件,crate root),有的是产生library(其它的非main.rs文件)。Crate只能是以下两种类型:
- binary
- library
Module:在一个.rs文件中,可以定义多个module。
Path:
还有一个概念叫crate root,是.rs源代码文件,编译器从这里开始组成我们的Module文件。
下面是Package的描述:
crate可以把相关的功能整合到一个作用域内,还可以避免命名冲突。
Module是在一个crate内,将代码进行分组,可以复用,并且可以控制代码的权限(pub or pri)。mod还是可以嵌套的。
7.2 权限
同级可以互相调用,父级不能调用子级的私有,子级可以调用所有父级的,无论暴露与否。外层mod加上了pub,里层的函数没加,函数依然是pri的。所以外面的mod里面的fn都需要加上pub才行。
1 | mod father { |
访问函数可以通过绝对路径和相对路径。建议绝对路径。
- 绝对路径:
crate::father::son_1::fun1();
- 相对路径:
father::son_1::fun1();
- 子级在调用父级的函数时,可以通过
super关键字
7.3 结构的权限
结构默认为pri,并且结构的字段也是pri的,如果像设置乘公有的,加上pub
1 | mod father { |
7.4 枚举的权限
枚举前面加上pub后,其里面的枚举变体自动变成公共的了。
7.5 use的使用
use
针对函数,use一般时引用它的上一级mod,而不是直接引入到函数本身,这样增强代码的可读性,避免函数冲突了。
而针对结构struct,enum的话,就是引入到本身,而不是父级条目。
1 | pub mod father { |
对于有同名的数据结构,有以下两种做法:
- 像函数一样,引入到父级条目下就停止,不到该数据结构。
- 可以使用别名,用as,指定一个别名。
可以看到报错了。
可以写成如下:
1 | use std::io::Result as IoResult; |
pub use
使用use导入mod后,这个mod对内部作用是可见的。而对如果外部去调用这个函数,对这个mod是没有访问权限的。所以这个时候可以使用pub use,这样这个模块对外部也是可见的了。
pub use 意思是重导出。
7.6 特殊的use使用
- 一次引入同一级下的多个包。
1 | use std::io::Result as IoResult; |
- 用self代表它自身
1 | use std::io::Result; |
- 通过通配符*引入全部的包(不建议经常使用),一般用于 以下情况
- 测试:将所有的公共条目引入测试test模块
- 预导入
1 | use std::io::*; |
7.7 引入自己写的mod
1 | mod my_lib; |
如果mod的嵌套太多,可以创建一个新的同名文件夹,然后里面建立子mod的同名.rs文件。
可以用mod和use一起作用。
1 | //main.rs |
树形结构:
1 | src |
8 集合
集和是建立在heap上的数据,因此在编译时不需要去确定大小,在运行时会自动变化。
8.1 Vector
1.创建
有两种方式创建Vector
- 通过关联函数,这种情况需要显示的指明类型。
- 通过已有的值来创建
1 | fn main() { |
rust有上下文推断机制,如果前面没有明确Vec的类型,这时会报错;然后后面添加了元素,又能够自动推断出类型了,报错会消失。
2.更新
我们使用第一种方式创建Vector,并且向里面添加元素。
1 | fn main() { |
3.清理
一般而言,离开作用域后,Vector就会被OS调用drop给清理掉。
4.获取
两种方法:
- 索引:得到的是数据本身
- get方法:得到的是Some(T)或者None,其中T是不可变引用。
get更安全,可以对得到的数据进行类型判断,如果是Some就取出,是None就不取出,提示错误。
而索引的话,就会出现panic,程序恐慌。
1 | fn main() { |
因为Vec在heap上,所以有所有权的借用,用get得到的是引用,用索引的到的是本身。但是借用两边并不会报错(i32),说明是存放在栈上的,copy和move都一样。这个地方是错误的,因为i32实现的是copy trait,所以使用等号时会在栈上压栈一个相同的数据。自然不会有所有权的问题。
1 | fn main() { |
下面修改成String类型的试试。
1 | //cannot move out of index of `std::vec::Vec<std::string::String>` |
错误信息如上。String没有实现copy trait,而是drop trait,如果操作成功,原来Vec里面对应位置的数据就会失效,所以只能采用借用,也就是用引用来获取。
而get默认的就是得到一个引用,用索引的话需要加上引用符号。
下面我们先通过索引加引用符号得到一个不可变引用,然后再添加一个元素进去,最后再打印这个不可变引用指向的值。
1 | fn main() { |
原因是什么呢?
- 不可变引用与可变引用不能同时存在。
- Vec的机制,因为在堆上分配的空间,所以空间可能不足,需要重新分配空间,然后进行一个数据的迁移,最后释放掉原来那部分的空间。如果发生这种情况,上面获取的不可变引用,它的指向是不会改变的,这样就指向了一片空的内存,是不安全的。所以编译器不允许这样的情况发生。
然后再次试了一下使用Vector存放i64,放很多数据。然后发生了栈溢出。
1 | /**memory allocation of 8589934592 bytes failed |
换成String也会发生溢出。
然后查阅了相关资料,确定Vec是存放在栈上的。
5.遍历
通过for循环遍历,通过解引用*更新。
1 | fn main() { |
8.2 enum & vector
存放时直接存放就行,取出时需要根据类型取出。
1 | enum InputKind { |
8.3 String类型
1.创建字符串
两种方法:
- 字符串无初值,使用new关联函数。
1 | let s = String::new(); |
字符串有初值:
- 使用关联函数from
1
let s = String::from("hhh");
- 使用to_String方法
1
let s = "hhh".to_string(); //这里的string是小写
2.更新
3.访问
不支持索引访问,只能用切片来访问。
原因有两个:
- UTF8编码,一个Unicode值对应的字节数不是固定的。
- 索引操作应该消耗O(1)的时间复杂度,但是String无法保证。
1 | fn main() { |
4.内部表示
String时对Vec
有一个len方法返回的是它的字节数。
String有一个大坑,String里面存储的是字节,但是字符都有它的Unicode标量值,一个Unicode值不一定就是一个字节。如下:
- 汉字:1 Unicode – 3 Byte
- 英语:1 Unicode – 1 Byte
- 印度:1 Unicode – 2 Byte
这个时候我们访问,用字符串切割也要看响应的场景了。
Rust里有三种看待字符串的方式(自底向上):
- 字节
1 | fn main() { |
- 标量值
1 | fn main() { |
- 字形簇:标准库里没有提供。
5.字符串切片的坑
使用[min..max]来进行切片,从[min, max - 1]
如果切割的不是完整的Unicode编码,不会报错,但会发生恐慌
1 | fn main() { |
6.遍历
遍历在内部表示中提及。
8.4 HashMap
1.创建及插入
- 引入包
- new创建
- insert插入
1 | use std::collections::HashMap; |
HashMap是同构的,所有的key是一种类型,value也是一种类型
上面是常规的创建方法,还可以使用tuple来创建。
1 | use std::collections::HashMap; |
先创建两个Vector,然后用一个vec生成一个迭代器,再跟另外一个vec的迭代器进行一一映射。然后再用collect方法打包返回一个hashmap。
那个<_, _>是会自动推断的,但是不可以省去。
向HashMap中插入数据时,如果数据是实现了copy trait的话,数据会被复制一份。如果是是西安了drop trait的话,数据会被一觉,所有权也会转移,源数据也会失效。
但如果插入的是引用,就不会发生所有权的移交了。
下面是通过get获取map中的值。
1 | use std::collections::HashMap; |
1 | use std::collections::HashMap; |
2.遍历
使用元组tuple和for-each进行遍历
1 | use std::collections::HashMap; |
3.更新
当向map中插入数据时,可能有三种情况:
数据不存在,直接插入即可。
数据存在
- 忽略原来的数据v,用新的v替换掉它 – insert()方法就是这样的
1
2
3
4
5
6
7
8
9use std::collections::HashMap;
fn main() {
let mut m = HashMap::new();
let s = String::from("qcy");
m.insert(&s, 0u8);
m.insert(&s, 99u8);
println!("{:?}", m);
}
//{"qcy": 99}- 保留现在的v,忽略新的v – 使用entry来判断是否存在,用or_insert(v)方法来插入,如果k不存在,执行;存在,不执行。
1
2
3
4
5
6
7
8
9
10use std::collections::HashMap;
fn main() {
let mut m = HashMap::new();
let s1 = String::from("qqcy");
let s2 = String::from("qcwdfg");
m.insert(&s1, 1u8);
m.entry(&s2).or_insert(122u8);
println!("{:?}", m);
}
//{"qqcy": 1, "qcwdfg": 122}- 合并旧的v和新的v – 还是使用or_insert(v),来判断,若k存在会返回一个k的可变引用,可以对k进行修改。
1
2
3
4
5
6
7
8
9
10
11
12use std::collections::HashMap;
fn main() {
let mut map: HashMap<String, u32> = HashMap::new();
let text = String::from("h h h h h a a x c v b g r e qw s f gf g h h ");
let res = text.split_whitespace();
for i in res {
let num = map.entry((*i).to_string()).or_insert(0);
*num += 1;
}
println!("{:?}", map);
}
//{"h": 7, "b": 1, "x": 1, "r": 1, "e": 1, "f": 1, "gf": 1, "g": 2, "s": 1, "c": 1, "qw": 1, "v": 1, "a": 2}
4.Hash函数
一般默认情况下:
- 可抵御Dos攻击 – 良好的安全性
- 并不是最快的 – 性能一般
若是觉得性能不好,可以修改trait
9.错误处理
1.不可恢复的错误与panic!宏
大多编程语言在错误处理这方面提供了异常机制,没有对可恢复错误与不可恢复错误进行区分,而Rust没有异常机制,但他对错误处理进行了分类:
- 可恢复错误:如文件找不到,可再次尝试
- 使用Result<T, E>
- 不可恢复错误:如Vec越界访问
- 使用panic!宏进行处理
针对不可恢复的错误,我们有两种处理,展开或终止(abort)调用栈。
- 展开调用栈:Rust沿着调用栈往回走,清理遇到每个函数中的数据。
- 终止调用栈:直接终止程序,不进行清理。但是需要由OS来清理。
若想二进制文件更小,需要将默认的展开改成终止。
具体就是在cargo.toml中设置profile。
1 | [package] |
main.rs
1 | fn main() { |
or
1 | fn main() { |
顺便再次复习一下,v.get(999)并不会报错,得到的返回值是None
2.Result枚举与可恢复的错误
执行文件操作会返回一个Result的枚举变体,操作成功为Ok(T),失败为Err(E)
1 | use std::fs::File; |
下面是针对不同的错误,通过match做的一些处理
1 | use std::{ |
3.unwrap与expect替换match
上面我们用了很多match,代码的可读性还行,但太臃肿了。
下面提供一种unwrap()方法。
使用unwrap打开文件
- 如果文件不存在,程序恐慌
- 如果文件存在,返回文件
1 | use std::fs::File; |
使用unwrap创建文件
- 如果文件不存在,创建文件
- 如果文件存在,返回文件
1 | use std::fs::File; |
但是有一个缺点,unwrap无法定位错误信息(所有unwrap返回的错误信息都是一样的),所以下面介绍expect
expect与unwrap一样,单数可以控制输出的错误信息,精确的定位到处错在哪一行。
1 | use std::fs::File; |
4.通过函数将错误返回
将函数的返回值设置为一个Result枚举类型。
1 | use std::fs::File; |
5.语法糖:”?”
?:执行一个操作
- 如果是Ok的话,就把Ok里的值作为结果绑定到变量。
- 如果是Err的话,就直接返回错误(注意main函数没有返回值,所以如果要使用”?”的话,需要加东西)。
1 | use std::fs::File; |
删掉注释后效果如下:
1 | use std::fs::File; |
非常精简。
然后再进行链式调用:
1 | use std::fs::File; |
6.main函数中如何使用”?”运算符
main的返回类型是(),也可以修改为Result,T对应的是(),E对应的是任意可能的错误类型(其实是一个trait对象7.)。
1 | use std::error::Error; |
7.何时使用panic!
总体原则如下:
- 尽量使用Result!将错误返回到代码的调用者,让他们决定如何去处理,如果我们觉得这个错误除了panic!,没有其它的解决办法,就直接使用panic!吧
10.泛型,trait,生命周期
1.泛型
Rust对类型的命名采用的是驼峰命名而非蛇形命名。
1 | pub struct DogAnimal {//...} |
泛型的声明
- 结构
1 | pub struct Good<X, Y> { |
- 函数
1 | fn hhh<X>() { |
- 枚举(Option
和 Result<T, E>)
1 | enum Option<T> { |
- 方法
注:针对具体的方法,impl后不需要接收泛型。如果是泛型方法,那么就需要
1 | fn main() {} |
使用泛型并不会影响性能。因为Rust使用了单态化,也就是编译的时候会将具体的类型带入到泛型参数里去,从而在运行时不需要额外的开销。
2.trait
- 类似于接口,告诉编译器哪些类型可以具有相同的功能。
- 还有一个trait bound的特性:要求传进来的泛型参数必须实现了对应的trait
trait的产生和接口是类似的,有些不同的类型会实现相同的方法。所以我们就把这些方法提取出来,实现一个trait。
1.定义
如下,只有方法签名,无具体实现。
1 | pub trait Behavior { |
2.实现
my_lib.rs
1 | pub mod my_struct { |
main.rs
1 | use fanxingggggg::my_struct::{ |
trait也可以使用默认实现,也就是在trait的定义时就实现trait。如果结构对trait的默认实现进行了重写的话,就不能再调用默认实现了。
3.将trait作为参数
- 参数类型为iml trait
1 | pub mod my_struct { |
- 使用trait bound
1 | pub mod my_struct { |
- 在返回类型后使用where
1 | pub mod my_struct { |
3.生命周期
定义:让引用保持有效的作用域。
Rust有一个东西叫做借用检查器。会在编译的时候比较两个引用的生命周期的长短。
1.简单使用
当你写了一个函数时,向里面传了多个引用,然后对其进行一系列操作,最后返回一个引用时。需要用到生命周期。
因为编译器需要确保传进来的生命周期,与传出去的生命周期一样,或者说大于。
1 | fn main() { |
目的检查非法调用。
实际返回结果的生命周期是两个参数中生命周期较小的那一个。
我们看下面这个错误调用。
通过函数,res的生命这些周期被缩短到和s2一样了。这样在外部继续调用的话,就会发生错误。
再试试Java里的效果。
1 | package com.example.reptile; |
res的值和s2是一样的,这意味着s2并没有被GC回收掉。
2.函数的生命周期
函数的返回值的生命周期跟输入的参数的生命周期有关。
如果要返回一个引用,需要确保这个引用不会被回收(即不是本地变量)。
1 | fn fun<'a>(s1: &'a str, s2: &'a str) -> &'a str { |
返回的引用指向的堆已经被drop掉了,所以不行。这个引用叫悬垂指针。在Rust里,只要提供了足够的信息(生命周期),就不会发生这种情况。
如果想要使用在函数里的变量,建议返回一个String,移交所有权,而不是返回一个引用。
1 | pub fn new(arg: &Vec<String>) -> Result<Config, String> { |
3.struct的生命周期
与函数类似:
1 | struct Man<'a> { |
这里生命周期的意思是:字段存活的时间必须比结构久,不然的话字段先被回收了,结构还在,就会发生内存泄漏。
即绑定给name的数据的生命周期的存活时间必须要覆盖这个结构的生命周期。
4.生命周期省略的规则
- 每个引用类型的参数都有自己的生命周期
- 如果只有一个输入生命周期参数,那么这个生命周期参数将被赋给输出生命周期参数
- 如果有多个输入生命周期参数,但是其中之一是
&self or &mut self
,那么self的生命周期将被赋给输出生命周期参数。
5.静态生命周期
静态生命周期用`static,表示,意思是比那辆的存活时间和程序的存活时间是一致的。也就是说编译器在编译的时候就把这一部分作为二进制值写进去了。
只有实现了copy trait的变量才可以声明static
11.测试
测试三个步骤,3A
- 准备数据
- 运行测试代码
- 断言结果
1.编写测试
在函数的上方加上 属性(aattribute)#[test]
1 |
|
2.运行测试
使用cargo test命令
3.断言的作用
1.assert!
断定此处为true!
可以接收一个bool类型,true通过,false则panic
1 | #[test] |
2.assert_eq!
断定两个同类型的变量相等!
比如下面的assert_eq!,就是表明括号里面传入的参数肯定相等,不然就会报错。
1 |
|
还可以接收字符串。
1 |
|
3.assert_ne!
与assert eq相反,ne的意思是not eq
4.给断言添加自定义消息
其实assert宏还有另外一个参数,可以传递字符串,而事实上这个字符串最终会传递给format宏。所以这个字符串里面可以添加占位符{},并且后面可以带参数。
1 |
|
4.属性should_panic
在测试下面,函数上面再添加一条属性(attribute),叫#[should_panic]
表示下面的测试函数应该恐慌,不恐慌测试就不会通过。
1 |
|
可以通过在should_panic(expected = “”)添加参数,让测试更加精确一点。如添加了字符串参数,然后如果恐慌信息里包含了这个expected参数,那么就测试通过;反之,如果不包含,那么测试失败。
1 |
|
5.使用Result枚举来进行测试
无需panic。
1 |
|
12.命令行项目
实现这样的功能:通过命令行,向程序中输入参数,一个是字符串,一个是文件绝对路径。然后找到这个绝对路径中跟字符串内容匹配的部分,并且打印出来。
1.接收命令行参数
使用std::env下的args接收参数,并调用collect方法,返回一个Vec
1 | fn main() { |
2.读取文件内容
使用std::fs下的read _to_string 来读取,会返回一个result,所以我们调用except来处理。
1 | use std::env; |
3.代码重构
遵循一个函数一个功能的原则,main函数现在太臃肿了。
选择将main.rs拆分成main.rs和lib.rs,将业务逻辑的实现放在libl里。
具体如下:
- 在main.rs里编写全部功能,可以忽略重构,忽略错误处理,只考虑理想情况。
- 将实现功能的业务逻辑抽取出来,独立成单个的函数。
- 在函数中进行错误处理,或者返回一个Result让main去处理。
- 最后将抽取出来的函数移动到lib里去
1 | use std::{env, fs, process}; |
1 | use std::error::Error; |
4.使用TDD在lib里进行查错
TDD:test driver development,测试驱动开发
测试部分
1 |
|
被测试的函数:
1 | pub fn search<'a>(query: &str, content: &'a str) -> Vec<&'a str> { |
以后使用TDD来进行测试。测试样例与功能实现分离开。
5.使用环境变量进行选择
方法如下:
程序中:
1 | let flag = std::env::var("IGNORE_CASE"); |
命令行中:IGNORE_CASE = 1 cargo run
这样就可以在程序中读到环境变量了。
6.进行错误信息定向输出
我们可以使用cargo run > output.txt 运行使得通过println!输出的内容定向到output.txt里面。
但是这样错误信息也输出到output里了。
可以使用eprintln!,将错误信息定向到控制台输出。
13.迭代器 闭包
1.闭包
1 闭包的定义
定义:可以捕获其所在环境的匿名函数。
闭包是一个匿名函数,他是将一个函数的定义存放在一个变量中去,而不是函数的执行结果。这个闭包只有在遇到向里面传输参数的时候,才会去执行函数,得到返回结果。
闭包并不需要显式声明它的参数和返回值类型。因为闭包是在当前作用域内工作的,范围狭小,不是作为接口去调用的。
1 |
|
将闭包绑定给变量后,变量的类型就是:variable bi_bao: fn(<unknown>) -> <unknown>
2用结构来存储闭包
1 | struct Cashe<T> |
上面结构中的calculation,是一个泛型参数T,并且加上了限制,要求这个泛型参数是实现了Fn trait的一个闭包。
但是只能存储一次。
可以用hashmap来进行改进。
1 | use std::collections::HashMap; |
3.使用闭包捕获外部变量
闭包可以捕获和他定义于同一个作用域的变量。这是闭包独有的功能,而函数是没有的。
但是会产生额外的内存开销。
4.闭包的trait
- Fn 不可变借用
- FnMut 可变借用
- FnOnce 取得所有权
可以使用move关键字将闭包外的所有权强行移动到闭包内。
1 | fn main() { |
2.迭代器
1.iterator trait
实现next方法即可。
2.几个迭代api
- iter:在不可变引用上创建迭代器
- into_iter:创建的迭代器会获取所有权 – 用一个数据引出迭代器,并且夺取了元数据的所有权。
- iter_mut:迭代可变的引用 – 可以通过解引用修改其中的值。
3.消耗/产生 迭代器
1.消耗(消耗性适配器)
消耗迭代器:当调用迭代器的next方法时,会消耗迭代器,迭代器中的元素会被一个一个消除掉,这就是叫消耗的原因。rust里有些方法(针对于实现了iterator trait的类型),会自主调用next,从而消耗迭代器。
1 | fn main() { |
要调用sum这个方法,self必须实现Sizd这个trait,并且泛型参数必须实现了Sum这个trait。
1 | /* |
2.产生(迭代器适配器)
将一个迭代器转换成另一个迭代器。
1 | fn main() { |
map()方法:一个泛型方法,接收两个泛型参数,目的是将传进来的类型T更改成传出去的类型F。接收两个参数,第一个是self,要求实现了Sized这个trait。第二个参数是T,要求实现FnMut这个trait,也就是一个闭包,并且是可变引用,因为要对参数进行修改。
调用map方法后,迭代器的所有权被Move。
3.迭代器+闭包 捕获环境
使用filter()这个方法,一个迭代器适配器。接收一个闭包,这个闭包必须返回bool类型。若是返回true,则元素被加到迭代器里,最终作为这个方法的返回值。
1 | fn main() { |
传进闭包后,要用collect进行收集,返回一个集合,不然闭包是不会执行的
4.构建自定义的迭代器
实现Iterator trait即可。
1 | fn main() { |
迭代器比for循环遍历要快一点。
用filter方法。
14.发布
15.智能指针
1.Box< T >
Rust中所有的类型在编译时都会知道大小。
考虑C语言中的链表的实现,如下:
1 | struct Node { |
有两个类型,一个是int,一个是指针。这实际就是一个递归,只不过递归的是指针,并不是结构,如果是结构的话,就永远无法知道声明一个struct的时候应该分配多少大小了。所以用的是指针。指针存放在栈上,就是一个地址而已。
rust也是类似的。
1 | fn main() { |
Box这个类型就是一个智能指针,实现了两个trait,deref的目的是确保Box可以被当成一个引用实现;drop则是确保Box在离开作用域时,其指针(栈内存)和指向的数据(堆内存)都会被释放。
2.deref trait
1.用法
实现这个trait后,可以确保类型被当成引用来使用。
1 | fn main() { |
2.实现Deref trait
给结构实现deref trait即可。
具体的话,就是指定一下类型,然后实现deref方法即可。
1 | use std::ops::Deref; |
3.deref coercion
假设实现了deref trait,然后传入的是引用,那么编译器就会自动调用deref方法,将&Box
如下:
1 | fn main() { |
首先将&MyBox
并且,所有的这些操作,都是在编译期完成的,不会产生额外的运行时性能开销。
3.drop trait
变量在离开作用域时,会自动调用drop方法,来释放相关的资源。
不可以提前调用drop trait的drop方法,但是可以提前释放资源,使用另外一个drop方法。
1 | fn main() { |
4.Rc< T >
引用计数智能指针
1 | fn main() { |
为什么要有Rc
把Box
使用Rc就不一样了,如果函数接收的是Rc
使用了Rc
Rc使用的是不可变引用,如果是可变引用就会违反引用规则。并且,Rc只能在单线程下使用。
5.RfCell< T >
16.并发
1.多线程
创建线程的两种方式:
- 通过OS的api来创建 – 运行时小 – 1 : 1
- 语言自己实现的线程 – 运行时大 – M : N
Rust提供的是1:1线程。
1 | use std::thread; |
这样写,一旦主线程结束了,我们创建的线程也就停止了。可以通过join方法来阻塞主线程。
1 | use std::thread; |
可以使用move将主线程里的值的所有权强制移动到分线程。
1 | use std::thread; |
2.通过channel实现线程通信
通过mpsc的一个关联函数可以构造一个元组(send, receive)
mpsc的意思:多个生产者,一个消费者。multiple producer, single consumer
可以通过克隆来实现多个发送者。
1 | use std::sync::mpsc; |
3.Mutex< T >共享内存
Mutex就是一个互斥锁,使用数据前需要先获取锁,然后使用完后需要释放锁。
1 | use std::sync::{mpsc, Mutex}; |
mutex可能会产生死锁
4.Arc< T >原子引用计数
在外面克隆引用,然后把克隆的引用传进去。
1 | use std::sync::{Arc, mpsc, Mutex}; |
ceshi: