<Rust权威指南>学习笔记
Contents
效率
离线本地文档
rustup doc
只检查能否编译(相对 cargo build
更快):
cargo check
构建该项目有关依赖的所有相关文档:
cargo doc --open
自动 fix
cargo fix
添加依赖
cargo install cargo-edit
cargo add rand
检查代码
rustup component add clippy
cargo clippy
cargo clippy --all -- -D warnings
cargo clippy --all-features --all --tests --examples -- -D clippy::all -D warnings
# 应用修改
cargo +nightly clippy --fix -Z unstable-options
更多选项, 参考官网:
https://github.com/rust-lang/rust-clippy
其他的 Cargo 子命令
https://github.com/rust-lang/cargo/wiki/Third-party-cargo-subcommands
格式化代码
rustup component add rustfmt
cargo fmt
# nightly 版本
rustup component add rustfmt --toolchain nightly
cargo +nightly fmt
常量
使用 const
修改, 且必须要指明类型, 而且常量只能绑定在一个常量表达式, 而不能是函数的返回值, 或在运行时计算的值.
常量通常以下划线分隔, 字母全大写. 例如
const PI: f32 = 3.14f32;
let
重复使用 let
是会创建出新的变量, 所以, 可以在复用变量名的同时修改它的类型.
数据类型
整数
除了 byte
, 其他的字面量都可以使用类型后缀. 例如 66u8
在 build 的调试模式下编译, rust 会检查是否溢出. 而用 --release
来编译, 则不会, 而且进行取模.
浮点
在现代 CPU 中, f32
和 f64
效率相差无几, 所以 rust 默认浮点为 `f64
布尔
只有 true
和 false
占一个字节大小.
字符
用单引用括起来的. 例如 'A'
. 它占 4 字节.
风格
- 使用
snake case
, 即小写字母 + 下划线. 它是普通变量名和函数的命名风格. - 而类型的话, 则使用
camel case
, 即驼峰式
语句和表达式
- 语句 : 不返回值. 例如
let a = 6;
- 表达式: 产生一个值. Rust 中大部分都是表达式
- 单单的字面量
- 函数调用
- 调用宏
- 创建新作用域的花括号
{}
- 代码块的表达式的值就是最后一个表达式的值
循环
loop
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
}
while
let mut num = 3;
while num != 0 {
println!("{}", num};
num = num - 1;
}
for
let a = [10, 20, 30];
for ele in a.iter() {
println!("{}", ele};
}
for num in (1..4).rev() {
println!("{}", num);
}
所有权
- 所在存储在栈中的数据必须拥有一个
已知
且固定
的大小. - 编译期无法确定大小的数据, 则只能存储在堆中.
规则
- Rust 中的每一个值都有一个对应的变量作为它的所有者
- 在同一时间内, 值有且仅有一个所有者
- 当所有者离开自己的作用域, 它持有的值就会被释放掉
作用域
从声明的位置到当前作用域结束
drop 函数
Rust 会在作用域结束的地方自动调用 drop
函数
在
C++
中, 这种在对象生命周期结束时释放资源的模式有时也称为资源获取即初始化RAII
移动 move
String 内存布局由三部分组成:
ptr
: 指向存放字符串内容的指针len
: 长度capacity
: 容量
Rust 永远不会自动创建数据的深度拷贝, 因此在 Rust 中, 任何自动的赋值操作都可以被视为高效的
clone
当你确实需要深度拷贝(堆上的数据) 而不仅是栈数据时, 可调用 clone
方法
注意, 这种操作相当消耗资源
copy
用于完全存储在栈上的数据类型. 一旦某种类型拥有了 Copy
的 trait , 那它的变量可以在赋值给其他变量之后保持可用性.
如果一种类型本身或这种类型的任何成员实现了
Drop
这种 trait , 那 Rust 就不允许实现Copy
这种 trait
原则上, 任何简单标题的组合类型都是 Copy
的, 任何需要分配内存或某种资源的类型都不会是 Copy
的. 以下是 Copy
的类型
- 所有的整数类型
- bool 类型
- 字符类型
char
- 所有的浮点类型
- 如果元组包含的所有字段都是
Copy
, 那这个元组也是Copy
的
所有权与函数
将值传递给函数在语义上类似对变量进行赋值
将变量传递给函数将会触发 Move
或 Copy
, 就像赋值语句一样
返回值与作用域
函数在返回值的过程中也会发生所有权转移.
所有权转移模式
- 将一个值赋值给另一个变量时, 就会转移所有权
- 当一个持有堆数据的变量离开作用域时, 它的数据就会被 drop 清理回收, 除非这些数据的所有权移动到了另一个变量上
引用与借用
&
: 引用语义, 不可变引用: 在不获取所有权的前提下使用值.- 解引用:
*
- 解引用:
&mut
: 可变引用.- 它需要变量声明为
mut
- 函数参数与要声明为
&mut
- 传递给函数时也要写为
&mut
- 它需要变量声明为
- 不能在拥有不可变引用的同时创建可变引用
通过传递参数给函数的方法也被称为借用
borrowing
切片
它不持有所有权. 切片是引用某一连续的元素序列, 而不是整个集合.
字符串切片写为
&str
字符串字面量就是切片.
其他类型的切片格式: &[type]
. 例如
&[i32]
&[u32]
结构体
如果实例是可变的, 则实例中所有的字段都是可变的
如果变量名与字段名相同时, 可以简化:
fn build_user(email: String, user: String) -> User {
User {
email,
user,
active:true,
sign_in_count: 1,
}
}
根据其他实例创建新实例:
let user1 = User {
email: "hello".to_string(),
email: "hello".to_string(),
active: true,
sign_in_count: 1,
}
let user2 = User {
email: "hello2".to_string(),
email: "hello2".to_string(),
..user1
}
方法
impl StructName {
fn hello(&self) -> u32 {
}
}
即, 第一个参数是 self
或 &self
或 &mut self
self
: 会转移所有权&self
: 不可变借用&mut self
: 可变借用
通过实例 o.hello()
来调用方法
关联函数
impl StructName {
fn new() -> u32 {
}
}
即第一个参数不是 self
或 &self
或 &mut self
通过 StructName::new()
来调用关联函数
集合
动态数组 Vec
let v: Vec<i32> = Vec::new();
let v = vec![1,2,3];
let muv v = Vec::new();
v.push(6);
//读取
let e = &v[2]; //如果越界, 则 panic, 否则返回该位置元素的引用
let ep = v.get(2); //返回 Option
//不可变引用遍历
let v = vec![100,32,57];
for i in &v {
println!("{}",i);
}
//可变引用遍历并修改
let mut v = vec![100,32,57];
for i in &mut v {
*i+=50;
}
String
let s = String::from("hello");
//拼接
let s = String::from("hello");
let s1 = String::from(" world");
let s3 = s + &s1 ; //这时, s 不可用了, 而 s1 还拥有所有权, 可以使用
let s3 = format!("{}{}", s, s1); //这种不会转移任何参数的所有权
内存布局
它实质上是基于 Vec<u8>
的封装类型. 它是使用 UTF8
编码保存的字节数组.
可从三个不同的角度来看待字符串中的数据
- 字节. 即
Vec<u8>
- 调用
.bytes()
- 调用
- 标量值. 即 Rust 中的
char
类型- 调用
.chars()
- 调用
- 字形簇. 最接近人认为的真正字符
- 标准库中没提供这个功能. 可从第三方库中获取
要小心使用字符串切片(因为它使用的是字节
)
HashMap
let teams = vec![String::from("Blue"),String::from("Yellow")];
let initial_scores = vec![10,50];
let scores:HashMap<_,_> = teams.iter().zip(initial_scores.iter()).collect();
//访问
scores.get(&key) // 返回 Option
一般情况下, 对于 Copy
类型, 会复制到 HashMap, 而拥有所有权的类型, 则会转移到 HashMap.
- 覆盖旧值: 两次插入相同的 key.
scores.insert(k, v);
- 只在键没有的情况下插入.
scores.enry(k).or_insert(v)
; 基于旧值来更新:因为
.or_insert()
返回的是关联值的可变引用&mut v
. 它的函数签名为pub fn or_insert(self, default: V) -> &'a mut V
let text = "helloworldwonderfulworld"; let mut map = HashMap::new(); for word in text.split_whitespace( { let count = map.entry(word).or_insert(0); *count+=1; }
Hash 函数
HashMap 默认使用了一个在密码学上安全的 Hash 函数. 这不是最快的 Hash 算法. 如果你发现默认的 Hash 函数成了你的性能热点并导致性能问题, 你可以指定不同的 Hash 工具来使用其他函数. 这些函数要实现了 BuildHasher
trait 类型.
错误处理
不可恢复错误
panic!
宏.
它会打印一段错误提示信息, 展开并清理当前的调用栈, 然后退出线程(如果main 线程则相当于终止程序).(默认). 这需要二进制文件中存储额外多的信息, 导致文件变大.
也可以选择立即终止当前线程(如果main 线程则相当于终止程序), 直接结束且不进行任何清理工作, 内存则只能由 OS 来回收. 如果需要二进制文件尽可能小, 那可以在 Cargo.toml
中加入如下来切换为终止模式
[profile.release]
panic = 'abort'
可恢复错误
Result<T, E>
Ok
Err
.unwrap()
方法
- Result 返回 OK 时, 它就会返回 Ok 的内部值
- Result 返回 Err 时, 则会调用
panic!()
宏
.expect(xxx)
方法类似.unwrap()
, 不过, 可以自定义错误信息.
传播错误
除了冗余地判断 Result , Rust 也提供了简化的语法: ?
问号运算符.
通过将 ?
放在 Result
值之后
- 如果 Result 的值是
Ok
, 则返回Ok
中的值, 并继续执行. - 如果 Result 的值是
Err
, 则这个值就会作为整个结果返回. 如同使用了return
一样将错误传播给调用者.
通过 ?
也可以进行链式调用
统一异常处理
fn read_and_validate(b: &mut dyn io::BufRead) -> Result<PositiveNonzeroInteger, Box<dyn error::Error>> {
let mut line = String::new();
b.read_line(&mut line)?;
let num: i64 = line.trim().parse()?;
let answer = PositiveNonzeroInteger::new(num)?;
Ok(answer)
}
泛型
类型声明必须被放置在函数名与参数列表之间的一对尖括号<>
中. 例如
fn largest<T>(list: &[T]) -> T {
}
结构体中的泛型
structPoint<T,U>{
x:T,
y:U,
}
枚举中的泛型
enumOption<T>{
Some(T),
None,
}
方法中使用泛型
struct Point<T> {
x:T,
y:T,
}
impl<T> Point<T> {
fn x(&self) ->&T {
&self.x
}
}
性能
使用泛型代码与具体类型的代码, 没有性能上的差别.
因为 Rust 在编译时执行泛型代码的单态化. 即在编译期间将泛型代码转换为特定类型的代码.
trait
限制: 只有当 trait 或类型定义在我们的库中时, 我们才能为该类型实现对应的 trait
类似其他语言中的接口interface
, 但不完全相同
定义一个 trait
pub trait Summary {
fn summarize(&self) -> String;
//默认实现
fn defaul_act(&self) -> String {
String::from("hello..")
}
//默认实现可以调用没默认实现的方法
fn author(&self) -> String;
fn default_author -> String {
format!("hello {}", self.author())
}
}
为类型实现 trait
impl Summary for xxxx {
fn summarize(&self) -> String {
format!("hello {} ", self.xxx)
}
}
使用 trait 作为参数
fn notify(item: impl Summary) {
}
完整的形式为
fn notify<T: Summary>(item: T) {
}
要同时限制多个 trait , 则可以通过 +
来指定约束
fn notify(item: impl Summary + Display) {
}
fn notify<T: Summary + Display>(item: T) {
}
使用 where 子句简化
fn some_function<T:Display+Clone,U:Clone+Debug>(t:T,u:U) ->i32 {
}
//可简化写为
fn some_function<T,U>(t:T,u:U) ->i32
where T:Display+Clone, U:Clone+Debug
{
}
生命周期
函数中的泛型生命周期
标语法 : (标并不会改变任何引用的生命周期长度, 仅用于检查)
&i32
&'a i32 // 显式生命周期
&'a mut i32 //显式生命周期的可变引用
它是用来关联一个函数中不同参数及返回值的生命周期的.
结构体中标生命周期:
struct ImportantExcerpt<'a> {
part: &'a str,
}
隐匿规则计算生命周期
- 每一个引用参数, 都会拥有自己的生命周期. (作用于输入生命周期)
- 当只存在一个输入生命周期时, 这个生命周期会被赋予所有输出生命周期参数. (作用于输出生命周期)
- 当拥有多个输入生命周期时, 而其中一个是
&
或&mut self
时,self
的生命周期会被赋予所有的输出生命周期参数
静态生命周期: 'static
同时使用泛型参数, trait 约束 和生命周期
fn longest_with_an_announcement<'a,T>(x:&'astr,y:&'a str,ann:T)>&'a str where T:Display
{
}
自动化测试
测试函数:
#[test]
fn hello() {
assert_eq!(2 + 2, 4);
}
assert!
接收一个布尔类型的参数. 如果为 true , 则通过, 否则调用panic!
宏assert_eq!
判断是否相等assert_ne!
判断是否不相等
不使用assert
, 而是使用 Result 作为测试
#[test]
fn hello() -> Result<(), String> {
....
}
should_panic
#[test]
#[should_panic]
fn hello() {
other_fn();
}
如果 other_fn
函数出现了 panic!
则表示通过…
控制测试的运行方式
查看所有 cargo test
后可用的参数
# 这表示所有可以用在 cargo test -- 之后的参数
cargo test -- --help
cargo test --help
会显示 cargo test
的可用参数
- 控制线程数量:
cargo test -- --test-threads=1
- 默认情况下 test 会过虑所有
println!
的输出. 可以通过下面来禁用(即显示输出):cargo test -- --nocapture
- 只执行指定的函数测试 :
cargo test 函数名
(如果只写前缀, 则所有符合前缀的测试函数都会执行) - 忽略测试函数:
#[ignore]
- 单独执行标注有 ignore 的测试:
cargo test -- --ignored
单元测试
约定俗成地在每个源代码中都新建一个 tests 模块来存放测试函数, 并使用 cfg(test)
对该模块进行标注.
#[cfg(test)]
可以让 Rust 在执行 cargo test
命令时编译和运行该部分测试代码, 而在 cargo build
时剔除它们.
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
集成测试
- 创建一个
tests
目录. 位于项目根目录下, 和src
并列. - 然后在该目录下添加测试代码
指定运行集成测试中指定的文件名:
cargo test --test integration_tset
表示只运行集成测试下的 integration_tset.rs
中的所有测试
命令行程序
std::env::args
函数会因为命令行参数中包含非法的 Unicode 字符而发生 panic
.
如果确实需要在程序中接收非法 Unicode 字符的参数, 则要用 std::env::args_os
函数, 它返回的是 OsString
而不是 String
- 输出到标准错误输出:
eprintln!(...)
- 处理环境变量:
std::env
闭包
不强制你标参数和返回值的类型
通常编译器能自动推断. 不过, 显式加上也没问题.
闭包可以通过 3 种方式从它们的环境中捕获值. 与函数接收参数是一致的
- 获取所有权.
FnOnce
trait - 可变借用.
FnMut
trait - 不可变借用.
Fn
trait
如果希望强制闭包获取环境中值的所有权, 可以在参数列表前加 move
关键字. 这在闭包传入新线程中时相当有用.
迭代器
它是惰性的 lazy
. 即除非你主动调用方法来消耗并使用迭代器, 否则它们不会产生任何的实际效果.
iter()
方法生成的是一个不可变引用的迭代器into_iter()
取得所有权并返回元素本身的迭代器iter_mut
可变引用的迭代器
文档中的描述是这样子的
iter()
, which iterates over&T
.iter_mut()
, which iterates over&mut T
.into_iter()
, which iterates overT
.
消耗迭代器
next()
方法. 它也消耗了迭代器本身.sum()
方法
生成其他迭代器
这些称为迭代器适配器 iterator adaptor
.
这些方法可以将已有的迭代器转换成其他不同类型的迭代器.
你可以链式地调用多个迭代器适配器来完成一些复杂的操作.
注意, 所有的迭代器都是惰性的, 所以你必须调用一个消耗适配器的方法才能从迭代器适配器中获得结果
map()
方法filter()
方法
性能
你可以完全无所畏惧地使用迭代器和闭包. 它不会带来任何运行时性能损失.
cargo
文档: https://doc.rust-lang.org/cargo/
文档注释
使用 ///
来注释, 里面可以用 Markdown 语法来格式化内容.
写完后, 可以用
# 生成基于文档注释的 HTML 文档
cargo doc
# 生成并打开
cargo doc --open
重新导出 reexport
pub use xxxx;
这样子对内是自己的模块结构.
对外则是顶层结构.用户可以直接使用 crate名::xxx
了
发布到 crates.io
在 crates.io 中注册一个账户并获取一个 API token.
然后执行下面的命令
cargo login token_value
这条命令, 会将 token 存入 ~/.cargo/credentials
文件中
然后完善 Cargo.toml
, 添加一些元数据等(在 [package]
中
- name
- version
- authors
- description
- license
然后就可以发布了
cargo publish
移除版本: (它不能删除旧版本的包, 但可以阻止新的项目使用该版本的包)
cargo yank --vers 1.0.1
# 取消撤回
cargo yank --vers 1.0.1 --undo
工作空间
在一目录下创建一个文件 Cargo.toml
, 内容如下
$ cat Cargo.toml
[workspace]
members = ["adder",]
members 就是该工作空间下各个子 lib 或 bin 项目.
- 各个子项目默认不是相互依赖的, 如果在一个子项目要使用另一个子项目的功能, 则要显式指定
- 构建: 在工作空间根目录下,
cargo build
- 生成二进制程序: 在工作空间根目录下 :
cargo build -p 二进制项目名
智能指针
它是一些数据结构, 行为类似指针, 但有额外的元数据和附加功能.
引用只是借用数据的指针;
大多智能指针本身拥有它们指向的数据
通常使用结构体来实现. 并会实现 Deref
和 Drop
两个 trait
Box<T>
: 用于在堆上分配值Rc<T>
: 允许多重所有权的引用计数类型Ref<T>
和RefMut<T>
: 通过RefCell<T>
访问, 可以在运行时而不是编译时执行借用规则的类型
Box<T>
它会将数据存放在堆, 而不是栈上. 使用场景
- 无法在编译时确定大小的类型, 但又想要在一个要求固定尺寸的上下文环境中使用这个类型的值时
- 比如定义递归类型 因为 Rust 必须在编译时知道每一种类型占据的空间大小.
- 当你需要传递大量数据的所有权, 但又不希望产生大量数据的复制行为时
- 当你希望拥有一个实现了指定 trait 的类型值, 但又不关心具体的类型时
Box 除了间接访问内存和堆分配, 没有其他任何特殊功能, 也就没附带的性能开销
手动释放堆内存:
drop(xxx);
该函数位于
std::mem::drop
Rc<T>
只能用在单线程中.
- 增加引用计数:
Rc::clone(&a);
RefCell<T>
内部可变性
它是在运行时, 而不是编译时检查借用规则.
它也只能用于单线程情况
选择依据
Rc<T>
允许一份数据有多个拥有者, 而Box<T>
和RefCell<T>
都只有一个所有者- 多线程版本为
std::sync::Arc
- 多线程版本为
Box<T>
允许在编译时检查的可变或不可变借用.Rc<T>
仅允许编译时检查不可变借用.RefCell<T>
允许运行时检查的可变或不可变借用RefCell<T>
允许我们在运行时检查可变借用, 所以即使RefCell<T>
本身是不可变的, 我们仍然能够更改其中存储的值
可变借用和不可变借用
.borrow()
.borrow_mut()
并发
创建线程
let handle = thread::spawn(|| {
for i in 0..5 {
println!("thread {}", i);
}
});
等待线程执行完毕:
handle.join().unwrap();
move 闭包
用 move 可以强制获取外部环境变量的所有权
let v = String::from("hello");
let handle = thread::spawn(move || {
for i in 0..5 {
println!("thread {}, {}", i, v);
}
});
消息传递
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let v = String::from("hello world from thread");
tx.send(v).unwrap();
});
let rec = rx.recv().unwrap();
println!("main rec {}", rec);
}
send 函数会获取参数的所有权
创建多个发送者
let tx1 = mpsc::Sender::clone(&tx);
共享状态
Mutex<T>
互斥体
use std::thread;
use std::sync::{mpsc, Arc};
use std::sync::Mutex;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut hs = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let h = thread::spawn(move || {
let mut v = counter.lock().unwrap();
*v += 1;
});
hs.push(h);
}
for h in hs {
h.join().unwrap();
}
println!("result {}", *counter.lock().unwrap());
}
Sync trait 和 Sync trait
- Send trait : 允许线程间转移所有权.
- Sync trait : 允许多线程同时访问
模式匹配
- match 分支
- if let 表达式
- while let 表达式
- for 循环
- let 语句
- 函数参数
语法
匹配字面量
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("any"),
}
匹配命名变量
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("match y {}", y),
_ => println!("default {:?}", x),
}
println!("main {:?}, {}", x, y);
}
match 会开启一个新的作用域
多重模式
fn main() {
let x = 5;
match x {
1 | 2 => println!("in..."),
3..=5 => println!("range..."),
_ => println!("default"),
}
}
- 使用
|
一次性匹配多个模式 - 使用
..=
来匹配区间
解构
let a = (1, 2);
let (x, y) = a;
println!("{}, {}", x, y);
忽略值
_
: 忽略一个..
: 忽略剩余fn main() { let a = (1, 2, 3, 4, 5, 6); let (x, _, y, ..) = a; println!("{}, {}", x, y); }
match guard
fn main() {
let num = Some(4);
match num {
Some(x) if x < 5 => println!("< 5"),
Some(x) => println!("{}", x),
None => (),
}
}
@
绑定
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello {
id: id_variable @ 3..=7,
} => println!("Found an id in range: {}", id_variable),
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {}", id),
}
高级特性
unsafe
它的能力有
- 解引用裸指针
- 调用不安全的函数或方法
- 访问或修改可变的静态变量
- 实现不安全 trait
裸指针 raw pointer
- 不可变:
*const T
- 可变:
*mut T
它们引用, 智能指针的区别
- 允许忽略借用规则, 可同时拥有指向同一内存的可变和不可变指针, 或同一个地址的多个可变指针
- 不能保证自己总是指向了有效的内存地址
- 允许为空
- 没有实现任何自动清理机制
高级 trait
使用关联类型指定点位类型.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
消除歧义的完全限定语法
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}