Rust学习笔记
变量
基本
-
不可变、可变、常量:
1
2
3let a = 0; // 不可变变量,但可以shadowing
let mut a = 0; // 可变变量
const A: i32 = 0; // 常量,必须手动标注类型 -
变量解构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16let (a, mut b): (bool,bool) = (true, false);
struct Struct {
e: i32
}
fn main() {
let (a, b, c, d, e);
(a, b) = (1, 2);
// _ 代表匹配一个值,但是我们不关心具体的值是什么,因此没有使用一个变量名而是使用了 _
[c, .., d, _] = [1, 2, 3, 4, 5];
Struct { e, .. } = Struct { e: 5 };
assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);
} -
整数类型
u8
~u128
,i8
~i128
不同类型的整数不能进行运算。
类型转换必须是显式的. Rust 永远也不会偷偷把你的 16bit 整数转换成 32bit 整数
-
序列(Range)
1
2
3for i in 1..=5
for i in 'a'..='z'
所有权与引用
- 基本类型及其复合的赋值不会转移所有权而是拷贝(例如,整数或整数数组),复合类型的赋值会转移所有权,函数的参数传递和返回值也是一样。
- 可以通过引用来使用和修改变量,引用默认不可变,可变引用
&mut
。 - 同一时刻,你只能拥有要么一个可变引用, 要么任意多个不可变引用
- 引用必须总是有效的
字符串与切片
-
字符串字面量属于
&str
类型,不可变引用,UTF-8编码。 -
切片语法:
&s[start..end]
。类似于python,属于不可变引用 -
引用不是单纯的取地址,它是一个单独的类型,
&str
就包括str
的地址和长度。 -
String
类型则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串 -
&str
->String
1
2String::from("Hello world");
"Hello world".to_string(); -
String
->&str
1
2
3
4
5
6
7
8
9
10fn main() {
let s = String::from("hello,world!");
say_hello(&s);
say_hello(&s[..]);
say_hello(s.as_str());
}
fn say_hello(s: &str) {
println!("{}",s);
} -
操作
String
:追加、插入、替换、删除、连接。具体看文档,有的操作会改变String
本身,有的不会。 -
遍历字符与字节:
1
2
3
4
5
6
7for c in "中国人".chars() {
println!("{}", c);
}
for b in "中国人".bytes() {
println!("{}", b);
}
元组
-
内部元素的类型可以互不相同
1
2
3
4
5
6fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup;
let five_hundred = tup.0; // 访问元素
}
结构体
-
定义与创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("another@example.com"),
..user1 // 结构体更新语法,.. 语法表明凡是我们没有显式声明的字段,全部从 user1 中自动获取。需要注意的是 ..user1 必须在结构体的尾部使用。
};
// user1 的部分字段所有权被转移到 user2 中:username 字段发生了所有权转移,作为结果,user1 无法再被使用。 -
元组结构体
1
2
3
4
5struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0); -
使用
#[derive(Debug)]
来打印结构体的信息1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
枚举
-
可以统一同类不同种结构等的处理,使得可以函数可以统一处理,通过match来分类讨论。
1
2
3
4
5
6
7
8
9
10
11
12enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let m1 = Message::Quit;
let m2 = Message::Move{x:1,y:1};
let m3 = Message::ChangeColor(255,255,0);
} -
Option
枚举用于处理空值(https://doc.rust-lang.org/std/option/enum.Option.html)1
2
3
4
5
6
7
8
9enum Option<T> {
Some(T),
None,
}
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None; // 赋None值时需要显示指定Option类型- 当不确定某些值在运行时是否会有空值时,可以使用Option 枚举来处理None,具体可使用match来控制对Some和None的处理。
- 可以使用cloned方法将
Option<&T>
转化为Option<T>
。
-
Result
枚举用于返回错误(https://doc.rust-lang.org/std/result/enum.Result.html)1
2
3
4
5
6
7
8
9
10enum Result<T, E> {
Ok(T),
Err(E),
}
let x: Result<i32, &str> = Ok(-3);
assert_eq!(x.is_ok(), true);
let x: Result<i32, &str> = Err("Some error message");
assert_eq!(x.is_ok(), false);
数组
1 | let a: [i32; 5] = [1, 2, 3, 4, 5]; |
- 长度固定
- 元素必须有相同的类型
- 依次线性排列
- 数组并没有实现
Index
特征,只有数组切片才有
Vector
-
Vector是动态数组,里面的类型应该都是相同的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 初始化
let mut v = Vec::new(); // 有时候需要手动标注Vec的类型
v.push(1); // 这里rustc自动推断v是<i32>类型
let v = vec![1, 2, 3];
let mut v = Vec::with_capacity(capacity)
// 取数据
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100]; // 会直接报错
let does_not_exist = v.get(100); // 返回Option,这里返回None
// 迭代
let v = vec![1, 2, 3];
for i in &v {
println!("{i}");
}
let mut v = vec![1, 2, 3];
for i in &mut v {
*i += 10
} -
如果需要含有不同类型,应该考虑使用枚举或者特征对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39// 枚举
enum IpAddr {
V4(String),
V6(String)
}
let v = vec![
IpAddr::V4("127.0.0.1".to_string()),
IpAddr::V6("::1".to_string())
];
// 特征对象
trait IpAddr {
fn display(&self);
}
struct V4(String);
impl IpAddr for V4 {
fn display(&self) {
println!("ipv4: {:?}",self.0)
}
}
struct V6(String);
impl IpAddr for V6 {
fn display(&self) {
println!("ipv6: {:?}",self.0)
}
}
fn main() {
let v: Vec<Box<dyn IpAddr>> = vec![ // 需要手动标注下类型
Box::new(V4("127.0.0.1".to_string())),
Box::new(V6("::1".to_string())),
];
for ip in v {
ip.display();
}
}
HashMap
-
创建,其中
key
必须实现std::cmp::Eq
和Hash
特征。1
2
3
4
5
6
7
8
9
10
11
12
13use std::collections::HashMap; // 必须
let mut my_gems = HashMap::new();
my_gems.insert("红宝石", 1);
let teams_list = vec![
("中国队".to_string(), 100),
("美国队".to_string(), 10),
("日本队".to_string(), 50),
];
// collect 方法在内部实际上支持生成多种类型的目标集合,因此我们需要通过类型标注 HashMap<_,_> 来提示编译器
let teams_map: HashMap<_,_> = teams_list.into_iter().collect(); -
查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score: Option<&i32> = scores.get(&team_name); // 这里score是值的引用
let score: i32 = scores.get(&team_name).copied().unwrap_or(0); // 这里score是值本身
for (key, value) in &scores {
println!("{}: {}", key, value);
} -
更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert("Blue", 10);
// 覆盖已有的值
let old = scores.insert("Blue", 20);
assert_eq!(old, Some(10));
// 查询新插入的值
let new = scores.get("Blue");
assert_eq!(new, Some(&20));
// 查询Yellow对应的值,若不存在则插入新值
let v = scores.entry("Yellow").or_insert(5);
assert_eq!(*v, 5); // 不存在,插入5
// 查询Yellow对应的值,若不存在则插入新值
let v = scores.entry("Yellow").or_insert(50);
assert_eq!(*v, 5); // 已经存在,因此50没有插入
}
// 在已有值基础上更新
let text = "hello world wonderful world";
let mut map = HashMap::new();
// 根据空格来切分字符串(英文单词都是通过空格切分)
for word in text.split_whitespace() {
// or_insert 返回了 &mut v 引用,因此可以通过该可变引用直接修改 map 中对应的值
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", map);
BTreeMap
类似于HashMap
,但BTreeMap
保证条目按其键排序,key必须实现Ord
特征。而Ord
特征需要实现Eq + PartialOrd
特征。
从 BTreeMap::iter
、 BTreeMap::into_iter
、 BTreeMap::values
或 BTreeMap::keys
等函数获得的迭代器按键顺序生成其项目。
函数
-
声明
1
2
3fn add(a: u32, b: u32) -> u32{
a + b
}函数的位置可以随便放,每个函数参数都需要标注类型,用表达式返回值。
流程控制
-
if
语句1
2
3
4
5
6
7
8
9
10fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};
println!("The value of number is: {}", number);
}- if语句可以作为表达式使用,返回值。使用if返回值时要注意类型要相同。
-
循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29for item in collection // 转移所有权
for item in &collection // 不可变借用
for item in &mut collection // 可变借用
for (i, v) in a.iter().enumerate() {
println!("第{}个元素是{}", i + 1, v);
}
for _ in 0..10 {
// 在 Rust 中 _ 的含义是忽略该值或者类型的意思
}
while n <= 5 {
}
fn main() {
let mut counter = 0;
let result = loop { // loop循环是无限循环,并且可以返回值
counter += 1;
if counter == 10 {
break counter * 2; // break可以返回一个值,类似return
}
};
println!("The result is {}", result);
}
模式匹配
-
match
通用形式1
2
3
4
5
6
7
8
9match target {
模式1 => 表达式1,
模式2 => {
语句1;
语句2;
表达式2
},
_ => 表达式3
}match
本身可作为表达式进行赋值,同时可以从模式中取出绑定的值:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26enum Action {
Say(String),
MoveTo(i32, i32),
ChangeColorRGB(u16, u16, u16),
}
fn main() {
let actions = [
Action::Say("Hello Rust".to_string()),
Action::MoveTo(1,2),
Action::ChangeColorRGB(255,255,0),
];
for action in actions {
match action {
Action::Say(s) => {
println!("{}", s);
},
Action::MoveTo(x, y) => {
println!("point from (0, 0) move to ({}, {})", x, y);
},
Action::ChangeColorRGB(r, g, _) => {
println!("change color into '(r:{}, g:{}, b:0)', 'b' has been ignored", r, g,);
}
}
}
} -
if let
用于只匹配一个条件且忽略其它条件的情况1
2
3
4let v = Some(3u8);
if let Some(3) = v { // 做两件事:匹配,然后赋值,Some(3)可以换成Some(i),这样i会被赋值为3
println!("three");
} -
matches!
宏将一个表达式跟模式进行匹配,然后返回匹配的结果true
orfalse
1
2
3
4
5
6
7
8
fn main() {
let foo = 'f';
assert!(matches!(foo, 'A'..='Z' | 'a'..='z'));
let bar = Some(4);
assert!(matches!(bar, Some(x) if x > 2));
} -
while let
条件循环,它允许只要模式匹配就一直进行while
循环。1
2
3
4
5
6
7
8
9
10
11
12// Vec是动态数组
let mut stack = Vec::new();
// 向数组尾部插入元素
stack.push(1);
stack.push(2);
stack.push(3);
// stack.pop从数组尾部弹出元素
while let Some(top) = stack.pop() {
println!("{}", top);
}
方法
-
可以为结构和枚举定义方法,格式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22struct Circle {
x: f64,
y: f64,
radius: f64,
}
impl Circle {
// new是Circle的关联函数,因为它的第一个参数不是self,且new并不是关键字
// 这种方法往往用于初始化当前结构体的实例
fn new(x: f64, y: f64, radius: f64) -> Circle {
Circle {
x: x,
y: y,
radius: radius,
}
}
// Circle的方法,&self/&mut self表示借用当前的Circle结构体
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
}可以看到方法和结构/枚举的定义是分开的,方法类型名要与对应的结构/枚举名相同。同时,可以多次定义impl块。
如果有泛型,需要在
impl
后用<>
表明泛型参数。 -
方法定义中参数不带self的函数是构造函数,被称为
关联函数
,需要通过::
运算符来调用。一般使用new
作为名字。 -
结构、类型和方法中的成员默认是私有的,需要添加
pub
关键字变为公有。
泛型&特征
泛型的使用
1 | struct Point<T,U> { // 泛型T和U既可以相同,也可以不同 |
const 泛型(针对值的泛型)
1 | fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) { |
特征定义
特征定义了一组可以被共享的行为,只要实现了特征,你就能使用这组行为。
如果不同的类型具有相同的行为,那么我们就可以定义一个特征,然后为这些类型实现该特征。定义特征是把一些方法组合在一起,目的是定义一个实现某些目标所必需的行为的集合。
如果你想要为类型 A
实现特征 T
,那么 A
或者 T
至少有一个是在当前作用域中定义的!
1 | struct Sheep { naked: bool, name: String } |
可以通过#[derive(...)]
为类型派生默认特征实现,如Debug
等
1 |
|
特征约束
在函数定义中,可以为参数进行特征约束:
1 | pub fn notify(item: &impl Summary) { |
可以在指定类型 + 指定特征的条件下去实现方法:
1 | use std::fmt::Display; |
也可以有条件地实现特征:
1 | impl<T: Display> ToString for T { |
特征可以被作为返回值类型被函数返回,但只能返回一个具体类型,多类型需要特征对象:
1 | fn returns_summarizable() -> impl Summary { |
特征对象
-
在针对实现某个特征的所有对象实现方法时,如果只需要同质(相同类型)集合,更倾向于采用泛型+特征约束这种写法,因其实现更清晰,且性能更好。但如果只单纯为实现了某个特征的所有类型实现方法,则应该考虑使用特征对象。可以通过
&dyn
引用或者Box<dyn T>
智能指针的方式来创建特征对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39trait Draw {
fn draw(&self) -> String;
}
impl Draw for u8 {
fn draw(&self) -> String {
format!("u8: {}", *self)
}
}
impl Draw for f64 {
fn draw(&self) -> String {
format!("f64: {}", *self)
}
}
// 若 T 实现了 Draw 特征, 则调用该函数时传入的 Box<T> 可以被隐式转换成函数参数签名中的 Box<dyn Draw>
fn draw1(x: Box<dyn Draw>) {
// 由于实现了 Deref 特征,Box 智能指针会自动解引用为它所包裹的值,然后调用该值对应的类型上定义的 `draw` 方法
x.draw();
}
fn draw2(x: &dyn Draw) {
x.draw();
}
fn main() {
let x = 1.1f64;
// do_something(&x);
let y = 8u8;
// x 和 y 的类型 T 都实现了 `Draw` 特征,因为 Box<T> 可以在函数调用时隐式地被转换为特征对象 Box<dyn Draw>
// 基于 x 的值创建一个 Box<f64> 类型的智能指针,指针指向的数据被放置在了堆上
draw1(Box::new(x));
// 基于 y 的值创建一个 Box<u8> 类型的智能指针
draw1(Box::new(y));
draw2(&x);
draw2(&y);
} -
使用特征对象是由于rust需要在编译期确定函数参数和返回值的大小,通过返回Box或者引用可以将未知大小的特征对象包装成大小固定的引用对象。
-
虽然特征对象没有固定大小,但它的引用类型的大小是固定的,它由两个指针组成(
ptr
和vptr
),因此占用两个指针大小一个指针
ptr
指向实现了特征的具体类型的实例另一个指针
vptr
指向一个虚表vtable
,vtable
中保存了类型的实例对于可以调用的实现于指定特征的方法,且只有这些方法。要注意,此时类型仅仅只是对应特征对象的实例,而不再是原先的类型,因此实例只能调用实现于指定特征的方法,而不能调用类型和类型实现于其他特征的方法。
-
在 Rust 中,有两个
self
,self
指代当前的实例对象,Self
指代特征或者方法类型的别名 -
不是所有特征都能拥有特征对象,只有对象安全的特征才行。当一个特征的所有方法都有如下属性时,它的对象才是安全的:
- 方法的返回类型不能是
Self
- 方法没有任何泛型参数
- 方法的返回类型不能是
-
作为函数参数时,特征约束是静态分发,在编译期就会确定类型,直接使用类型特定的方法;特征对象会动态调用vtable中的方法,会有性能损耗。
深入特征
-
关联类型:关联类型是在特征定义的语句块中,申明一个自定义类型,这样就可以在特征的方法签名中使用该类型。**
Self
用来指代当前调用者的具体类型,那么Self::Item
就用来指代该类型实现中定义的Item
类型。**这种设计可以减少泛型的使用,提高可读性:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
}
}
fn main() {
let c = Counter{..}
c.next()
} -
默认泛型类型参数:当使用泛型类型参数时,可以为其指定一个默认的具体类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17trait Add<RHS=Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
struct Millimeters(u32);
struct Meters(u32);
// 指示类型为Meters
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
} -
调用同名方法:有时候特征和类型的有些方法名相同,可以通过命名空间进行调用,并且rust会优先调用类型上的方法。有些关联函数不能通过self指定,那可以使用完全限定语法调用:
1
<Type as Trait>::function(receiver_if_method, next_arg, ...);
-
特征定义中的特征约束:有时候一个特征的实现依赖于其它特征,这时候就需要通过特征约束来约束特征,被称为
supertrait
1
2
3
4
5
6
7
8
9
10
11
12
13use std::fmt::Display;
trait OutlinePrint: Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
} -
在外部类型上实现外部特征(newtype):由于孤儿规则,我们无法为外部类型实现外部特征。为了绕开这个限制,可以为指定类型多包装一层:为一个元组结构体创建新类型。该元组结构体封装有一个字段,该字段就是希望实现特征的具体类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w);
}同时,Rust 提供了一个特征叫
Deref
,实现该特征后,可以自动做一层类似类型转换的操作,可以将Wrapper
变成Vec<String>
来使用。这样就会像直接使用数组那样去使用Wrapper
,而无需为每一个操作都添加上self.0
。
一些重要的特征
Deref
特征
-
通过为类型
T
实现Deref<Target = U>
,可以告诉编译器&T
和&U
在某种程度上可以互换。 -
在不可变上下文中,
*v
(其中T
既不是引用也不是原始指针)相当于*Deref::deref(&v)
。 -
对
T
的引用隐式转换为对U
的引用(即&T
变为&U
) -
可以调用
&T
中U
上定义的所有以&self
作为输入的方法。 -
一般用于智能指针,在使用时编译器会在合适的地方插入
Deref::deref
方法。 -
pub trait Deref { type Target: ?Sized; // Required method fn deref(&self) -> &Self::Target; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#### `Sized`特征
- **表示该类型的大小在编译时已知。**
- 这个特征为**标记特征**和**自动特征**。
#### `Error`特征
- 用于在`Result`中表示错误类型。
- `source`方法用于跨模块传递错误。
- ```rust
pub trait Error: Debug + Display {
fn source(&self) -> Option<&(dyn Error + 'static)> { ... }
}
#[derive(Debug)]
struct SuperError {
source: SuperErrorSideKick,
}
impl fmt::Display for SuperError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SuperError is here!")
}
}
impl Error for SuperError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
From
/Into
和TryFrom
/TryInto
-
这两对特征是孪生特征:实现了一个,另外一个就会跟着实现
-
pub trait From<T>: Sized { fn from(value: T) -> Self; } pub trait Into<T>: Sized { fn into(self) -> T; } pub trait TryFrom<T>: Sized { type Error; fn try_from(value: T) -> Result<Self, Self::Error>; } pub trait TryInto<T>: Sized { type Error; fn try_into(self) -> Result<T, Self::Error>; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#### `Clone`与`Copy`特征
- **实现`Clone`特征的类型能够完整地复制自身一份,包括栈上内存和管理的堆内存。这使得类型可以显式调用`clone()`方法来复制自己。**
- **`Copy`特征是一个标记特征,它意味着该类型可以隐式地复制自己,使得能够在传递所有权的同时自身仍然能使用。它要求类型实现了`Clone`特征,并且不管理超出其在内存中占用的 `std::mem::size_of` 字节之外的任何其他资源(例如堆内存)。这意味着像String类型不能实现`Copy`特征。**
- **绝大部分情况下,可以直接通过derive派生自动实现`Clone`与`Copy`特征。**
#### `IntoIterator`与`Iterator`特征
- `IntoIterator`特征用于将类型转换为迭代器。
- `Iterator`特征表示该类型为迭代器。
- ```rust
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
trait IntoIterator {
type Item; // 表示每次迭代返回的元素类型
type IntoIter: Iterator<Item = Self::Item>; // std::slice::Iter中的T为slice内部元素的类型
fn into_iter(self) -> Self::IntoIter;
}
Index
与IndexMut
特征
1 | pub trait Index<Idx> |
可以被自动derive派生的特征
- 用于开发者输出的
Debug
- 等值比较的
PartialEq
和Eq
- 次序比较的
PartialOrd
和Ord
- 复制值的
Clone
和Copy
- 固定大小的值映射的
Hash
- 默认值的
Default
生命周期
当你在函数或结构中使用引用时,有时候编译器无法推断引用的生命周期长短,这时候就需要我们手动标注生命周期,以提示编译器引用之间的生命周期关系。
生命周期标注
生命周期标注并不会改变任何引用的实际作用域,仅仅只是对编译器的提示功能,告诉编译器当不满足此约束条件时,就拒绝编译通过!!!
-
函数签名格式如下:这里仅仅说明,x和y至少活得跟周期’a一样长,至于到底活多久或者哪个活得更久,抱歉我们都无法得知
1
2
3
4
5
6
7
8
9// 同泛型一样,需要先声明用到的生命周期<'a>,可以用'_占位
// 有些引用参数并不影响返回引用,可以不标注生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
} -
结构体此时也可以使用引用:只要为结构体中的每一个引用标注上生命周期即可
1
2
3
4
5
6
7
8
9
10
11
12// 该生命周期标注说明,结构体 ImportantExcerpt 所引用的字符串 str 必须比该结构体活得更久。
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
} -
当结构体包含生命周期标注时,生命周期标注本身也成了结构体定义一部分,与泛型一样。
1
2
3
4
5
6
7struct Example<'a, 'b> {
a: &'a u32,
b: &'b NoCopyType
}
fn fix_me<'a>(foo: &Example<'_, 'a>) -> &'a NoCopyType
{ foo.b } -
方法的生命周期标注格式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21struct ImportantExcerpt<'a> {
part: &'a str,
}
// 当'a和'b有明显先后关系时可以这样表示,用于说明 'a 必须比 'b 活得久
impl<'a: 'b, 'b> ImportantExcerpt<'a> {
fn announce_and_return_part(&'a self, announcement: &'b str) -> &'b str {
println!("Attention please: {}", announcement);
self.part
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'b str
where
'a: 'b,
{
println!("Attention please: {}", announcement);
self.part
}
} -
静态生命周期:
'static
,拥有该生命周期的引用可以和整个程序活得一样久。实在遇到解决不了的生命周期标注问题,可以尝试T: 'static
,有时候它会给你奇迹。
生命周期消除
编译器为了简化用户的使用,运用了生命周期消除大法。这使得有些情况下我们不必为引用标注生命周期。目前消除规则仅针对函数,结构体仍需手动标注。
有2点需要注意:
- 消除规则不是万能的,若编译器不能确定某件事是正确时,会直接判为不正确,那么你还是需要手动标注生命周期
- 函数或者方法中,参数的生命周期被称为
输入生命周期
,返回值的生命周期被称为输出生命周期
以下是消除规则:
- **每一个引用参数都会获得独自的生命周期。**两个引用参数的有两个生命周期标注:
fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
- 若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期。函数
fn foo(x: &i32) -> &i32
等同于fn foo<'a>(x: &'a i32) -> &'a i32
- 若存在多个输入生命周期,且其中一个是
&self
或&mut self
,则&self
的生命周期被赋给所有的输出生命周期。这使得大多数方法不必标注生命周期。
返回值和错误处理
panic!
异常终止
-
可以主动进行调用:
1
2
3fn main() {
panic!("crash and burn");
} -
默认情况下,在提供命令行参数的情况下,
Debug
模式触发panic!
时会打印栈帧方便分析:- Linux/macOS 等 UNIX 系统:
RUST_BACKTRACE=1 cargo run
- Windows 系统(PowerShell):
$env:RUST_BACKTRACE=1 ; cargo run
- Linux/macOS 等 UNIX 系统:
-
当出现
panic!
时,程序提供了两种方式来处理终止流程:栈展开和直接终止。默认是栈展开。但可以修改Cargo.toml
文件,实现在release
模式下遇到panic
直接终止:1
2[profile.release]
panic = 'abort' -
main
线程触发panic
会终止整个程序,子线程触发只会终止整个线程。 -
当调用
panic!
宏时,它会:- 格式化
panic
信息,然后使用该信息作为参数,调用std::panic::panic_any()
函数 panic_any
会检查应用是否使用了panic hook
,如果使用了,该hook
函数就会被调用(hook
是一个钩子函数,是外部代码设置的,用于在panic
触发时,执行外部代码所需的功能)- 当
hook
函数返回后,当前的线程就开始进行栈展开:从panic_any
开始,如果寄存器或者栈因为某些原因信息错乱了,那很可能该展开会发生异常,最终线程会直接停止,展开也无法继续进行 - 展开的过程是一帧一帧的去回溯整个栈,每个帧的数据都会随之被丢弃,但是在展开过程中,你可能会遇到被用户标记为
catching
的帧(通过std::panic::catch_unwind()
函数标记),此时用户提供的catch
函数会被调用,展开也随之停止:当然,如果catch
选择在内部调用std::panic::resume_unwind()
函数,则展开还会继续。
还有一种情况,在展开过程中,如果展开本身
panic
了,那展开线程会终止,展开也随之停止。一旦线程展开被终止或者完成,最终的输出结果是取决于哪个线程
panic
:对于main
线程,操作系统提供的终止功能core::intrinsics::abort()
会被调用,最终结束当前的panic
进程;如果是其它子线程,那么子线程就会简单的终止,同时信息会在稍后通过std::thread::join()
进行收集。 - 格式化
Result
和?
-
Result
常用于函数返回值进行错误处理,定义如下:1
2
3
4enum Result<T, E> {
Ok(T),
Err(E),
} -
使用
unwrap()
和expect()
获取返回值,出现错误直接触发panic
:1
2
3
4
5
6
7
8use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap(); // 获取OK(T)中的T,如果是Err则直接panic
// 同unwrap,只不过可以自定义错误提示信息
let f = File::open("hello.txt").expect("Failed to open hello.txt");
} -
使用
?
自动解析返回值和返回错误:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
// ?是一个宏,约等价于
/*
let mut f = match f {
// 打开文件成功,将file句柄赋值给f
Ok(file) => file,
// 打开文件失败,将错误返回(向上传播)
Err(e) => return Err(e),
};
*/ -
?
会自动获取正确的值,当出现错误时会直接返回错误。这使得它在函数正常返回时能够将返回值自动提取并赋值,当出现错误时会终止函数并返回错误,及时止损,达成链式调用。使用形式:1
2let v = xxx()?;
xxx()?.yyy()?; -
?
可以对错误类型进行隐式类型转换。因此只要函数返回的错误ReturnError
实现了From<OtherError>
特征,那么?
就会自动把OtherError
转换为ReturnError
。这种转换非常好用,意味着你可以用一个大而全的ReturnError
来覆盖所有错误类型,只需要为各种子错误类型实现这种转换即可。1
2
3
4fn open_file() -> Result<File, Box<dyn std::error::Error>> {
let mut f = File::open("hello.txt")?;
Ok(f)
} -
?
也可以被用来处理Option
,效果差不多。
包和模块
-
mod xxx;
用于将项目内的模块导入进该文件,类似于import xxx
。 -
use xxx::yyy;
用于导入模块路径方便调用,以及引入第三方模块,类似于from xxx import yyy
。特别地,可以用use xxx::*;
导入xxx模块所有内容。当引入多个项时,可以用大括号括起来:use xxx::{self, yyy};
。同时,可以为引入的模块取别名:use xxx::yyy as zzz;
-
为了引入第三方模块,需要在
Cargo.toml
的[dependencies]
进行添加,VSCode
+rust-analyzer
会自动拉取。 -
引入本地模块时,可以通过绝对路径或相对路径进行引入:
- 绝对路径,从包根开始,路径名以包名或者
crate
作为开头 - 相对路径,从当前模块开始,以
self
,super
或当前模块的标识符作为开头
- 绝对路径,从包根开始,路径名以包名或者
-
将文件夹作为模块引入时,需要在同级目录下设有与文件夹同名的
.rs
文件。
注释与文档
-
代码注释
1
2
3
4// 这是行注释
/*
这是块注释
*/ -
文档注释:在代码中可以额外书写文档注释,并可以使用
cargo doc
生成HTML网页文档(可以使用cargo doc --open
自动生成网页并打开)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29/// 这是文档行注释
/// `add_one` 将指定值加1
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
/** 这是文档块注释
`add_two` 将指定值加2
```
let arg = 5;
let answer = my_crate::add_two(arg);
assert_eq!(7, answer);
```
*/
//! 这是包级行注释
/*! 这是包级块注释 */
/*!
包级注释要添加到包、模块的最上方!如lib.rs的最上方
*/- 文档注释需要位于
lib
类型的包中,例如src/lib.rs
中 - 文档注释可以使用
markdown
语法!例如# Examples
的标题,以及代码块高亮 - 被注释的对象需要使用
pub
对外可见,记住:文档注释是给用户看的,内部实现细节不应该被暴露出去
- 文档注释需要位于
-
文档测试
-
文档注释的代码跳转
格式化输出
闭包与迭代器
闭包
-
闭包类似于python的lambda函数,可以快速创建一个简单函数等。与一般函数不同的是,闭包可以捕获作用域中的变量。
1
2
3
4
5
6
7|param1, param2,...| {
语句1;
语句2;
返回表达式
}
|param1| 返回表达式 -
闭包的类型会被编译器自动推断(也可手动定义),一旦类型被推断之后就无法被修改,也就是参数和返回值类型都已固定。同时,每个闭包都有单独的一个类型,即使两个闭包完全一样,也会生成不同的类型。
-
闭包的类型可以用三个特征来表示:
FnOnce
、FnMut
、Fn
。分别表示按值捕获变量、按可变引用捕获变量、按不可变引用捕获变量。一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们。 -
如果你想强制闭包取得捕获变量的所有权,可以在参数列表前添加
move
关键字,这种用法通常用于闭包的生命周期大于捕获变量的生命周期时,例如将闭包返回或移入其他线程。 -
如果闭包没有实现Copy特征,那么在调用一次之后会转移所有权,无法再次被调用。只要闭包捕获的类型都实现了
Copy
特征的话,这个闭包就会默认实现Copy
特征。
迭代器
-
与迭代器相关的有两个主要特征:
IntoIterator
和Iterator
特征。实现了前者表示该类型可以转化为迭代器,实现了后者表示该类型是迭代器。 -
实现了
IntoIterator
特征的类型可以通过into_iter
、iter
、iter_mut
三个方法生成不同的迭代器:into_iter
会夺走所有权、iter
是借用、iter_mut
是可变借用。1
2
3
4
5
6
7
8
9
10
11impl<I: Iterator> IntoIterator for I {
type Item = I::Item; // 表示每次迭代返回的元素类型
type IntoIter = I; // std::slice::Iter中的T为slice内部元素的类型
fn into_iter(self) -> I {
self
}
...
} -
实现了
Iterator
特征的类型为迭代器,可以被for
循环使用,并且可以使用大量属于它的方法。要为自定义集合类型实现Iterator
特征,最关键的是实现next
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 省略其余有默认实现的方法
}
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}只要实现了
next
方法,Iterator
其它方法都有基于next
方法的默认实现,无需重复实现! -
next
方法是消耗性的,每次迭代都会消耗掉迭代器的一个元素,并返回Option
枚举,当迭代器空时,便返回None
。基于next
方法的Iterator
方法也会消耗迭代器元素。 -
Iterator
的其它方法大致可分为消费者适配器和迭代器适配器。前者会消耗迭代器,后者会返回一个新迭代器。由于迭代器的惰性性质,迭代器适配器需要接一个消费者适配器来消耗,否则会报错。 -
消费者适配器:
sum
、collect
、enumerate
、zip
等,其中collect
需要手动标注返回类型。 -
迭代器适配器:
map
、filter
等,这些会接受一个闭包。
深入类型
类型转换
-
可以使用
as
关键字和TryInto
特征(可以用于处理转换错误)进行内置转换。一般用于小数值扩展到大数值。1
2
3
4
5
6
7fn main() {
let a = 3.1 as i8;
let b = 100_i8 as i32;
let c = 'a' as u8; // 将字符'a'转换为整数,97
println!("{},{},{}",a,b,c)
}1
2
3
4
5
6
7
8
9
10
11fn main() {
let b: i16 = 1500;
let b_: u8 = match b.try_into() {
Ok(b1) => b1,
Err(e) => {
println!("{:?}", e.to_string());
0
}
};
} -
编译器处理方法调用步骤:
- 首先,编译器检查它是否可以直接调用
T::foo(value)
,称之为值方法调用 - 如果上一步调用无法完成(例如方法类型错误或者特征没有针对
Self
进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,例如会尝试以下调用:<&T>::foo(value)
和<&mut T>::foo(value)
,称之为引用方法调用 - 若上面两个方法依然不工作,编译器会试着解引用
T
,然后再进行尝试。这里使用了Deref
特征 —— 若T: Deref<Target = U>
(T
可以被解引用为U
),那么编译器会使用U
类型进行尝试,称之为解引用方法调用 - 若
T
不能被解引用,且T
是一个定长类型(在编译期类型长度是已知的),那么编译器也会尝试将T
从定长类型转为不定长类型,例如将[i32; 2]
转为[i32]
- 首先,编译器检查它是否可以直接调用
-
无视类型检查的强制转换:
mem::transmute<T, U>
和mem::transmute_copy<T, U>
,属于unsafe!
枚举与整数
要想实现像C语言那样的由整数组成的枚举,需要做一些转化。
-
枚举可以轻松转化成整数(如使用
as
),但整数无法直接转换成枚举。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18enum MyEnum {
A = 1,
B,
C,
}
fn main() {
// 将枚举转换成整数,顺利通过
let x = MyEnum::C as i32;
// 将整数转换为枚举,失败
match x {
MyEnum::A => {}
MyEnum::B => {}
MyEnum::C => {}
_ => {}
}
} -
将整数转换成枚举,可以使用第三方库:
num_enums
-
除此之外,也可以使用
TryFrom
+ 宏 或者std::mem::transmute
来进行转化。
智能指针
智能指针比一般的引用增加了一些特定的功能,它往往是基于结构体实现,它与我们自定义的结构体最大的区别在于它实现了 Deref
和 Drop
特征。
Rust 规则 | 智能指针带来的额外规则 |
---|---|
一个数据只有一个所有者 | Rc/Arc 让一个数据可以拥有多个所有者 |
要么多个不可变借用,要么一个可变借用 | RefCell 实现编译期可变、不可变引用共存 |
违背规则导致编译错误 | 违背规则导致运行时panic |
Box<T> 堆对象分配
- Box智能指针可以强制将对象分配到堆中。
- Box智能指针将动态大小类型变为 Sized 固定大小类型。
Box::leak
:它可以消费掉Box
并且强制包含的对象从内存中泄漏。可以用于一个在运行期初始化的值,但是可以全局有效,也就是和整个程序活得一样久。例如,在运行时加载的配置信息。可以使用Box::from_raw
进行回收。
Deref 解引用
- 实现了
Deref
特征的引用类型可以自动解引用。 - 一个类型为
T
的对象foo
,如果T: Deref<Target=U>
,那么,相关foo
的引用&foo
在应用的时候会自动转换为&U
。也就是说,解引用可以得到不同类型的引用。 - 当
T: Deref<Target=U>
,可以将&T
转换成&U
。 - 当
T: DerefMut<Target=U>
,可以将&mut T
转换成&mut U
。 - 当
T: Deref<Target=U>
,可以将&mut T
转换成&U
。
Drop 释放资源
- Drop特征的drop方法类似于析构函数。编译器会自动为几乎所有类型实现drop方法,即使你自行定义了drop方法,也会调用默认生成的drop方法。
- 可以通过调用
std::mem::drop
方法来手动释放变量,原理是传入所有权而不带出,离开作用域后自行调用std::ops::Drop::drop()
实现。 - 无法为一个类型同时实现
Copy
和Drop
特征。这是出于内存安全来考虑的。
Rc与Arc实现1对多所有权
-
当我们**希望在堆上分配一个对象供程序的多个部分使用且无法确定哪个部分最后一个结束时,就可以使用智能指针
Rc
成为数据值的所有者。**例如,多线程、图数据结构时。这与多个不可变引用不是一个概念,多个不可变引用更多是作为“别名”的概念,不等同于同时拥有数据的所有权。**使用Rc::clone
方法复制Rc
指针。**具体用法如下:1
2
3
4
5
6
7
8use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("hello, world"));
let b = Rc::clone(&a);
assert_eq!(2, Rc::strong_count(&a));
assert_eq!(Rc::strong_count(&a), Rc::strong_count(&b))
} -
Rc/Arc
是不可变引用,一旦最后一个拥有者消失,则资源会自动被回收,这个生命周期是在编译期就确定下来的。 -
Rc 只能用于同一线程内部,想要用于线程之间的对象共享,你需要使用 Arc,用法几乎相同。
-
Rc 是一个智能指针,实现了 Deref 特征,因此你无需先解开 Rc 指针,再使用里面的 T,而是可以直接使用 T。
Cell与RefCell
允许在存在不可变引用的情况下对数据进行有限制的可变性,主要用于内部可变性。即在表面不可变的对象内部实现可变性。
-
**
Cell<T>
适用于T
实现Copy
的情况,也就是说当T为简单类型时首选Cell<T>
。**它永远不会提供&mut T
和产生panic
。基本用法:1
2
3
4
5
6
7
8use std::cell::Cell;
fn main() {
let c = Cell::new("asdf");
let one = c.get(); // 通过直接复制值来返回
c.set("qwer");
let two = c.get();
println!("{},{}", one, two);
} -
RefCell<T>
适用于复杂类型,它产生引用,它将对不可变引用和可变引用的制约关系推断推迟到运行期检测:1
2
3
4
5
6
7
8
9use std::cell::RefCell;
fn main() {
let s = RefCell::new(String::from("hello, world"));
let s1 = s.borrow();
let s2 = s.borrow_mut(); // 运行时报错,不能同时出现不可变和可变
println!("{},{}", s1, s2);
} -
Cell
容器主要是为了解决内部可变性问题。**例如,对于一个结构,它的大部分内容都是不需要改变的,但内部有一些内容需要改变,这使得在一些只接受结构的不可变引用参数的方法和函数是无法更改其内部字段的。**此时,就可以Cell
系列容器:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26use std::cell::RefCell;
// 定义在外部库中的特征
pub trait Messenger {
// &self, 整体的不可变借用
fn send(&self, msg: String);
}
// --------------------------
// 我们的代码中的数据结构和实现
pub struct MsgQueue {
msg_cache: RefCell<Vec<String>>,
}
impl Messenger for MsgQueue {
fn send(&self, msg: String) {
// 绕过整体的不可变借用,实现了内部可变性
self.msg_cache.borrow_mut().push(msg)
}
}
fn main() {
let mq = MsgQueue {
msg_cache: RefCell::new(Vec::new()),
};
mq.send("hello, world".to_string());
} -
Rc容器是不可变的,可以在Rc容器内部引入Cell容器来获取可变性。
1
2
3
4
5
6
7
8
9
10
11
12use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let s = Rc::new(RefCell::new("我很善变,还拥有多个主人".to_string()));
let s1 = s.clone();
let s2 = s.clone();
// let mut s2 = s.borrow_mut();
s2.borrow_mut().push_str(", oh yeah!");
println!("{:?}\n{:?}\n{:?}", s, s1, s2);
} -
Cell::from_mut
,该方法将&mut T
转为&Cell<T>
。Cell::as_slice_of_cells
,该方法将&Cell<[T]>
转为&[Cell<T>]
。两个方法结合起来可以将&mut [T]
类型转换成&[Cell<T>]
类型。 -
Cell
容器没有线程安全特性。
循环引用与自引用
Weak 与循环引用
-
当出现循环引用时,Rc容器会导致引用计数无法正确归零,对象所占内存无法释放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27use crate::List::{Cons, Nil};
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a的初始化rc计数 = {}", Rc::strong_count(&a));
println!("a指向的节点 = {:?}", a.tail());
// 创建`b`到`a`的引用
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("在b创建后,a的rc计数 = {}", Rc::strong_count(&a));
println!("b的初始化rc计数 = {}", Rc::strong_count(&b));
println!("b指向的节点 = {:?}", b.tail());
// 利用RefCell的可变性,创建了`a`到`b`的引用
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("在更改a后,b的rc计数 = {}", Rc::strong_count(&b));
println!("在更改a后,a的rc计数 = {}", Rc::strong_count(&a));
// 下面一行println!将导致循环引用
// 8MB大小的main线程栈空间将被它冲垮,最终造成栈溢出
// println!("a next item = {:?}", a.tail());
} -
为了解决
Rc
容器循环引用造成的内存泄露,可以使用Weak
容器。Weak
不持有所有权,它仅仅保存一份指向数据的弱引用:如果你想要访问数据,需要通过Weak
指针的upgrade
方法实现,该方法返回一个类型为Option<Rc<T>>
的值。 -
使用方式简单总结下:对于父子引用关系,可以让父节点通过
Rc
来引用子节点,然后让子节点通过Weak
来引用父节点:- 可访问,但没有所有权,不增加引用计数,因此不会影响被引用值的释放回收
- 可由
Rc<T>
调用downgrade
方法转换成Weak<T>
Weak<T>
可使用upgrade
方法转换成Option<Rc<T>>
来获取指向的对象,如果资源已经被释放,则Option
的值是None
- 常用于解决循环引用的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52use std::rc::Rc;
use std::rc::Weak;
use std::cell::RefCell;
// 主人
struct Owner {
name: String,
gadgets: RefCell<Vec<Weak<Gadget>>>,
}
// 工具
struct Gadget {
id: i32,
owner: Rc<Owner>,
}
fn main() {
// 创建一个 Owner
// 需要注意,该 Owner 也拥有多个 `gadgets`
let gadget_owner : Rc<Owner> = Rc::new(
Owner {
name: "Gadget Man".to_string(),
gadgets: RefCell::new(Vec::new()),
}
);
// 创建工具,同时与主人进行关联:创建两个 gadget,他们分别持有 gadget_owner 的一个引用。
let gadget1 = Rc::new(Gadget{id: 1, owner: gadget_owner.clone()});
let gadget2 = Rc::new(Gadget{id: 2, owner: gadget_owner.clone()});
// 为主人更新它所拥有的工具
// 因为之前使用了 `Rc`,现在必须要使用 `Weak`,否则就会循环引用
gadget_owner.gadgets.borrow_mut().push(Rc::downgrade(&gadget1));
gadget_owner.gadgets.borrow_mut().push(Rc::downgrade(&gadget2));
// 遍历 gadget_owner 的 gadgets 字段
for gadget_opt in gadget_owner.gadgets.borrow().iter() {
// gadget_opt 是一个 Weak<Gadget> 。 因为 weak 指针不能保证他所引用的对象
// 仍然存在。所以我们需要显式的调用 upgrade() 来通过其返回值(Option<_>)来判
// 断其所指向的对象是否存在。
// 当然,Option 为 None 的时候这个引用原对象就不存在了。
let gadget = gadget_opt.upgrade().unwrap();
println!("Gadget {} owned by {}", gadget.id, gadget.owner.name);
}
// 在 main 函数的最后,gadget_owner,gadget1 和 gadget2 都被销毁。
// 具体是,因为这几个结构体之间没有了强引用(`Rc<T>`),所以,当他们销毁的时候。
// 首先 gadget2 和 gadget1 被销毁。
// 然后因为 gadget_owner 的引用数量为 0,所以这个对象可以被销毁了。
// 循环引用问题也就避免了
}
结构体自引用
-
结构体中同时存在一个值及其对应的引用,会导致所有权转移和借用相互冲突。
-
使用裸指针+
unsafe
+Pin
来作为安全实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44use std::marker::PhantomPinned;
use std::pin::Pin;
use std::ptr::NonNull;
// 下面是一个自引用数据结构体,因为 slice 字段是一个指针,指向了 data 字段
// 我们无法使用普通引用来实现,因为违背了 Rust 的编译规则
// 因此,这里我们使用了一个裸指针,通过 NonNull 来确保它不会为 null
struct Unmovable {
data: String,
slice: NonNull<String>,
_pin: PhantomPinned,
}
impl Unmovable {
// 为了确保函数返回时数据的所有权不会被转移,我们将它放在堆上,唯一的访问方式就是通过指针
fn new(data: String) -> Pin<Box<Self>> {
let res = Unmovable {
data,
// 只有在数据到位时,才创建指针,否则数据会在开始之前就被转移所有权
slice: NonNull::dangling(),
_pin: PhantomPinned,
};
let mut boxed = Box::pin(res);
let slice = NonNull::from(&boxed.data);
// 这里其实安全的,因为修改一个字段不会转移整个结构体的所有权
unsafe {
let mut_ref: Pin<&mut Self> = Pin::as_mut(&mut boxed);
Pin::get_unchecked_mut(mut_ref).slice = slice;
}
boxed
}
}
fn main() {
let unmoved = Unmovable::new("hello".to_string());
// 只要结构体没有被转移,那指针就应该指向正确的位置,而且我们可以随意移动指针
let mut still_unmoved = unmoved;
assert_eq!(still_unmoved.slice, NonNull::from(&still_unmoved.data));
// 因为我们的类型没有实现 `Unpin` 特征,下面这段代码将无法编译
// let mut new_unmoved = Unmovable::new("world".to_string());
// std::mem::swap(&mut *still_unmoved, &mut *new_unmoved);
} -
可以使用第三方库:ouroboros、rental、owning-ref
错误处理
组合器
用于对返回结果的类型(Option和Result)进行变换。
or()
和and()
:or()
,表达式按照顺序求值,若任何一个表达式的结果是Some
或Ok
,则该值会立刻返回;and()
,若两个表达式的结果都是Some
或Ok
,则第二个表达式中的值被返回。若任何一个的结果是None
或Err
,则立刻返回。or_else()
和and_then()
:它们跟or()
和and()
类似,唯一的区别在于,它们的第二个表达式是一个闭包,会调用闭包,获取返回值。filter
:filter
用于对Option
进行过滤map()
和map_err()
:map
可以将Some
或Ok
中的值映射为另一个;map_err
用于Err
map_or()
和map_or_else()
:map_or
在map
的基础上提供了一个默认值;map_or_else
与map_or
类似,但是它是通过一个闭包来提供默认值。ok_or()
和ok_or_else()
:这两兄弟可以将Option
类型转换为Result
类型。其中ok_or
接收一个默认的Err
参数;而ok_or_else
接收一个闭包作为Err
参数。
自定义错误类型
-
自定义错误类型只需要实现
Debug
和Display
特征即可。 -
为自定义类型实现
From
特征以将其它错误类型转化为自定义类型:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28use std::fs::File;
use std::io;
struct AppError {
kind: String, // 错误类型
message: String, // 错误信息
}
// 为 AppError 实现 std::convert::From 特征,由于 From 包含在 std::prelude 中,因此可以直接简化引入。
// 实现 From<io::Error> 意味着我们可以将 io::Error 错误转换成自定义的 AppError 错误
impl From<io::Error> for AppError {
fn from(error: io::Error) -> Self {
AppError {
kind: String::from("io"),
message: error.to_string(),
}
}
}
fn main() -> Result<(), AppError> {
let _file = File::open("nonexistent_file.txt")?;
Ok(())
}
// --------------- 上述代码运行后输出 ---------------
// Error: AppError { kind: "io", message: "No such file or directory (os error 2)" }
归一化不同的错误类型
当一个函数产生不同的错误类型时,返回错误类型需要做归一化:
-
使用特征对象
Box<dyn Error>
-
自定义错误类型,需要实现:
impl std::error::Error for MyError {}
-
如果你想要设计自己的错误类型,同时给调用者提供具体的信息时,就使用
thiserror
,例如当你在开发一个三方库代码时。如果你只想要简单,就使用anyhow
,例如在自己的应用服务中。
使用thiserror
- 为自定义错误类型(一般是错误类型枚举)自动派生
Error
特征:#[derive(thiserror::Error)]
#[error("{0}")]
:这是为自定义错误类型的每个枚举变量定义Display
实现的语法。当显示错误时,{0}
将被变体的第 0 个字段替换。- 可以添加
#[source]
或#[from]
来自动实现Error
特征的source
方法。
unsafe rust
https://blog.logrocket.com/unsafe-rust-how-and-when-not-to-use-it/
主要功能:
- 解引用裸指针
- 调用一个
unsafe
或外部的函数 - 访问或修改一个可变的静态变量
- 实现一个
unsafe
特征 - 访问
union
中的字段
在unsafe中使用引用仍然会触发借用检查。
裸指针
-
裸指针类型格式:
*const T
和*mut T
,它们分别代表了不可变和可变。 -
裸指针可以绕过Rust的借用规则,可以同时拥有一个数据的可变、不可变指针,甚至还能拥有多个可变的指针
-
创建裸指针是安全的行为,而解引用裸指针才是不安全的行为
-
基于引用创建裸指针:
1
2
3
4let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32; -
基于智能指针创建裸指针:
1
2
3
4
5let a: Box<i32> = Box::new(10);
// 需要先解引用a
let b: *const i32 = &*a;
// 使用 into_raw 来创建
let c: *const i32 = Box::into_raw(a);
unsafe函数、方法与特征
-
当调用unsafe函数时,你需要注意它的相关需求,因为 Rust 无法担保调用者在使用该函数时能满足它所需的一切需求。
-
unsafe
无需套娃,在unsafe
函数体中使用unsafe
语句块是多余的行为。 -
之所以会有
unsafe
的特征,是因为该特征至少有一个方法包含有编译器无法验证的内容。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15unsafe fn dangerous() {}
unsafe trait Foo {
// 方法列表
}
unsafe impl Foo for i32 {
// 实现相应的方法
}
fn main() {
unsafe {
dangerous();
}
}
FFI(Foreign Function Interface)
-
Rust调用外部函数:
1
2
3
4
5
6
7
8
9extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
} -
在其它语言调用Rust函数:
1
2
3
4
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
实用工具(库)
- rust-bindgen 和 cbindgen:对于
FFI
调用来说,保证接口的正确性是非常重要的,这两个库可以帮我们自动生成相应的接口,其中rust-bindgen
用于在 Rust 中访问 C 代码,而cbindgen
则反之。 - cxx:与C++交互,使用
cxx
,它提供了双向的调用,最大的优点就是安全:无需通过unsafe
来使用它!但它没有公开所有Rust方法等。 - Autocxx:bindgen和cxx的结合,可以以最少的工作在rust中使用C++库。
- Zngur:它尝试公开任意 Rust 类型、方法和函数,同时尽可能保留其语义和人体工程学。使用 Zngur,您可以在 C++ 代码中使用任意 Rust 包,就像在普通 Rust 代码中使用它一样容易,并且您可以在 C++ 中为 C++ 库编写惯用的 Rusty API。
- Miri:
miri
可以生成并模拟执行 Rust 的中间层表示 MIR。它可以帮助检查常见的未定义行为。 - Clippy:官方的
clippy
检查器提供了比rustc
更强大的静态检查,其中包括有限的unsafe
支持。原理 - Prusti:
prusti
需要大家自己来构建一个证明,然后通过它证明代码中的不变量是正确被使用的,当你在安全代码中使用不安全的不变量时,就会非常有用。具体的使用文档见这里。 - 模糊测试(fuzz testing):在 Rust Fuzz Book 中列出了一些 Rust 可以使用的模糊测试方法。同时,我们还可以使用
rutenspitz
这个过程宏来测试有状态的代码,例如数据结构。
内联汇编
course、Rust Reference、Rust By Example
Macro宏编程
特点:元编程、可变参数、编译期宏展开。
分类:声明式宏、过程宏(syntax extensions,语法扩展)。前者转化源码,后者生成源码。
声明式宏 macro_rules!
reference、The Little Book of Rust Macros
宏的声明在语法上类似match表达式。宏接受一段rust代码,并对这段代码进行匹配,一旦匹配,传入宏的那段源代码将被模式关联的代码所替换,最终实现宏展开。也就是说,传入宏的这段源代码最终会被转化成另一段源代码,类似于C/C++中的宏。在调用上,类似与函数调用,只不过在名称后加!
。如:println!
。
1 |
|
过程宏
过程宏允许在编译时运行通过 Rust 语法运行的代码,生成 Rust 语法。可以将过程宏视为从一个 AST 到另一个 AST 的函数。过程宏必须在 crate 类型为 proc-macro
的 lib crate 中定义。可以选择嵌套crate来创建自定义过程宏。
可以使用cargo-expand
展开宏,查看自定义宏的正确性:
1 | cargo install cargo-expand |
定义过程宏,需要在crate的Cargo.toml
中添加:
1 | [lib] |
对于过程宏定义,大致框架:
1 | extern crate proc_macro; |
derive
过程宏
只能用于struct、enum、union。
例子:
1 | fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream { |
属性宏
可以接受属性参数来生成代码。
使用:
1 | // Get, "/"都是属性 |
定义:
1 |
|
类函数宏
可以像函数那样调用,将参数转化为TokenStream,并生成新的代码。可以生成函数、闭包等。
使用:
1 | let sql = sql!(SELECT * FROM posts WHERE id=1); |
定义:
1 |
|
多线程并发编程
使用多线程
-
基本使用。要注意:因为Rust无法确定线程的存活时间,所以传递给线程的变量必须要转移它的所有权,也就是说线程要拿到变量所有权!这时候就需要move关键字。(或者,保证传递的是’static引用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15use std::thread;
fn main() {
let v = vec![1, 2, 3];
// 线程使用的变量必须要获取它的所有权,用move关键字表示所有权的转移。
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap(); // 让主线程阻塞,等待子线程返回退出,join()返回子线程的返回值。
// 下面代码会报错borrow of moved value: `v`
// println!("{:?}",v);
} -
可以使用作用域线程确保线程作用域不超过父线程,从而更方便地使用父线程的变量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16let v = vec![1, 2, 3];
let midpoint = v.len() / 2;
// 使用 Scope 生成的所有线程将在作用域结束时自动join。
std::thread::scope(|scope| {
scope.spawn(|| {
let first = &v[..midpoint];
println!("Here's the first half of v: {first:?}");
});
scope.spawn(|| {
let second = &v[midpoint..];
println!("Here's the second half of v: {second:?}");
});
});
println!("Here's v: {v:?}"); -
当主线程结束时,其它子线程会强制结束,否则子线程会一直等到执行完再结束。
-
可以使用
Barrier
让多个线程都执行到某个点后,才继续一起往后执行:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20use std::sync::{Arc, Barrier};
use std::thread;
fn main() {
let mut handles = Vec::with_capacity(6);
let barrier = Arc::new(Barrier::new(6));
for _ in 0..6 {
let b = barrier.clone();
handles.push(thread::spawn(move|| {
println!("before wait");
b.wait();
println!("after wait");
}));
}
for handle in handles {
handle.join().unwrap();
}
} -
使用
thread_local
宏可以初始化线程局部变量(每个线程都可持有的独立局部变量),然后在线程内部使用该变量的with
方法获取变量值。这种方法只能通过引用来使用变量:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25use std::cell::RefCell;
use std::thread;
thread_local!(static FOO: RefCell<u32> = RefCell::new(1));
FOO.with(|f| {
assert_eq!(*f.borrow(), 1);
*f.borrow_mut() = 2;
});
// 每个线程开始时都会拿到线程局部变量的FOO的初始值
let t = thread::spawn(move|| {
FOO.with(|f| {
assert_eq!(*f.borrow(), 1);
*f.borrow_mut() = 3;
});
});
// 等待线程完成
t.join().unwrap();
// 尽管子线程中修改为了3,我们在这里依然拥有main线程中的局部值:2
FOO.with(|f| {
assert_eq!(*f.borrow(), 2);
}); -
可以在结构体中使用线程局部变量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31use std::cell::RefCell;
// 方法一
struct Foo;
impl Foo {
thread_local! {
static FOO: RefCell<usize> = RefCell::new(0);
}
}
fn main() {
Foo::FOO.with(|x| println!("{:?}", x));
}
// 方法二
use std::cell::RefCell;
use std::thread::LocalKey;
thread_local! {
static FOO: RefCell<usize> = RefCell::new(0);
}
struct Bar {
foo: &'static LocalKey<RefCell<usize>>,
}
impl Bar {
fn constructor() -> Self {
Self {
foo: &FOO,
}
}
} -
可以使用 thread-local 库,它允许每个线程持有线程局部变量的值的独立拷贝。
-
如果需要某个函数在多线程环境下只被调用一次,可以使用Once类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28use std::thread;
use std::sync::Once;
static mut VAL: usize = 0;
static INIT: Once = Once::new();
fn main() {
let handle1 = thread::spawn(move || {
INIT.call_once(|| { // INIT.call_once意味着这个方法只会被调用一次
unsafe {
VAL = 1;
}
});
});
let handle2 = thread::spawn(move || {
INIT.call_once(|| {
unsafe {
VAL = 2;
}
});
});
handle1.join().unwrap();
handle2.join().unwrap();
println!("{}", unsafe { VAL });
}
线程同步:消息传递
线程之间可以通过信道来通信,从而实现彼此之间的交互、内存访问、同步性等。它类似于一种多线程单所有权机制。
-
标准库提供了通道
std::sync::mpsc
,该通道支持多个发送者,但是只支持唯一的接收者。通道只能发送和接收一个特定类型的数据。接收消息的操作rx.recv()
会阻塞当前线程,直到读取到值,或者通道被关闭。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21use std::sync::mpsc;
use std::thread;
fn main() {
// 创建一个消息通道, 返回一个元组:(发送者,接收者)
let (tx, rx) = mpsc::channel();
// 因为会转移所有权,所以多发送者需要clone
let tx1 = tx.clone();
// 需要使用move将tx的所有权转移到子线程的闭包中
thread::spawn(move || {
tx.send(String::from("hi from raw tx")).unwrap();
});
thread::spawn(move || {
tx1.send(String::from("hi from cloned tx")).unwrap();
});
for received in rx {
println!("Got: {}", received);
}
} -
可以使用
try_recv
尝试接收一次消息,该方法并不会阻塞线程,当通道中没有消息时,它会立刻返回一个错误。 -
对于传递给通道的值,若值的类型实现了
Copy
特征,则直接复制一份该值,然后传输过去;若值没有实现Copy
,则它的所有权会被转移给接收端,在发送端继续使用该值将报错。 -
mpsc::channel()
为异步通道,缓冲区无限,无论接收者是否正在接收消息,消息发送者在发送消息时都不会阻塞;mpsc::sync_channel
则为同步通道,同步通道发送消息是阻塞的,只有在消息被接收后才解除阻塞,参数为缓冲区大小。 -
所有发送者被
drop
或者所有接收者被drop
后,通道会自动关闭。 -
可以使用多个通道或枚举来实现传输多种类型的数据。
-
如果需要多发送者,多接收者,可以考虑crossbeam-channel库。
线程同步:锁、Condvar 和信号量
线程之间可以通过共享内存(并发原语)来实现线程之间的交互。实现简洁、性能高,但会产生更多竞争。它类似于一种多线程多所有权机制。
-
Arc<T>
+Mutex<T>
:实现了多线程的内部可用性与多所有权。Mutex<T>
是访问锁,无论读还是写,都只允许一个线程使用目标值。使用值时必须先加锁才能使用,手动drop或者离开变量域后将自动解锁。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
// 这里可以使用try_lock方法,当无法加锁时将直接返回错误
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
} -
RwLock
:使用方法Mutex
类似,与允许并发读或写锁,在高并发读的情况下使用。但性能比Mutex
低,且可能长时间获取不到写锁。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25use std::sync::RwLock;
fn main() {
let lock = RwLock::new(5);
// 同一时间允许多个读
{
let r1 = lock.read().unwrap();
let r2 = lock.read().unwrap();
assert_eq!(*r1, 5);
assert_eq!(*r2, 5);
} // 读锁在此处被drop
// 同一时间只允许一个写
{
let mut w = lock.write().unwrap();
*w += 1;
assert_eq!(*w, 6);
// 以下代码会阻塞发生死锁,因为读和写不允许同时存在
// 写锁w直到该语句块结束才被释放,因此下面的读锁依然处于`w`的作用域中
// let r1 = lock.read();
// println!("{:?}",r1);
}// 写锁在此处被drop
} -
第三方并发原语库可以获得比官方库更为强大的性能:parking_lot
-
条件变量(
Condvar
):和Mutex
一起使用,可以让线程挂起,直到某个条件发生后再继续执行。这可以控制线程的执行顺序。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42use std::sync::{Arc,Mutex,Condvar};
use std::thread::{spawn,sleep};
use std::time::Duration;
fn main() {
let flag = Arc::new(Mutex::new(false));
let cond = Arc::new(Condvar::new());
let cflag = flag.clone();
let ccond = cond.clone();
let hdl = spawn(move || {
let mut lock = cflag.lock().unwrap();
let mut counter = 0;
while counter < 3 {
while !*lock {
// wait方法会接收一个MutexGuard<'a, T>,且它会自动地暂时释放这个锁,使其他线程可以拿到锁并进行数据更新。
// 同时当前线程在此处会被阻塞,直到被其他地方notify后,它会将原本的MutexGuard<'a, T>还给我们,即重新获取到了锁,同时唤醒了此线程。
lock = ccond.wait(lock).unwrap();
}
*lock = false;
counter += 1;
println!("inner counter: {}", counter);
}
});
let mut counter = 0;
loop {
sleep(Duration::from_millis(1000));
*flag.lock().unwrap() = true;
counter += 1;
if counter > 3 {
break;
}
println!("outside counter: {}", counter);
cond.notify_one();
}
hdl.join().unwrap();
println!("{:?}", flag);
} -
信号量
Semaphore
:限制最大并发数。推荐使用tokio
中提供的Semaphore
实现。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20use std::sync::Arc;
use tokio::sync::Semaphore;
async fn main() {
let semaphore = Arc::new(Semaphore::new(3));
let mut join_handles = Vec::new();
for _ in 0..5 {
let permit = semaphore.clone().acquire_owned().await.unwrap();
join_handles.push(tokio::spawn(async move {
// 在这里执行任务...
drop(permit);
}));
}
for handle in join_handles {
handle.await.unwrap();
}
}
线程同步:Atomic 原子类型与内存顺序
原子类型利用了原子指令,使得在cpu层面就实现了无锁并发原语,具有比锁更高的性能。常用于全局变量等。
-
内存顺序是指 CPU 在访问内存时的顺序,该顺序可能受以下因素的影响:
- 代码中的先后顺序
- 编译器优化导致在编译阶段发生改变(内存重排序 reordering)
- 运行阶段因 CPU 的缓存机制导致顺序被打乱
-
使用原子类型,需要根据需要设定内存屏障,确保正确的内存读写顺序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42use std::thread::{self, JoinHandle};
use std::sync::atomic::{Ordering, AtomicBool};
static mut DATA: u64 = 0;
static READY: AtomicBool = AtomicBool::new(false);
fn reset() {
unsafe {
DATA = 0;
}
READY.store(false, Ordering::Relaxed);
}
fn producer() -> JoinHandle<()> {
thread::spawn(move || {
unsafe {
DATA = 100; // A
}
READY.store(true, Ordering::Release); // B: 内存屏障 ↑
})
}
fn consumer() -> JoinHandle<()> {
thread::spawn(move || {
while !READY.load(Ordering::Acquire) {} // C: 内存屏障 ↓
assert_eq!(100, unsafe { DATA }); // D
})
}
fn main() {
loop {
reset();
let t_producer = producer();
let t_consumer = consumer();
t_producer.join().unwrap();
t_consumer.join().unwrap();
}
} -
原子类型缺陷:需要考虑的方面较多;只支持基本数值类型等。
Send 和 Sync
Send
和Sync
是标记特征(不定义任何行为),它们限定了哪些类型是可以安全地在线程间传递的。
1 | // Rc源码片段 |
- 实现
Send
的类型可以在线程间安全的传递其所有权。 - 实现
Sync
的类型可以在线程间安全的共享(通过引用)。 - 若类型 T 的引用
&T
是Send
,则T
是Sync
。 - 几乎所有类型都默认实现了
Send
和Sync
,而且由于这两个特征都是可自动派生的特征(通过derive
派生),意味着一个复合类型(例如结构体), 只要它内部的所有成员都实现了Send
或者Sync
,那么它就自动实现了Send
或Sync
。
全局变量
编译期初始化
无法用函数进行静态初始化
1 | use std::sync::atomic::{AtomicUsize, Ordering}; |
运行期初始化
-
使用
Box::leak
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Config {
a: String,
b: String,
}
static mut CONFIG: Option<&mut Config> = None;
fn init() -> Option<&'static mut Config> {
let c = Box::new(Config {
a: "A".to_string(),
b: "B".to_string(),
});
Some(Box::leak(c))
}
fn main() {
unsafe {
CONFIG = init();
println!("{:?}", CONFIG)
}
} -
使用sync::OnceLock(1.70版本以后):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21use std::collections::HashMap;
use std::sync::OnceLock;
fn hashmap() -> &'static HashMap<u32, &'static str> {
static HASHMAP: OnceLock<HashMap<u32, &str>> = OnceLock::new();
HASHMAP.get_or_init(|| {
let mut m = HashMap::new();
m.insert(0, "foo");
m.insert(1, "bar");
m.insert(2, "baz");
m
})
}
fn main() {
// First access to `HASHMAP` initializes it
println!("The entry for `0` is \"{}\".", hashmap().get(&0).unwrap());
// Any further access to `HASHMAP` just returns the computed value
println!("The entry for `1` is \"{}\".", hashmap().get(&1).unwrap());
} -
使用
lazy_static
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20use lazy_static::lazy_static;
use std::collections::HashMap;
lazy_static! {
static ref HASHMAP: HashMap<u32, &'static str> = {
let mut m = HashMap::new();
m.insert(0, "foo");
m.insert(1, "bar");
m.insert(2, "baz");
m
};
}
fn main() {
// 首次访问`HASHMAP`的同时对其进行初始化
println!("The entry for `0` is \"{}\".", HASHMAP.get(&0).unwrap());
// 后续的访问仅仅获取值,再不会进行任何初始化操作
println!("The entry for `1` is \"{}\".", HASHMAP.get(&1).unwrap());
}
异步编程
Rust异步编程基于async/await
模型,并提供了基于**内置语法(async/await
)+官方库(future)+第三方异步运行时库(tokio)**的综合实现。
async
标注的函数或语句块会产生一个Future,相当于一个能够产生值的任务。当线程执行Future时,遇到阻塞后会搁置当前Future执行,转而执行其它Future,直到原Future不再阻塞,调用wake方法唤醒并继续执行当前任务。
Future要被执行,需要被执行器poll到,或者由.await
关键字主动唤醒。
有点复杂。。后面结合tokio再看看
自动化测试
测试编写
测试函数往往会被整合到一些单独的测试模块中,同时使用#[test]
进行标注。
运行cargo test
可以运行所有测试,并且可以添加参数从而控制测试的执行,例如执行一部分测试、指定测试用例的执行线程数等。
可以使用panic和assert等进行测试。
1 | // --snip-- |
单元测试与集成测试
-
单元测试目标是测试某一个代码单元(一般都是函数),验证该单元是否能按照预期进行工作,例如测试一个
add
函数,验证当给予两个输入时,最终返回的和是否符合预期。在 Rust 中,单元测试的惯例是将测试代码的模块跟待测试的正常代码放入同一个文件中。对于这种测试模块,需要使用#[cfg(test)]
进行标注。1
2
3
4
5
6
7
8
9
10
11
12
13pub fn add_two(a: i32) -> i32 {
a + 2
}
mod tests {
use super::*;
fn it_works() {
assert_eq!(add_two(2), 4);
}
} -
集成测试文件放在项目根目录下的
tests
目录中,由于该目录下每个文件都是一个包,我们必须要引入待测试的代码到当前包的作用域中,才能进行测试,正因为此,集成测试只能对声明为pub
的 API 进行测试。需要注意,tests
目录下的子目录文件不会被作为测试。运行cargo test --test
来只运行集成测试。1
2
3
4
5
6use adder;
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
GitHub Actions(CI)
可以编写GitHub Actions脚本,由GitHub自动化进行集成测试。可以使用其它项目的脚本。