rust 学习笔记
1. 入门指南
- 安装 & 更新 & 卸载
- 安装指令
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - 默认启动
export PATH="$HOME/.cargo/bin:$PATH - 更新
rustup update - 卸载
rustup self uninstall
- 安装指令
- 文件编译 & 执行
- 编译
rustc xxx.rs生成二进制文件直接执行
- 编译
- 项目编译 & 执行
- 项目构建
cargo new xxx - 项目编译
cargo build在 debug 文件下生成二进制文件 - 项目执行
cargo run可以直接执行 - 项目检查
cargo check比 build 快,方便检查 - release 版本
cargo build --release以及cargo run --release正式编译
- 项目构建
2. 猜数游戏
1 | use std::io; |
- rust 变量默认是不可变的
- 不可变变量声明
let,可变变量声明let mut
- 不可变变量声明
- rust 引用默认也是不可变的,引用访问同一份代码,没有拷贝开销
- 不可变引用
&sth,可变引用$mut sth
- 不可变引用
expect为了处理io::Result的结果Errprintln!中通过{}实现占位Cargo最主要的功能是管理、使用第三方库- 在
Cargo.toml中的[dependencies]下可以添加依赖 cargo build执行太慢换源
- 在
1 | [source.crates-io] |
- 没有修改重复使用
cargo build很快,因为Cargo.lock文件确保构建可重现 - 升级包版本使用
cargo update,但是遵从语义化版本规则,跨版本升级需要手动修改[dependencies] - rust 是静态强类型语言,能自动类型推导,也可以显示声明
- rust 允许同名变量,这一行为被称为隐藏
1 | use std::io; |
3. 通用编程概念
- 标量类型:整数、浮点数、布尔值、字符
- 复合类型:元组、数组
- rust 使用蛇形命名snake case作为函数、变量名称的风格,全小写、下划线分割
- rust 中语句 statement 和表达式 expression 有区别
- 语句执行操作但是没有返回值,比如
let x = 5; - 表达式会计算并产生一个值作为结果,比如
{}
- 语句执行操作但是没有返回值,比如
1 | fn main() { |
- rust 在函数中通过
->表明返回类型,函数中返回值等同于函数中最后一个表达式的值 - rust 中语句没有没有返回值,所以上面的大括号里的
x + 1如果带上;反而会报错 - rust 中的条件表达式会返回一个 bool 结果
1 | fn main() { |
- rust 中的
break后可以跟返回结果
1 | fn main() { |
for循环遍历 list 时,可以通过list.iter()遍历集合
1 | fn main() { |
- 可以通过
..补充集合,其中rev()反转集合
4. 所有权
- rust 通过包含特定规则的所有权系统来管理系统,在编译过程中完成,不会产生运行时开销
- rust 中的每一个值都有一个对应的变量作为它的所有者
- 在同一时间内,值有且仅有一个所有者
- 当所有者离开自己的作用域时,它持有的值就会被释放掉
- rust 中为了避免二次释放内存,堆上如果有两个指针指向这片内存,被复制的指针(栈)会被废弃,因此以下代码会报错,这个不是浅拷贝,而是 move
1 | fn main() { |
- rust 永远不会自动地创建数据的深拷贝,确实需要深拷贝堆数据时,可以使用
clone()方法,以下代码不会报错
1 | fn main() { |
- 拥有
Copy这种 trait 的类型可以在赋值其他变量后保持可用性,但是如果有Drop这种 trait 不能实现Copy- 整数、bool、char、浮点、所有字段都可
Copy的元组
- 整数、bool、char、浮点、所有字段都可
- 堆变量进入函数以后,就离开了作用域,不再有效
1 | fn main() { |
- rust 中将一个值赋值给另一个变量时就会转移所有权,当一个持有堆数据的变量离开作用域时,它的数据就会被
Drop清理回收,除非这些数据的所有权移动到了另一个变量上 - rust 中可以通过
&引用变量,而同时不交出所有权,但是同时也不能在引用中修改变量本身,因为引用默认是不可变的
1 | fn main() { |
- rust 中可变引用,需要变量本身是可变的,同时在作用域内,一个变量只能声明一个可变引用,因此在作用域里一个变量超过一个可变引用会报错
- 此外,在拥有不可变引用的情况下,不能创建可变引用
- 假设当前有某个引用,在这个引用被销毁前,编译器会确保堆空间不被销毁,也就不会出现悬垂引用
1 | fn main() { |
- rust 的字符串切片是专们用于字符串的,还有其他通用切片,比如数组切片
5. 结构体
- 在变量名与字段名相同时可以使用简化版的字段初始化方法
1 | fn build_user(emial: String, user_name: String) -> User { |
- 使用其他实例创建新的实例,
..可以表达剩下的未被显式赋值的字段都使用相同的值
1 | let user2 = User { |
- 元组结构体依然使用
struct关键字开头,并由结构体名称及元组类型定义组成
1 | struct Color(i32, i32, i32); |
{:#?}告知println!当前的结构体需要使用名为Debug的格式化输出,#[derive(Debug)]添加注解来派生 Debug trait
1 |
|
- 通过
impl块可以增加方法实现,&self代替了&Rectangle
1 | impl Rectangle { |
- Rust 会自动为调用者 object 添加
&&mut*以符合方法的签名 impl块中允许定义不接收self作为参数的函数,他们被称为 关联函数,常被用作构造器,可以通过::调用- Rust 中允许有多个
impl块
6. 枚举
- rust 中枚举和结构体都可以通过
impl定义结构体的方法
1 |
|
- rust 的
if与match的区别在于if后只能是 bool,但是match后可以是任何类型 - rust 中的
match支持绑定被匹配对象的部分值,我们可以据此从枚举变体中提取值
1 | fn plus_one(x: Option<i32>) -> Option<i32> { |
- rust 中的匹配是穷尽的,我们需要确保代码是合法的
- rust 中可以使用
if let作为match的语法糖,它只在值满足某一特定模式时运行,忽略其他可能,此外可以配合else使用
1 | fn main() { |
7. 包&模块
- rust 提供了一系列功能管理代码
- package: 用于构建、测试单元包的 cargo 功能
- crate: 用于生成库或可执行文件的树形模块
- module & use: 控制文件结构、作用域、路径的私有性
- path: 一种用于命名条目的方法
- 一个包中只能拥有最多一个单元包(库单元包或二进制单元包)
- cargo 默认将
src/main.rs视作一个二进制单元包的根节点,将src/lib.rs视作库单元包的根节点,它们拥有与包相同的名称 - 我们可以在路径
src/bin下添加源文件来创建更多二进制单元包,这个路径下每个源文件被视为单独的二进制单元包 - 使用关键字
mod可以定义一个模块,其内可以继续定义其他模块、结构体、枚举、常量、trait 或函数 - 模块结构组成模块树,模块间有父子关系,整个模块树被放在名为
crate的隐式根模块下 - rust 中路径有两种形式,路径的标识符之间使用
::分隔- 绝对路径:使用单元包或字面量 crate 从根节点开始
- 相对路径:使用 self、super 或内部标识符开始从当前模块开始
- rust 中所有条目默认是私有的,通过
pub可以暴露路径
1 | mod front_of_house { |
super关键字可以找到父模块
1 | fn serve_order() {} |
- 结构体为公共时,字段保持了私有,需要逐一决定是否公开;枚举为公共时,所有的变体都自动变为公共状态
- 当结构体有非公共字段时,需要提供一个公共的关联函数,否则无法构建实例
- 使用
use可以引入作用域,在user后使用相对路径需要从self开始,而不是从当前作用域开始 - 使用
as配合use可以给指定内容取别名 - 可以使用
pub use将当前引入内容重导出
8. 通用集合
- vector 连续存储多个值,string 字符集合,hash map 关联特定的键
- 可以通过
let v: Vec<i32> = Vec::new();创建动态数组,还可以通过宏创建动态数组let v = vec![1, 2, 3]; - 可以通过
push将元素放入 vector 中 - vector 被销毁时,其内元素也会被销毁
- 使用
&与[]会直接获得元素的引用,使用get会获得Option<&T> - vector 会遵循『不能同时拥有可变引用与不变边引用』的规则
- 需要使用解引用
*来获得 vector 绑定的值
1 | fn main() { |
- 可以通过
+、format!、push_str()、push()拼接 String,其中push_str()接收的是一个字符串切片,push接收单个字符 - 字符串使用
+会默认调用add方法,fn add(self, s: &str) -> String
1 | let s1 = String::from("Hello, "); |
- 这里 s2 的类型是
&String,但是符合方法&str是因为使用了强制转换,即&s2[..],由于self没有引用,所以调用结束后 s1 失效 String是基于Vec<u8>的封装,随着编码不同所需长度不同,所以len不能表达内容长度,实际表达的是字节长度,使用下标会报错String的chars()会遍历各个 char 值,bytes()会返回每个字节,合法的 Unicode 可能会占用 1 字节以上
1 | use std::collections::HashMap; |
- 实现了
Copytrait 的类型,他们的值会被复制到哈希映射中,但是比如String这种持有所有权的值,其值、所有权都会转移到哈希映射 HashMap提供了entry(),它返回一个枚举检查键值是否存在,如果不存在可以使用or_insert()将参数作为新值插入,并把新值的可变引用返回
9. 错误
- Rust 错误分为『可恢复』与『不可恢复』,针对『可恢复』错误提供了
Result<T, E>,『不可恢复错误』提供了中止运行的panic!宏 - 当
panic!发生时,Rust 沿着调用栈的反向顺序遍历所有函数并清理数据,也可以立即中止程序不进行清理,需要在配置中使用中止模式 - 使用
RUST_BACKTRACE=1 cargo run可以查看回溯表 - 可以使用
match匹配错误不同的情况
1 | use std::fs::File; |
- 这与下面的方法等价,可以少写不少
match
1 | use std::fs::File; |
unwrap当 Result 是 Ok 时,自动解析结果,否则调用panic!expect在unwrap的基础上附带错误信息,推荐使用这个- Rust 中
?是传播错误的快捷方式,此外?只适用于返回 Result 的函数
1 | use std::fs::File; |
- Rust 中的
?还可以链式处理
1 | use std::io; |
- 除了以上这些直接,还可以使用
std::fs下的fs::read_to_string()它已经做好了封装 main的返回类型除了()还可以是Result<T, E>,Box<dyn Error>被称为trait对象,表示任何可能的错误类型
1 | use std::error::Error; |
panic!对外抛出异常,应该优选选择Result- 测试时,应该使用
unwrap与expect作为占位符,测试时以panic!作为标记 - 如果代码处于『损坏』状态,应该使用
panic!;在自定义类型中,也可以配合panic!防止代码出现『损坏』状态的情况
10. 泛型&trait&生命周期
- 抽取相同功能作为函数,抽象逻辑
1 | fn main() { |
- 与此同时希望抽象类型,引入泛型,如果需要对所有类型实现判断
>判断还需要实现std::cmp::PartialOrd这个 trait
1 | struct Point<T> { |
- 在方法中使用泛型,需要在方法名后立即使用泛型的定义,如上方法
x所示,注意这里的<T>是为了后续的使用
1 | struct Point<T, U> { |
- 如上的泛型方法中,方法
mixup的类型V, W仅与当前方法有关,而结果显然是T, W,这里被类、方法的泛型共同定义 - rust 中泛型方法与具体类型的代码不会有任何速度上的差异,这是编译时完成的单态化,泛型被编译器生成了特定的定义,下面两段代码,第二段是编译器自动解析的
1 | let integer = Some(5); |
1 | enum Option_i32{ |
trait是 rust 编译器描述某些特定类型拥有的且能够被其他类型共享的功能。它与interface类似,但是不尽相同
1 | pub trait Summary { |
- 上面是一个实现 trait 的例子,只有当 trait 与类型定义在我们的库中时,才能实现对应的 trait,但是我们不能在『外部类』中实现『外部 trait』,这被称为孤儿规则
- trait 方法可以有默认的实现
1 | pub trait Summary { |
- trait 可以作为参数传递,比如
pub fn notify(item: impl Summary),这里的impl Summary表示传入的参数必须实现Summary这个 trait;同时它仅仅是一个语法糖,换一种写法pub fn notify<T: Summary>(item: T)效果相同,这里的T可以被多次使用,比如fn notify(item1: T, item2: T),这里的T是同一个类型 - 通过
+可以指定多个 trait 约束,比如pub fn notify(item: impl Summary + Display),这里的item必须实现Summary和Display这两个 trait;同样,它也可以被写作pub fn notify<T: Summary + Display>(item: T),这里的T必须实现Summary和Display这两个 trait - 这样的指定比较繁琐,可以使用
where关键字,比如pub fn notify<T>(item: T) where T: Summary + Display,这里的T必须实现Summary和Display这两个 trait - 返回值也可以是 trait,比如
fn returns_summarizable() -> impl Summary,这里的返回值必须实现Summary这个 trait,但是注意只能返回一个类型,不能返回两个不同的类型,比如fn returns_summarizable(flag: bool) -> impl Summary { if flag { NewsArticle{} } else { Tweet{} } }这样是不行的,因为NewsArticle和Tweet是不同的类型
1 | fn largest<T: PartialOrd + Copy>(list: &[T]) -> T { |
- 通过 trait 的方式实现泛型,可以避免代码重复,比如上面的
largest函数,如果不使用泛型,那么就需要写两个函数,一个是largest_i32,一个是largest_char,这样就会有很多重复的代码
1 | use std::fmt::Display; |
- 上面的例子中,
Pair结构体有一个泛型T,它有一个new方法,这个方法可以创建一个Pair实例,同时Pair还有一个cmp_display方法,这个方法可以比较x和y的大小,但是这个方法有一个限制,就是T必须实现Display和PartialOrd这两个 trait,这样的限制可以保证cmp_display方法可以使用x和y的Display方法,同时可以使用x和y的PartialOrd方法 - 普通泛型可以确保类型拥有期望的行为,『生命周期』可以确保引用在我们的使用过程中一直有效
1 | { |
- 上面的代码将会报错,因为
x的生命周期比r的生命周期短,所以r引用的x已经被释放了,所以r引用的是一个无效的值
1 | { |
- 上面的代码是可以正常运行的,因为
x的生命周期比r的生命周期长,所以r引用的x一直有效
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { |
- 上面的代码中使用了
'a显式声明声明周期,这里的'a表示x和y的生命周期一样长,所以返回值的生命周期也是'a,这样就可以保证返回值的引用一直有效(如果不加,我们就不知道x与y哪一个被返回,也就不知道他们的生命周期的长度,将会报错),上面的代码'a的意思是返回值生命周期会使用x与y中生命周期较短的那个
1 | fn main() { |
- 上面的代码将会报错,因为
string2的生命周期比result的生命周期短,所以result引用的string2已经被释放了,所以result引用的是一个无效的值 - 『生命周期』语法就是用来关联一个函数中不同参数及返回值的生命周期的。一旦它们形成了某种联系,Rust 就获得了足够的信息来支持保障内存安全的操作,并阻止那些可能会导致悬垂指针或其他违反内存安全的行为的代码
- 在么有显示标注的情况下,编译器目前有 3 种规则计算引用的生命周期:
- 一,每一个引用参数都会拥有自己的生命周期参数
- 二,当只有一个输入生命周期参数时,这个生命周期会被赋给所有输出生命周期参数
- 三,当拥有多个输入生命周期参数,其中一个是
&self或&mut self时,self的生命周期会被赋给所有输出生命周期参数
- 有一种特殊的生命周期
'static,表示整个程序的执行期
1 | fn longest_with_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str |
- 上面的代码中,
longest_with_announcement函数有三个参数,其中ann参数是泛型,这个泛型必须实现Displaytrait,这样才能使用Display的fmt方法,这个泛型没有生命周期,所以不需要加'a,但是x和y参数都有生命周期,所以需要加'a,这样才能保证返回值的引用一直有效
11. 测试
- rust 中将需要测试的函数头中增加
#[test]标注,然后使用cargo test命令进行测试,测试函数中使用assert!宏进行断言,如果断言失败,测试将会失败,如果断言成功,测试将会成功 assertt_eq!与assert_ne!宏可以用来比较两个值是否相等或不相等,后者在不相等时通过;如果是自定义类型,需要自行实现PartialEq和Debug这两个 trait,可以再定义的上方添加#[derive(PartialEq, Debug)]注解来自动实现这两个 trait- 在
assert中还可以增加自定义的错误信息,当断言失败时,可以根据错误信息来判断是哪里出错了 - 可以在
#[test]下方使用#[shold_panic]表示预期出现 panic,如果没有出现 panic,测试将会失败,同时参数expect表示预期中的 panic 信息,如果 panic 信息不一致,测试也将会失败 - 此外还可以使用
Result<T, E>编写测试,此时不要加上#[shold_panic],而是应该直接返回一个 Err 值,同时可以使用?运算符来简化代码 cargo test默认使用并行执行,可能因此出现测试错误,可以使用cargo test -- --test-threads=1控制执行线程数- 使用
cargo test -- --nocapture可以显示测试函数中的打印信息 - 可以使用
cargo test name中只要包含name的测试函数将会被执行 - 可以在代码中通过
#[ignore]来忽略某些测试函数,此外可以通过cargo test -- --ignored来执行被忽略的测试函数 - 集成测试代码被放在
src目录同级的tests文件中,集成测试中需要使用use xxx这是因为集成测试中每一个文件都是一个独立的包,同时不需要使用[cfg(test)]因为 rust 对tests目录做了特殊处理 - 因为集成测试中的代码都是独立的包,所以需要一个库来引入其他包,如果没有
src/lib.rs,我们可以创建一个空的src/lib.rs文件,然后在Cargo.toml中增加[[bin]]来创建一个二进制包,这样就可以在tests中引入这个二进制包了
12. 命令行程序
std::env可以获取输入信息,入参&x[0]存储的是程序的名称,&x[1]存储的是第一个参数,以此类推- 将程序拆分成
main.rs和lib.rs做到关注点分离,main.rs中只有一些简单的逻辑,而lib.rs中包含所有的业务逻辑,这样可以方便测试