Rust 基础
Misc
-
let
声明的是不可变变量,let mut
声明的是可变变量。同时 Rust 中还有常量,必须用const
声明且指定类型。因为常量必须在编译期被估值 -
与 Java,Go 不同,使用
let
声明的变量不会被赋予零值,必须手动对其进行初始化才能使用(未被使用的变量可以不用初始化)。未被使用的变量编译器会给 warning,在这个变量名前加一个_
以显式告诉编译器这个变量就是用不到的(常见情形:只接收返回值但不使用;只传递参数但不使用) -
let a: i32 = 3;
,这其中的类型声明不是必须的 -
println!()
其实不是一个函数,而是一个宏(名字后面是!
) -
在格式化输出语句中,
{}
可以当做任何类型的占位符,它输出的内容由是变量的std::fmt::Display
这个 trait 决定;{:?}
输出的内容由变量的std::fmt::Debug
这个 trait 决定
可以简单地认为 Rust 中的 trait 就是其他语言中的 Interface
-
加了
;
就是语句,不加就是表达式。结合编译原理的知识可知,表达式可以看做有返回值。这在 Rust 的函数中尤为重要,函数体内最后一条表达式的结果将会被当成函数的返回值,因此无需显式的return
-
Rust 中存在元组 tuple类型,元组中的元素可以不是一个类型的。可以通过将函数的返回值设置成一个元组,来实现 Go 中类似的函数多返回值特性
-
与 Java/C++/C 中的
for
不同,Rust 中的 for 语句的格式为for <element> in <iterator>
。事实上,Rust 的风格是基于迭代器(更具体的,是into_iter
)而不是下标,来对集合(包含数组、切片等)的元素进行访问 -
Rust 中的
mod
与use
,十分类似 Java 中的 package 与 import,但更灵活一些,例如在同一个文件中可以定义多个mod
-
size_of::<String>
在一台 64 位的机器上运行的结果为何是 24?首先一个 String 对象的字符是存储在堆中的,这部分的大小是无法获知的。使用
size_of
得到的其实只是这个对象在栈上管理的元数据的大小:一个指向堆中数据的指针、length、capacity。这三个字段的长度都是usize
,在 64 位机器上,usize
就是u64
,占据 8 个字节,因此这三个栈上的字段总共占据 24 字节。结合后面的内容我们可以知道,String
类型也可以看作是一种智能指针 -
Rust 中的宏比 C/C++ 中的宏更强大,它的本质上是代码生成器,类似于 Java 中的 lombok 插件。C/C++ 中的宏完全基于文本层面的替换,而 Rust 中的宏是带有语义特征的,直接在编译期在 AST 层面上进行替换
-
char
类型是 32 位的,因为存储的是 Unicode。与 Java 和 Go 不同,Rust 中的char
类型必须显式转为整数类型才能相减 -
if
是表达式而不是语句,因此if
块可以返回值(并且每个分支的返回值类型必须一致)。或者从另一个角度理解,{}
都是有返回值的。同理,loop
也是一个表达式,它的功能等于其他语言的while (TRUE)
。此外,if
的条件表达式也可以写在一个局部作用域中初学 Rust 时可能会觉得一些代码中的局部作用域完全是多余的,但是这其实方便了对象生命周期的管理。例如在一个局部作用域中使用锁,那么局部作用域结束后,锁就被自动释放了
-
Rust 中的闭包由
|| -> () {}
的形式实现,可以捕获外部的变量(此时要格外小心所有权的移动);而在函数内部定义的函数(使用fn
关键字)是不能捕获外部的变量的(这点与 Go 不同)。并且 Rust 中的闭包不能进行递归调用(使用函数指针也许可以做到,不过太丑了) -
在涉及多态的编程中,有这样一条经验:「参数最好是引用(
&dyn TraitA
),返回值最好是 owner(Box<dyn TraitA>
)」
所有权与借用
下文中的「引用」和「借用」可认为是同义词
引用本身是一种类型,并且是尺寸固定的,即
usize
-
Rust 采取的内存回收机制更像是 C++ 中的 RAII 思想
回忆:C++ 中的 RAII 思想:
RAII = Resource Acquisition Is Initialization
一句话总结:堆内存资源随着其关联的栈上局部变量一起被回收
我们知道,C++ 中,一个作用域结束后,会自动清理在这个作用域中声明的全部局部变量。对于在这个函数中声明的局部对象(是的,C++ 中对象可以创建在栈上,此即局部对象),它会自动调用该对象的析构函数,释放其占有的资源。于是使用局部对象来实现 RAII 是简单的。但是,将对象全部创建成局部对象也许不太合适(例如想要创建的对象太大了,不适合放在栈上)。如果我们想对创建在堆上的对象也使用 RAII 的思想进行管理呢?在 C++ 11 以后,提供了智能指针的概念。智能指针本身是创建在栈上的,它所指的对象创建在堆上。但是,当智能指针离开其作用域后,其析构函数被调用时,不仅会释放自己所占的内存,也会自动调用它所指的堆上对象的析构函数,从而实现 RAII
-
基本类型可以不考虑所有权与借用,因为变量之间的赋值其实自动执行了拷贝
哪些类型在赋值时会自动拷贝,而不转移所有权?
- 所有的整数类型、浮点类型、
bool
、char
- 由以上类型组成的 tuple
- 由以上类型组成的 array
- 不可变引用
&T
那么由以上类型组成的 struct 是否会自动拷贝?这取决于是否实现了
Copy
这个 trait(事实上,对于上面能够自动拷贝的类型,其实是编译器自动为它们实现了Copy
) - 所有的整数类型、浮点类型、
-
可以认为只有堆上的值(也许称为对象更能与已有的认知联系在一起)才要考虑所有权与借用的关系。当我们把一个值赋给一个变量时,此时这个变量就称为这个值的拥有者,一个值的拥有者是唯一的,这种拥有关系可以进行转移,转移过后,原有的变量不再有效;在离开一个作用域时,Rust 会自动调用
drop
函数,它将清理所有的有效的所有者所引用的堆内存。下面是一段示例代码,它可能与常见的面向对象语言(如 Java)的行为不一致1
2
3
4
5fn main() {
let x: String = String::from("Hello");
let y = x;
println!("{}", x)
}这段代码将不能通过编译,原因是第 3 行代码使得值的所有权被转移到
y
,x
不再有效;编译器告诉我们,‘move occurs because
x
has typeString
, which does not implement theCopy
trait’。意思是说,如果 x 变量绑定的值具有Copy
这个 trait,那么将发生值的拷贝,执行第 3 行代码后,x
与y
均有效另外,编译器提示我们可以使用
clone()
方法,以下是修改后的代码,这个代码可以顺利通过编译:1
2
3
4
5fn main() {
let x: String = String::from("Hello");
let y = x.clone();
println!("{}", x)
}这很像其他编程语言中浅拷贝和深拷贝的关系。在 Rust 中,除非显式地使用
clone()
,否则变量间的赋值永远都是浅拷贝(即发生所有权的转移)除了上述的变量间的赋值语句,函数调用也会转移值的所有权:
1
2
3
4
5
6
7
8fn main() {
let x: String = String::from("Hello");
take_ownership(x);
println!("{}", x)
}
fn take_ownership(s: String) {
println!("{}", s)
}执行第 3 行代码调用函数后,
x
所绑定的值的所有权被转移到函数的形参s
中,第 7 行代码执行结束后,由于s
离开其作用域,将导致其绑定的值被回收。值得注意的是,尽管第 7 行代码可以顺利执行到,但是运行这段代码并不能看到这个输出。因为上述关于所有权与借用的讨论,编译器在编译期进行检查。上述代码未能通过编译,因此不会执行所以函数参数的类型基本都是引用类型或者是智能指针类型
使用
for in
迭代集合对象时也会发生所有权的转移,因此要尽量使用引用此外,函数的返回值也可以转移其所有权到函数的调用者中,可以将它形象地理解成被外部接收的返回值幸运地逃脱了函数调用结束后的回收
-
如果将一个变量的引用赋给另一个变量,此时值的所有权发生了借用。借用只允许获得值的使用权,但不能获得值的所有权(ownership)。因为引用并不拥有这个值,所以当引用离开作用域后,其引用的值也不会被drop
-
通过引用修改原始的值
1
2
3
4
5
6
7
8
9
10fn main() {
let mut s = String::from("Hello");
let s1: &mut String = &mut s;
foo(s1);
println!("{}", s)
}
fn foo(s: &mut String) {
s.push_str(" World");
}正如可变变量的声明一样,可变引用必须显示使用
mut
关键字进行声明只能通过可变引用修改借用的值,并且这个值必须也是可变的!并且一旦存在一个可变引用,相同作用域下不能存在同一个值的任何其他引用(包括不可变引用)。这与读写指针的思想一致
在一个不可变引用的作用域内,可以存在其他的不可变引用,但不能存在对于同一个值的可变引用!
Rust 的编译器检查可以保证不可能出现 C/C++ 中的悬垂指针(尝试访问一块已经被释放的区域)
let mut a = &c;
a = &d;
这里 a 前面的 mut 指的是允许 a 引用其他的值,而不是允许修改 a 指向的值本身
-
一些函数调用也会转移值的所有权,原因是这个函数虽然是通过
.
操作符调用,但是隐含了一个self
参数,这个参数就是这个对象的引用(联想 Python 类的方法定义中自带的self
),而且这个引用往往是可变引用。因此,函数调用可能会十分隐晦地违反上述引用的冲突规则,例如:1
2
3
4
5
6
7
8
9fn main() {
let mut s = String::from("Hello");
let first = first_char(&s); // immutable borrow here
s.clear(); // mutable borrow here
println!("{}", first) // immutable borrow here
}
fn first_char(s: &String) -> &str {
&s[..1]
}在第 4 行,由于
first
仍旧活跃(因为第 5 行将被使用),并且,这个切片的底层与s
的底层是一致的,因此可以看做是一个活跃的对于s
的不可变引用。但是在调用clear
方法时,实际上是为这个方法提供了一个s
的可变引用。这就违反了上述的引用冲突的规则。尽管first
(即&str
切片)只是指向s
的部分数据,它仍然是基于s
的不可变引用。在s
的一部分(通过first
指向)仍然存在不可变引用时,不能存在s
的可变引用 -
与函数调用类似的,使用宏(macro)时也可能隐晦地违反上面的规则
1
2
3
4
5
6
7fn main() {
let mut a = 10u32;
let b = &mut a;
*b = 20;
println!("{a}"); // 这里发生了对a的不可变借用
println!("{b}"); // 可变借用b的生命周期在这里终止
} -
所有型变量和引用型变量生命周期的不同
1
2
3
4
5
6
7fn main() {
let mut a = 10u32;
let c = &a;
let b = &mut a;
*b = 20; // b的生命周期这里结束
println!("{c}"); // c的生命周期这里结束
} // a的生命周期这里结束NLL(Non Lexical Lifetime)
- 所有型变量(如上面的
a
)的生命周期在一个}
结束 - 引用型变量(如上面的
b
,c
)在它最后一次被使用后结束
其他规则还有
-
所有型变量的生命周期一定长于它的引用型变量,否则会出现悬垂指针的问题
-
一个所有型变量的不可变引用的生命周期可以交叉
-
一个所有型变量的可变引用的生命周期与其他任何引用的生命周期都不能交叉
-
在一个所有型变量的引用的生命周期内,不允许通过原来的所有型变量对值进行修改
1
2
3
4let mut a = 3;
let r = &a;
a = 4;
println!("r={}", r);报错是:
1
2
3
4
5
6
7
8
9error[E0506]: cannot assign to `a` because it is borrowed
--> src/main.rs:6:5
|
5 | let r = &a;
| -- `a` is borrowed here
6 | a = 4;
| ^^^^^ `a` is assigned to here but it was already borrowed
7 | println!("r={}", r);
| - borrow later used here -
将一个可变引用赋值给另一个引用,那么原来的可变引用将无效
- 所有型变量(如上面的
类型
数组
-
与 Go 类似,Rust 中数组的长度也是其类型的一部分
1
2// Array type syntax: [ <type> ; <number of elements> ]
let numbers: [u32; 3] = [1, 2, 3]; -
因为数组的大小是编译时可知的,因此其被分配在栈上
动态数组 Vec
1 |
|
-
Vec 本身在栈中存储:指向堆中数据的指针、
len
、cap
。因此也可以将Vec
类型本身视作一种智能指针 -
放入 Vec 中的元素,这个 Vec 拥有它的所有权;如果放入 Vec 的是一个引用,那么要保证这个引用的生命周期至少跟这个 Vec 的生命周期一样久(下面的
HashMap
同样适用于这个规则)
切片
- 切片的类型是
&[T]
,本质上是对底层数组的借用 - Rust 中的切片语法与 Go 中的相似,只不过使用
..
连接左右索引 - 字符串切片的类型是
&str
(可以看做是对底层String
的引用)。对字符串使用切片语法时需要注意,切片索引的是字节,如果存在某些非英文字符,此时需要考虑每个 Unicode 字符占据多少个字节 - 一个比较 tricky 的点是,可以使用
&mut[u32]
去修改这个切片的底层数组,但是并不适合用&mut str
去修改底层字符串的内容(虽然使用unsafe
依旧可以做到)
哈希表 HashMap
-
HashMap 的
get
方法,参数和返回值均是引用类型。使用for in
语法进行迭代时,所迭代的键值也都是引用 -
考虑我们要使用一个哈希表实现统计单词词频的任务,这包含创建不存在的单词的键值对、更新已经存在的单词的词频,在 Java 中,可以使用以下方式:
1
2
3
4
5
6var map = new HashMap<String, Integer>();
for (String word: text) {
map.put(word, map.getOrDefault(word, 0)+1);
// optional
// map.merge(word, 1, Integer::sum);
}在 Rust 中实现类似的功能:
1
2
3
4
5
6
7
8
9
10use std::collections::HashMap;
fn main() {
let text = String::from("text text text");
let mut map = HashMap::new();
for word in text.split_whitespace() {
let cnt = map.entry(word).or_insert(0); // 返回的cnt时一个可变的引用
*cnt += 1 // 通过这个可变的引用实现value++
}
}
元组
1 |
|
- 元组中的元素可以是不同类型的,这在函数的多返回值时十分有用
- 元组的元素个数是固定的
- 当元组没有任何元素时,它变成
()
,对应的类型是unit
类型。unit
类型的唯一实例就是()
结构体
-
命名结构体
1
2
3
4struct User {
name: String,
age: u32,
} -
元组结构体
实际上是为一个元组取了一个名字。对于元组结构体中字段的访问,与元组一样,使用下标
1
struct Color(i32, i32, i32);
-
单元结构体
1
2struct Foo;
let foo = Foo; // 这里创建了Foo的一个实例并绑定给foo单元结构体不包含任何字段,它拥有的唯一信息就是它的名字
Rust 中类(结构体)的字段定义与类的方法定义是分离的:
1 |
|
方法的第一个参数设置为 self: &Self
(更常见的情况是简写为 &self
,注意区分大小写。这里的第二个 Self
值得注意一下,它将绑定调用这个方法的实例的类型。即 Self
对应正在被 impl
的那个类型)来调用结构体的字段或其他方法。此时由于 &self
是一个不可变引用,则不能通过这个引用去修改实例。可变引用是 &mut self
。实际上第一个参数也可以设置为 self
,不过这将导致实例所有权的转移,因此并不常用
在 impl{}
中定义的所有函数,全部称为关联函数。只要含有 self
参数(还包含 &self
, &mut self
),它就是一个方法,可以使用 .
操作符进行调用。除方法以外的关联函数,常见于对象的构造函数,比如使用一个名为 new
的函数定义类的构造器(Rust 中 new
不是一个关键字,只是习惯上用 new
命名构造函数)。对于这样的关联函数,使用 ::
调用,即 Foo::new()
可以将上面的两种函数的区别理解成 Java 中的静态方法(从属于类,用
::
调用)和非静态方法(从属于类的实例,用.
调用)的区别
如果想为一个 struct 实现一种 triat,使用 impl ... for ... {}
1 |
|
枚举
1 |
|
使用 enum
定义一个枚举。枚举里面的选项叫做此枚举的变体(variants)。变体是其所属枚举的类型的一部分
可以认为,结构体中,是所有的字段在起作用;而枚举中,是其中的某一个字段在起作用。结构体是一种积类型(product type),枚举是一种和类型(sum type)。元组也是一种积类型(因为所有的字段均起作用)
更形式化的理解,积类型的变量的取值是其中每一个成员的取值的笛卡尔积
C/C++ 中的
union
类型也是和类型
每一个变体可以关联任意类型的数据,这些数据称为变体的负载:
1 |
|
当然也支持 C 风格的枚举,不过不常用:
1 |
|
枚举也是一种类型,可以使用 impl
定义枚举关联的方法:
1 |
|
match
表达式通常与枚举配套使用。match
的每个分支的返回值类型必须相同(或者是属于同一枚举的不同变体),必须涵盖所有的变体,可以使用 _
匹配其他剩下的变体
match
也可以处理其他的基础类型:
1 |
|
字符串
-
字符串的字面量本身存储在静态存储区
-
String
类型将字符串的内容拷贝到堆上1
2let a = "1".to_string(); // "1" 本身位于静态存储区
// 将字面量的值拷贝到堆上 -
&String
是对String
的引用,&str
是对String
的切片引用
泛型
泛型其实就是额外的类型参数
1 |
|
有关
::<>
turbofish 运算符的由来:它看上去像一条游动的鱼遗憾的是,它的提出者在 2021 年患癌去世了
可以将 impl
作用在泛型上:
1 |
|
Option 与 Result
这是两个十分常用的 enum 类型,起源于函数式编程语言
Option<T>
表示有或无
1 |
|
Result<T, E>
表示结果正确还是错误
1 |
|
可以将 Option 与 Result 视作包裹类型,那么如何从中取出被包裹的值?
expect()
:可以指定错误信息。如果 Option 实例为None
,或是 Result 实例为 Err 时,就会panic
,并打印出这个错误信息unwrap()
:不带提示信息,其余行为与expect()
一致unwrap_or()
:可以提供 Option 为None
或 Result 为Err
时的指定默认参数,不会 panicunwrap_or_default()
:可以提供 Option 为None
或 Result 为Err
时类型的默认参数,不会 panic
迭代器
1 |
|
使用迭代器进行遍历时,编译器可以确信不会引起越界访问,所以不会进行越界检查。因此,使用迭代器进行遍历一般要比使用下标进行遍历更快
iter()
:获取集合元素的不可变引用的迭代器iter_mut()
:获取集合元素的可变引用的迭代器into_iter()
:获取集合元素的所有权的迭代器
for
语句其实是语法糖,for item in c {}
其实是:
1 |
|
可以看出,使用 for
循环迭代一个集合,集合中的元素的所有权将被转移。同时,要使用 for
迭代一个类型的实例,这个类型必须 impl
了 into_iter()
这个方法
此外,for ... in &c {}
和 for ... in &mut c
提供了不转移所有权来遍历集合的方法
解构(模式匹配)
注意解构成功后默认会改变变量的所有权
如果希望解构只获得变量的引用而不是所有权,要使用
ref
关键字
let (ref x, ref mut y) = t
,其中 x 是一个不可变引用,y 是一个可变引用
-
解构元组
1
2
3
4
5
6
7let t = (1, 3);
let (x, y) = t;
println!("x: {}, y: {}", x, y);
// 注意解构成功后会改变变量的所有权
let t = (1, 3);
let (ref x, ref mut y) = t; // x是原始变量的不可变引用,y是原始变量的可变引用
println!("x: {}, y: {}", x, y); -
解构结构体
1
2
3
4
5
6
7
8
9
10struct Point {
x: i32,
y: i32,
}
let p = Point {x: 10, y: 20};
let Point {x, y} = p;
println!("x: {}, y: {}", x, y);
// 当然也可以解构部分字段
let Point {x, ..} = point; // 使用..忽略字段。不过被忽略的字段的所有权也一样被转移了
println!("x: {}", x); -
解构枚举
1
2
3
4
5
6
7
8
9
10
11enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
}
let msg = Message::Write("Hello, World!".to_string());
match msg {
Message::Quit => println!("Quitting"),
Message::Move { x, y } => println!("Moving: ({}, {})", x, y),
Message::Write(s) => println!("Writing: {}", s),
}- 可以使用
|
并列匹配多种情况 - 使用
_
处理其他分支
- 可以使用
-
解构数组和切片
1
2
3let arr = [1, 2, 3];
let [a, b, ..] = arr;
println!("{:?}", a); -
解构引用
1
let &(a, b) = &(1, 2);
-
if let
和while let
解构成功即满足条件
1
2
3
4
5
6
7if let Some(x) = Some(5) {
// 解构成功
}
let mut x = 0;
while let Some(x) = None::<i32> {
// 解构失败
}
trait
Rust 中的 trait 与 Java 中的 Interface 代表了两类思想:组合和继承
而软件工程的经验表明,组合优于继承
-
trait 用来限定泛型参数的具体类型。
T: TraitA
类型对变量的取值空间进行约束,trait 对类型参数的取值空间进行约束
-
trait 中可以定义关联函数。这些关联函数要么只提供函数签名(不带
{}
),要么提供默认的实现1
2
3
4
5
6
7
8trait Sport {
fn play(&self) {} // 提供了默认实现,尽管这个实现什么都不做
fn play_mut(&mut self); // 仅仅是函数签名
}
struct Tennis;
impl Sport for Tennis {
fn play_mut(&mut self) {}; // 提供一个空实现
} -
trait 中可以带关联类型,起到一种占位的功能,在为某一个类型实现这个 trait 的时候再为关联类型指定具体的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16trait Sport {
type T; // T仅仅是一个占位符
fn play(&self, st: Self::T);
}
struct Tennis;
enum SportType {
Land,
Water,
}
impl Sport for Tennis {
type T = SportType;
fn play(&self, st: Self::T) {};
}
let t = Tennis{};
t.play(SportType::Land) -
可以在使用 trait 对类型参数进行约束时,提供指定的关联类型:
1
2
3
4
5
6
7
8trait Sport {
type T; // T仅仅是一个占位符
fn play(&self, st: Self::T);
}
struct Foo<T: Sport<T=SportType>> {
x: T,
} -
关联类型也是可以添加约束的
1
2
3
4trait Sport {
type T: Display; // 具体的T类型必须实现了Display这个trait
fn play(&self, st: Self::T);
} -
trait 间的依赖
1
trait A: B {}
这表示任何试图实现 A 的类型也必须同时实现了 B
-
trait 上带类型参数
1
trait TraitA<T> {}
这表示这个 trait 里的关联函数,可能会用到这个类型参数 T。此时
TraitA<T>
作为一个整体,比如TraitA<u32>
和Trait<i32>
就是两个不同的类型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
36trait Add<T> { // 这里的T表示该trait的关联函数会用到T
type Output;
fn add(self, rhs: T) -> Self::Output;
}
struct Point {
x: i32,
y: i32,
}
impl Add<Point> for Point {
type Output = Self;
fn add(self, rhs: Point) -> Self::Output {
Self {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
impl Add<i32> for Point {
type Output = Self;
fn add(self, rhs: i32) -> Self::Output {
Self {
x: self.x + rhs,
y: self.y + rhs,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let p3 = p1.add(p2); // p1与另一个Point相加
let delta = 2;
let p4 = p3.add(delta); // p4与一个i32相加
}可以看出,Rust 虽然不支持 C++/Java 中的函数重载,但是用 trait 灵活地实现了类似功能
-
由标准库定义的 trait
-
Operator traits (e.g.
Add
,Sub
,PartialEq
, etc.)(C++ 中用操作符重载实现类似的功能。一种简单的理解方式是将内置操作符看成是一种特殊的方法名,x <op> y
其实是x.op(y)
) -
From
andInto
, for infallible conversions(无损的类型转换) -
Clone
andCopy
, for copying values -
Deref
and deref coercion这是一个很方便编程的 trait,可以将对于一个类型的访问替换为另一个类型的访问,比如:
- 将对
Vec<T>
的访问转换为对切片&[T]
的访问,从而使得Vec<T>
类型可以调用对应的切片类型的方法 - 将对智能指针的访问转换为对其所包裹的类型的访问
这些行为全都是编译器自动实现的
1
2
3
4
5
6
7mod std::ops {
pub trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
} - 将对
-
Sized
, to mark types with a known size -
Drop
, for custom cleanup logic
-
-
在 Rust 中,可以为任何一个内置类,通过实现一个自定义的 trait 以附加新方法。而在 Java 中,要为内置类添加新方法并不是一件容易的事
-
trait 的定义,实现该 trait 的 struct 的定义,与具体对于 trait 的 impl,这三者的作用域要符合孤儿规则中的至少一点
1
2
3
4
5trait t_name {} // trait 的定义
struct s_name {} // type 的定义
impl t_name for s_name {} // 具体的实现-
本地类型:trait 或 struct 至少有一个是在当前 crate 中定义的
-
外部类型和外部 trait:如果想为一个外部类型(例如来自标准库或第三方库的类型)实现一个外部 trait(同样可能来自标准库或第三方库),则不能这么做,除非这种实现是在类型或 trait 的原始定义的 crate 中进行的。
例如下面的代码将无法通过编译:
1
2
3
4
5impl PartialEq for u32 {
fn eq(&self, _other: &Self) -> bool {
todo!()
}
}因为 trait
PartialEq
和 typeu32
均是在std
中定义的,而不是在当前作用域
-
-
Marker Trait / Auto Trait
这类 trait 一般是标准库内置的,且 trait 的体是空的,意味着声明该类型具有某种性质,以便编译器进行优化;如
Sized
,它表明这个类型的实例的大小在编译期是可知的;再如Copy
,它的声明如下:1
pub trait Copy: Clone { }
即使不需要为
Copy
本身实现方法,但是得保证为Clone
实现了方法 -
一个符合 Rust 风格的泛型示例
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
52trait Power<M> {
fn power(&self, arg: M) -> Self;
}
impl Power<u16> for u32 { // Self就是u32
fn power(&self, arg: u16) -> Self {
let mut res = 1;
for _ in 0..arg {
res *= *self;
}
res
}
}
impl Power<u32> for u32 {
fn power(&self, arg: u32) -> Self {
let mut res = 1;
for _ in 0..arg {
res *= *self;
}
res
}
}
impl Power<&u32> for u32 {
fn power(&self, arg: &u32) -> Self {
let mut res = 1;
for _ in 0..*arg {
res *= *self;
}
res
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_power_u16() {
let x: u32 = 2_u32.power(3u16);
assert_eq!(x, 8);
}
#[test]
fn test_power_u32() {
let x: u32 = 2_u32.power(3u32);
assert_eq!(x, 8);
}
#[test]
fn test_power_ref_u32() {
let x: u32 = 2_u32.power(&3u32);
assert_eq!(x, 8);
}
}
生命周期
严格来讲,为了避免悬垂引用的出现,借用检查器会基于一套规则为每一个引用都标注生命周期。不过当这些规则不足以推导出引用的生命周期时,借用检查器要求进行手动标注
对于生命周期的简单理解:一个引用不能比它所引用的值的生命周期还长,如果一个引用所引用的对象活得比自己长,那这个引用就是安全的
NLL
Non Lexical Lifetime 是 Rust >= 1.63 引入的特性,旨在更加智能化地判断变量的生命周期
简单来说,在 NLL 之前,一个变量的生命周期是从它的声明点开始,到它的声明点所在的词法作用域的结束点(例如一个用 {}
标记的作用域,它的结束点就是 }
),即基于词法分析的生命周期
引入 NLL 之后,一个变量的声明周期是从的它的声明点开始,到它的最后一次被使用结束,即基于控制流信息的声明周期)
例如在未引入 NLL 之前,下面的代码不能通过编译
1 |
|
在引入了 NLL 之后,编译器能够更加「智能」地推导 r
的生命周期
1 |
|
下面的代码仍然不能通过编译,因为 NLL 仍然判断出引用的生命周期长于它所引用的对象
1 |
|
智能指针
Box<T>
Rust 的内存模型中,栈上存放的数据类型的大小必须是在编译时即可获知的。如果需要创建编译时大小不可知的数据类型、或者该数据类型的大小过大以致于不便放在栈上,那么可以使用 Box
,它包裹的对象是分配在堆上的。由于 Box
本身的大小是编译时可确定的,因此 Box
本身可分配在栈上。不过注意这个例子,let v = vec![Box::new(2), Box::new(3)]
,v
中的这两个 Box
其实是分配在堆上的,因为 Vec
类型本身也是一个智能指针
1 |
|
或者用 Box
来实现特征对象 trait object
1 |
|
RefCell<T>
用于不可变结构体的内部可变性
Rc<T>
与 Arc<T>
二者均用于共享对象。基于引用计数机制,一旦一个对象的所有 Rc
或 Arc
均被 drop 了,那么这个对象就自动被 drop 了
Rc<T>
用于单线程中,Arc<T>
用于多线程中
Rc<RefCell<T>>
十分常用,用于实现链表这样的自包含结构
Arc<Mutex<T>>
十分常用,用于在多线程间共享锁
Cow<T>
Copy-on-write
并发和异步编程
-
利用
std::thread::spawn
开启一个新线程,它的参数是一个闭包(但是这个闭包几乎不用指定参数,如果要捕获外部参数,要使用move
关键字),返回值是一个句柄1
2
3
4
5pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static -
注意在 main 函数中开启的其他线程,一旦 main 函数执行结束(这个函数也叫主线程),它开启的所有线程也将立即结束;
其余的线程与其开启的线程之间没有这样的约束关系
-
use std::thread; fn main() { let handle = thread::spawn(|| { println!("Hello from a thread!"); }); handle.join().unwrap(); // join(): the main thread will wait for the spawned thread to finish before exiting // also get the result from the newly-spawned thread } <!--code55-->
-
多线程编程中要格外小心生命周期带来的约束,因为 spawn 出的线程的生命周期很可能超出原来的线程的生命周期。设父线程为 A,spawn 出的线程为 B,B 中不应该借用(引用)那些 A 中可能被提前 drop 的值。我们结合上面的第 9 行中的 move 关键字来理解。move 关键字将外部的 v1 的所有权转移到新线程中来
如果确实需要在线程中使用外部的引用,一定要保证它是
'static
的,如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15pub fn sum(slice: &'static [i32]) -> i32 {
if slice.len() <= 1 {
return slice.iter().sum();
}
let (s1, s2) = slice.split_at(slice.len() / 2); // s1与s2均是 'static 引用
let h1 = thread::spawn(move || {
s1.iter().sum::<i32>()
});
let h2 = thread::spawn(move || {
s2.iter().sum::<i32>()
});
let sum1 = h1.join().unwrap();
let sum2 = h2.join().unwrap();
sum1 + sum2
}'static 声明的值的声明周期是整个程序的运行时间
还有一种方式,即用
leak
,告知编译器,程序员永远不会手动释放一个堆上的值的内存,因此编译器可以为其返回一个'static
的引用1
2
3
4
5
6
7
8
9
10
11pub fn sum(v: Vec<i32>) -> i32 {
let tmp: &'static[i32] = v.leak(); // deliberately leak
let (v1, v2) = tmp.split_at(tmp.len()/2);
let h1 = thread::spawn(move || {
v1.iter().sum::<i32>()
});
let h2 = thread::spawn(move || {
v2.iter().sum::<i32>()
});
h1.join().unwrap()+h2.join().unwrap()
}还有一种方式,使用
scope
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15pub fn sum(v: Vec<i32>) -> i32 {
if v.len() <= 1 {
return v.iter().sum();
}
let (v1, v2) = v.split_at(v.len()/2);
thread::scope(|scope| {
let h1 = scope.spawn(|| {
v1.iter().sum::<i32>()
});
let h2 = scope.spawn(|| {
v2.iter().sum::<i32>()
});
h1.join().unwrap()+h2.join().unwrap()
})
} -
channel
Rust 原生的 channel 是
mpsc
风格的,即multiple producer, single consumer
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::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel(); // tx是sender,rx是receiver
// 创建多个生产者
for i in 0..3 {
let tx_clone = tx.clone(); // sender允许clone
thread::spawn(move || {
let val = format!("hello from thread {}", i);
tx_clone.send(val).unwrap();
println!("Thread {} finished", i);
});
}
// 创建单个消费者
thread::spawn(move || {
for received in rx { // rx的所有权被转移
println!("Got: {}", received);
}
});
// 注意这里我们如果想要使用多个消费者的话,情况就变得复杂了。因为receiver是不允许clone的
// 可以考虑使用Arc<Mutex<>>,即共享了receiver的引用,又考虑了互斥的问题
// 主线程暂停一段时间,确保子线程全部执行完毕
thread::sleep(Duration::from_secs(1));
} -
锁
Go 的
sync.Mutex
相关的操作是十分简洁的,使用Lock()
和相应的Unlock()
。在 Rust 中,由于所有权的限制,同一个 Mutex 要想被多个线程共享,还要包裹在Arc
中。一个典型的多线程锁的代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(Vec::new())); // Arc包裹Mutex,初始值为空Vec
let mut handles = vec![];
for i in 0..5 {
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut vec = data_clone.lock().unwrap(); // 注意这里的lock操作
vec.push(i); // 向 Vec 中插入数据
}); // vec在这里被drop,对应的锁自动被释放
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
let result = data.lock().unwrap();
println!("Final vector: {:?}", *result);
}-
let data = Arc::new(Mutex::new(Vec::new()));
这样的代码在 Rust 并发编程中十分常见 -
注意到上面的代码中没有显式的 unlock 操作,因为 unlock 操作其实与对象的 drop 操作绑定在一起了,持有锁的对象(如上面 11 行中的
vec
对象)离开其作用域而被 drop 时,它持有的锁自动被释放。在 Rust 的并发代码中,尝尝会见到在局部作用域中进行加锁操作,使得结束该作用域时锁能被释放1
2
3
4
5let ok = { // Rust的局部作用域可以返回值
let raft = self.inner.lock().unwrap(); // raft变量是MutexGuard类型
raft.state.role != Role::Leader
&& raft.election_timeout_start.elapsed() >= raft.election_timeout_duration
}; // make sure 'raft'(the lock) is scoped
-
条件编译
有点类似 C++ 中的
#ifdef
和其他条件编译手段Rust 不愧是 modern C++
根据编译目标机器的特性选择性执行代码 。比如说在一个命令行程序中,想根据 OS 的不同提示用户:
1 |
|
宏
Todo
第三方库
-
https://github.com/tokio-rs/tokio 最广泛使用的 Rust 异步运行时(Rust 没有内置的异步运行时)
-
https://github.com/rust-itertools/itertools 增强的迭代器相关方法,例如返回多个迭代器的值的笛卡尔积
-
https://github.com/clap-rs/clap 命令行参数解析器
-
https://github.com/serde-rs/serde 序列化和反序列化库
-
https://github.com/google/tarpc RPC 库。因为 gRPC 没有对 Rust 的原生支持
-
https://github.com/crossbeam-rs/crossbeam 并发编程数据结构,例如多生产者多消费者的 channel
-
https://github.com/tokio-rs/bytes 提供了对字节进行操作的相关方法。在网络协议的开发、持久化数据结构中十分常用
-
https://github.com/moka-rs/moka Cache 库,在数据库或者存储引擎中常用
-
https://github.com/xacrimon/dashmap 并发安全的哈希表
-
https://github.com/indexmap-rs/indexmap 可一致迭代的哈希表(Rust 内置的 HashMap 并不能保证一致地迭代)
下面是 Web 框架(Rust 的 Web 框架基本是所有语言中最快的。不过用 Rust 写 Web 应用应该是挺虐的 🤣)
- https://github.com/tokio-rs/axum Tokio 的 Web 库
- https://github.com/actix/actix-web Actix 的 Web 库
- https://github.com/leptos-rs/leptos yet another web framework in Rust…