《Rust权威指南》
本书由 Rust 核心开发团队编写而成,由浅入深地探讨了 Rust 语言的方方面面。从学习函数、选择数据结构及绑定变量入手,逐步介绍所有权、trait、生命周期、安全保证等高级概念,模式匹配、错误处理、包管理、函数式特性、并发机制等实用工具,以及两个完整的项目开发实战案例。 作为开源的系统级编程语言,Rust 可以帮助你编写出更为快速且更为可靠的软件,在给予开发者底层控制能力的同时,通过深思熟虑的工程设计避免了传统语言带来的诸多麻烦。 本书被视为 Rust 开发工作的必读书目,适合所有希望评估、入门、提高和研究Rust语言的软件开发人员阅读。
[!tip]- 作者简介
Steve Klabnik,Rust文档团队负责人,Rust核心开发者之一,Rust布道者及高产的开源贡献者,此前致力于Ruby等项目的开发。
Carol Nichols,Rust核心团队成员,i32、LLC联合构建者,Rust Belt Rust会议组织者。
毛靖凯,游戏设计师,一直专注于游戏领域研发,曾负责设计和维护了多个商业游戏的基础框架。业余时间活跃于Rust开源社区,并尝试使用Rust来解决游戏领域中的诸多问题。
唐刚,资深开发者,Rustcc社区创始人和维护者之一。目前就职于Cdot Network。使用Rust从事区块链共识协议的开发工作。
沙渺,嵌入式开发者,国内Rust语言社区和Raspberry Pi(树莓派)开发社区早期参与者。负责维护多个RISC-V架构硬件平台的基础函数库。
🗒️我的笔记
Rust简介
主要是看视频 https://www.bilibili.com/video/BV1hp4y1k7SV?p=2&spm_id_from=pageDriver
Rust 特别擅长的领域:
- 高性能 Web Service
- WebAssembly
- 命令行工具
- 网络编程
- 嵌入式设备
- 系统编程
Rust优点:
- 性能
- 安全性
- 无所畏惧的并发
install
https://www.rust-lang.org/
安装 in mac
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
更新:
rustup update
卸载Rust
rustup self uninstall
验证安装:
rustc --version
查看本地文档:
rustup doc
hello world
rustc main.rs
编译
Rust的缩进是4个空格而不是tab
println! 是个宏。函数是没有!的。
rustc 只适合简单的Rust程序。
hello Cargo
复杂的程序得用Cargo。
Cargo是Rust的构建系统和包管理工具。
- 构建代码、下载依赖的库、构建这些库…
创建项目 cargo new xxx
cargo build
编译构建项目。会生成target
目录,可执行文件就在里面。
cargo run
会编译 并运行项目。
cargo check
检查项目,比build快很多。
为发布构建,得加参数--release
cargo build --release
编译时会进行优化,编译的更慢,生成的执行文件运行的更快。
猜数游戏
use std::io; // 引用标准库
fn main() {
println!("猜数!");
println!("测试一个数");
let mut guess = String::new(); // let 默认声明常量。加上mut才是变量。 new类似于静态方法调用。
io::stdin().read_line(&mut guess).expect("无法读取行"); // &表示按引用传递。 expect表示错误处理。 Can
println!("你猜测的数是:{}", guess); //{}是占位符。类似于%s
}
rust中库不叫lib, 而是叫crate.
为了生成随机数,得引用一个叫rand的crate。
直接在Cargo.toml
里添加即可。
[dependencies]
rand = "0.3.14"
版本依赖会存到Cargo.lock
中,更新版本时也会更新Cargo.lock
trait 类似于其他语言的接口。
match guess.cmp(&secret_number) {
Ordering::Less=>println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win"),
} // match 类似于其他语言里的switch.
match 类似switch
loop 引入无限循环。break 退出循环。
如果要处理异常,而不是直接panic, 就需要去掉expect
, 改成match
自己匹配了。
完整的代码:
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("猜数!");
let secret_number = rand::thread_rng().gen_range(1,101); // i32 u32 i64
// println!("神秘数字是:{}", secret_number);
loop {
println!("测试一个数");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("无法读取行");
println!("你猜测的数是:{}", guess);
// shadow
let guess:u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
match guess.cmp(&secret_number) {
Ordering::Less=>println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {println!("You win");break;},
}
}
}
通用的编程概念
变量与可变性。
默认let
声明的是 不可变的。可以指定变量类型,也可自动推导。
要指明可变需要在变量前加mut
修饰符。
- 常量与不可变的变量不一样
- 得用
const
关键词声明,不能用mut
修饰符。 - 需要标注类型
- 常量可以在任何作用域内进行声明,包括全局作用域
- 常量只能绑定到常量表达式,无法绑定到函数的调用结果或运行才能计算的。(必须编译期间确定值)
- 得用
- 在程序运行期间,常量在其作用域内一直有效
- 命名规范,全大写,用
_
分隔。 如 MAX_POINT
Shadowing(隐藏)
可以声明同名变量 覆盖他。类型可以不一样。一般是类型转换时这么做。
数据类型
标量类型:一个标量类型代表一个单个的值
- 整数类型 i8,i16,i32,i64,i128,u8,u16,u32,u64,u128. arch和架构相关。开发模式会检查溢出。发布模式不会检查。
- 浮点类型 f32,f64(默认类型)
- 布尔类型(bool) true/false
- 字符类型(char) 4字节大小。
复合类型: 将多个值放在一个类型里。
Rust提供两种: 元组(长度固定,元素可以是多种类型)、数组(长度固定,元素类型统一)。
Tuple示例
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x,y,z) = tup;
println!("{},{},{}", x,y,z); // 通过结构访问
println!("{},{},{}", tup.0,tup.1,tup.2); // 按索引访问
}
数组示例(实际用Vector更多)
数组是分配在栈上的连续内存。
索引越界时 编译会通过,运行时会panic。
申明方式有3种:
- 字面量
let a = [1, 2, 3, 4, 5];
- 指明类型和长度
let a: [i32; 5] = [1, 2, 3, 4, 5];
- 重复的内容
let a = [3; 5];
// [3,3,3,3,3]
函数
关键词是fn
, 命名规范为snake case。 入口为main
.
参数:
- parameters, 形参,定义时的参数
- arguments, 实参。实际传进来的值。
函数签名必须声明类型。
函数体中的语句与表达式
- 函数体由一系列语句组成,可选的由一个表达式结束
- Rust是一个基于表达式的语言
- 语句是执行一些动作的指令
- 表达式会计算产生一个值
- 函数的定义也是语句
- 语句不返回值,所以不可以使用let将一个语句赋值给变量。
函数的返回值:
- 需要声明(-> 后面),不能命名
- 返回值是最有一个表达式的值
- 如果想提前返回,使用return , 并指定一个值
注意,表达式不要加’;'号,否则变成了语句。
分支语句
if
每种情况都叫arm
, 必须是bool
类型。 按顺序执行。
不用三元运算符。 let number = if condistion { 5 } else {6 };
这种写法就可以了。
match
类似于其他语言的switch, 处理多分支的。
loop
反复运行。无限循环。 loop
快里最后一个表达式可以返回值。
while
条件循环。
for
循环遍历集合。(由于其安全、简介。是用的最多的)
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {}", element);
}
}
Range
协助循环。
fn main() {
for number in (1..4).rev() { // (1..4) 就是 Range 生成[1,2,3] rev()为反转
println!("{}!", number);
}
println!("LIFTOFF!!!");
}
所有权
什么是所有权
Rust的核心机制。
其他语言要么是手动内存管理,要么是gc 自动管理。
Rust采用了第三种方式
- 内存是通过一个所有权系统来管理的,其中包含一组编译器在编译时检查的规则。
- 当程序运行时,所有权特性不会减慢程序的运行速度。
栈内存 vs 堆内存
所有存储在Stack上的数据必须拥有已知的固定的大小。
- 编译时大小未知的数据或运行时大小 可能发生变化的数据 必须存放在heap上。
使用栈内存 性能更高。因为不需要分配内存( 查找可用空间 需要时间)。访问也更快。
所有权解决的问题:
- 跟踪代码的哪些部分正在使用heap的哪些数据。
- 最小化heap上的重复数据量
- 清理heap上未使用的数据以避免空间不足。
所有权规则
- 每个值都有一个变量,这个变量是该值的所有者
- 每个值同时只能有一个所有者
- 当所有者超出作用域时,该值将被删除。
为什么String类型的值可以修改,而字符串字面值却不能修改。
因为他们处理内存的方式不同。
字符串字面值,在编译时就知道它的内容了,其文本内容直接被硬编码到最终的可执行文件里
- 速度快、高效。是因为其不可变性。
String类型,为了支持可变性,需要在heap上分配内存来保存编译时未知的文本内容:
- 操作系统必须在运行时来申请内存。
- 当用完String之后,需要使用某种方式将内存返回给操作系统。(在Rust中,对于某个值来说,当拥有它的变量走出作用域范围时,内存会立即自动的交还给操作系统)
drop
函数。离开作用域范围时自动调用。
变量与数据交互的方式:
- 移动(move)
- 多个变量可以与同一个数据使用一种独特的方式来交互。
let x=5;let y=x;
- 多个变量可以与同一个数据使用一种独特的方式来交互。
let s1 = String::from("hello");
let s2 = s1;// 这里 就是移动。。所有权从s1移动到了s2
println!("{}, world!", s1); // 编译通不过。s1被废弃掉了。否则会出现两次释放内存的问题。
![[Pasted image 20220530235851.png]]
2. 克隆(clone)
let s1 = String::from("hello");
let s2 = s1.clone(); // 如下图所示。heap上的内存也复制了一分。
println!("s1 = {}, s2 = {}", s1, s2);
![[Pasted image 20220531000207.png]]
Stack上的数据:复制
由于Stack上复制数据没啥性能消耗。所以不存在heap上的废弃问题。
Copy
trait, 可以用于像整数这样完全存放在stack上面的类型(就是大小很明显的值)
如果一个类型实现了Copy
这个trait, 那么旧的变量在赋值后仍然可用。
如果一个类型或者该类型的一部分实现了Drop
trait, 那么Rust不允许它再去实现Copy
trait了
一些拥有Copy trait的类型
- 任何简单标量的组合类型都可以是Copy的
- 任何需要分配内存或某种资源的都不是Copy的
- 一些拥有Copy trait的类型
- 所有的整数类型,如u32
- bool
- float32, float64
- char
- Tuple, 如果其所有内容都是Copy的
所有权和函数
- 在语义上,将值传递给函数和把值赋值给变量是类似的:
- 将值传递给函数会发生移动或复制
- 返回值与作用域
- 函数在返回值的过程中同样会发生所有权的转移
- 一个变量的所有权总是遵循同样的模式:
- 把一个值赋给其他变量时就会发生移动
- 当一个包含heap数据的 变量离开作用域时,它的值就会被drop函数清理,除非数据的所有权移动到另一个变量上了
引用与借用
&
表示引用: 允许你引用某些值 而 不取得其所有权
![[Pasted image 20220602223830.png]]
如图。s就是s1的引用。
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // s1的所有权 不会转移
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
我们把引用作为函数参数这个行为叫做借用。
不能修改借用的东西
和变量一样,引用默认是不可变的。
加上mut
可以把引用改成可变引用,这样就可以修改了。
不过在特定的作用域内,可变引用只能有一个。
- 这样做的好处 是可在编译时防止数据竞争。
以下三种行为会发生数据竞争:
- 两个或多个指针同事访问同一个数据。
- 至少有一个指针用于写入数据。
- 没有使用任何机制来同步对数据的访问。
可以通过创建新的作用域,来允许非同时的创建多个可变引用。
另一个限制:
不可以同时拥有一个可变引用和一个不变的引用
[!quote]- 悬空引用
悬空指针:一个指针引用了内存中的某个地址,而这块内存可能已经释放并分配给其他人使用了。
在Rust里,编译器可保证引用永远都不是悬空引用;
[!quote]- 引用的规则
在任何给定的时刻,只能满足下列条件之一;
一个可变的引用
任意数量的不可变的引用。
引用必须一致有效
切片
Rust 的另外一种不持有所有权的数据类型: 切片
[!quote]- 字符串切片
字符串切片就是指向字符串中一部分内容的引用。
示例:&a[1..5]
注意:
- 字符串切片的范围索引必须发生在有效的UTF-8字符边界内。
- 如果尝试从一个多字节的字符串中创建字符串切片,程序会报错并退出。
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); //这里是不可变引用
s.clear(); // error! 所以这里不能改数据了
println!("the first word is: {}", word);
}
fn first_word(s: &str) -> &str { // &str表示字符串切片。
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
使用structs结构化相关数据
定义和实例化结构体
例子
// 定义
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// 实例化
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// 引用字段
user1.email = String::from("anotheremail@example.com");
}
注意: 一旦struct的实例是可变的,那么实例中所有的字段都是可变的
[!quote]- Tuple struct
可定义类似tupe的struct, 叫做tuple struct
整体有名字,但是里面的元素没有名字。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
[!quote]- Unit-Like Struct(没任何字段)
适用于需要在某个类型上实现某个trait, 但是在里面又没有想要存储的数据。
struct数据的所有权
只要struct实例是有效的,那么里面的字段数据也是有效的。
struct里也可以存放引用,需要生命周期
生命周期保证只要struct实例是有效的,那么里面的引用也是有效的。
格式化:
std::fmt::Display {}
std::fmt::Debug
{:?} 单行打印
{:#?} 美化打印
#[derive(Debug)]
struct方法
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
方法与函数的不同之处:
- 方法是在struct(或enum、trait对象)的上下文中定义
- 第一个参数是self, 表示方法被调用的struct实例
[!quote]- 关联函数
可以再impl块里定义不把self作为第一个参数的函数,他们叫关联函数(不是方法)
示例:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let sq = Rectangle::square(3); // 调用
}
枚举与模式匹配
定义枚举
// 定义
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4; // 使用
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {}
将数据附加到枚举的变体中
enum IpAddrKind {
V4(String),
V6(String),
}
优点:
不需要额外使用struct
每个变体可以拥有不同的类型以及关联的数据量。
enum IpAddrKind {
V4(u8,u8,u8,u8),
V6(String),
}
[!quote]- Option枚举
定义于标准库中
在Prelude(预导入模块)中
描述了: 某个值可能存在(某种类型) 或不存在的情况
在Rust中没有Null
这个概念,而是用Option表达的。
enum Option<T> {
None, // Nome和Some可以直接使用
Some(T),
}
[!tip]-
Option<T>比Null好在哪?
Option<T>和T是不同的类型,不可以把Option<T>直接当做T
若想使用Option<T>
中的T, 必须将她转化为T
match控制流
[!quote]- 强大的控制流运算符-match
允许一个只与一系列模式进行匹配,并执行匹配的模式对应的代码
模式可以是字面值、变量名、通配符…
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {}
[!quote]- 绑定值的模式
匹配的分支可以绑定到被匹配对象的部分值。
因此,可以从enum变体中提取值。
匹配Option(T)
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
}
}
}
fn main() {
value_in_cents(Coin::Quarter(UsState::Alaska));
}
[!tip]- match匹配必须穷举所有可能
用_ 表示没列举的可能性。
if let 控制流
用于替代 match
只关心一种匹配的情况。
fn main() {
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}
}
改成if let
fn main() {
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
// 可以有ese 分支 处理其他情况
}
代码组织
代码组织主要包括:
- 哪些细节可以暴露,哪些细节是私有的
- 作用域内哪些名称有效
[!quote]- 模块系统
- Package(包): Cargo的特性,让你构建、测试、共享crate
- Crate(单元包): 一个模块树,它可以产生一个library或可执行文件
- Module(模块)、use: 让你控制代码的组织、作用域、私有路径
- Path(路径): 为struct、function或module等项命名的方式
Package 和 Crate
- Crate的类型
- binary
- library
- Crate Root:
- 是源代码文件
- Rust编译器从这开始,组成你的Crate的根Module
- 一个Packge:
- 包含一个Cargo.toml, 它描述了如何构建这些Crates
- 只能包含0-1个library crate
- 可以包含任意数量的binary crate
- 必须至少包含一个crate(library或binary)
Cargo的惯例
- src/main.ts:
- binary crate的crate root
- crate 名与package名相同
- src/lib.ts:
- package包含一个library crate
- library crate的crate root
- crate 名与package名相同
- 一个Package可以同时包含src/main.rs 和 src/lib.rs
- 一个binary crate, 一个libary crate
- 名称与package名相同
- 一个Package可以有多个binary crate:
- 文件放在src/bin
- 每个文件可以是单独的binary crate
[!quote]- Crate的作用
将相关的功能组合到一个作用域内,便于在项目间进行共享-防止冲突
定义module来控制作用域和私有性
- Module
- 在一个crate内,将代码进行分组
- 增加代码可读性,易于复用
- 控制项目(item)的私有性。public、private
- 建立module:
- mod 关键字
- 可嵌套
- 可包含其它项(struct、enum、常量、trait、函数等)的定义
路径 Path
- 为了在Rust的模块中找到某个条目,需要使用路径
- 路径的两种形式
- 绝对路径: 从crate root开始,使用crate名或字面值crate
- 相对路径:从当前模块开始,使用self,super或当前模块的标识符
- 路径至少由一个标识符组成,标识符之间用
::
。
私有边界
- 模块不仅可以组织代码,还可以定义私有边界。
- 如果想把函数或struct等设为私有,可以将它放到某个模块中。
- Rust中所有条目默认都是私有的
- 父级模块无法访问子模块中的私有条目
- 子模块里可以使用所有祖先模块中的条目
[!quote]- pub
加上pub就变成公有了。
super
引用上级
use关键词
可以使用use
关键字将路径导入导作用域内
可以使用相对路径/绝对路径
use的习惯用法:
- 函数:引入到父级
- struct, enum, 其他: 指定完整路径
- 同名条目: 指定到父级
可用 as
取别名。
use
引入的模块默认是私有的。外部模块无法访问。
加上pub
之后 外部模块也就可以访问了。
使用外部包
- 在Cargo.toml里添加依赖的package
- Cargo会从https://crates.io/ 下载包及其依赖项
- use引入
标准库也被当做外部包。由于内置了。不需要修改配置文件。但使用的时候还得用use
更多参考 https://download.youkuaiyun.com/download/goodparty/86824159?spm=1001.2014.3001.5503