Apr 02, 2022
Rust程序设计
into()
方法可以快速的将当前类型转换为目标参数类型;
基本类型
- as操作符可以将
bool
转为整数类型,但是反过来转换不可以; bool
在内存中使用了一个字节作为其值;- as操作符可以将
char
转为整数类型,但是目的类型如果小于了32位,高位会被截断;反过来转换仅支持u8
转为char
; - Rust中的指针类型有三种:引用、Box(指向堆)、不安全指针(原始指针);
- 胖指针(fat pointer):指除了包含指向引用地址的值,还有一些额外的信息,如向量(还包含了大小)、trait(还包含了其实现类的地址);
所有权
对于向量的遍历,一般是使用引用来访问其数据的值,这样不会转移向量中元素的所有权;如下代码示例:
1
2
3
4
5
6let mut v = vec!["1".to_string(), "2".to_string()];
for mut s in v { // 这里的for会将v中的元素的所有权转移到s中
s.push('!');
println!("{}", s);
}
println!("v's length {}", v.len()); // 报错上诉的代码执行会报错
1
2
3
4
5
6
7
820 | for mut s in v {
| -
| |
| `v` moved due to this implicit call to `.into_iter()`
| help: consider borrowing to avoid moving into the for loop: `&v`
...
24 | println!("v's length {}", v.len());
| ^^^^^^^ value borrowed here after move编译器提示的很清晰了,因为for循环中将v里的元素的所有权都进行了转移到了s中,因此v在for循环代码块之后变成了初始化的状态,这对Rust来说是不允许使用的,因此报错了;将代码修改如下
1
2
3
4
5
6
7let mut v = vec!["1".to_string(), "2".to_string()];
for mut s in &mut v {
// *s.push('!');
s.push('!');
println!("{}", *s);
}
println!("v's length {}; {:?}", v.len(), v);注意注释的代码的写法是错误的,因为.操作符本身就隐式带有解引用,所以不需要再额外的写*去解引用
Option
提供的*take()*方法,会将值置换为None并赋值给左值;机器整数、浮点型、char和bool类型都是Copy类型,会直接复制值而不会转移;默认情况下,struct和enum都不是Copy类型,但是,如果一个结构体中,仅当所有类型都是Copy类型,是可以通过添加属性来将该结构体标注为Copy类型(如果结构体中存在非Copy类型,即使加了属性也无效),如:
1
2
struct Label { number: i32 }函数返回的值的生命周期,如果返回的是
&str
需要注明为&'static str'
,但是返回String
的话不需要。因为String
是一个新的对象,该对象的生命周期直接返回了。
引用
能力不足,怨不得书
——Mark Miller
引用的篇章,都是对这个概念做出大量的解释,所以只有在练习中才能巩固对其的理解;
生命周期的意义在于约束赋值或传参时,对结构体或者函数返回值的生命周期的控制;
在存在**&self方法(非函数)中,Rust默认生命周期是&self**的生命周期;如下
1
2
3
4
5impl StringTable {
fn find_by_prefix(&self, prefix: &str) -> Option<&String> {
...
}
}该方法的完整版本应该是这样的
fn find_by_prefix<'a, 'b>(&'a self, prefix: &'b str)-> Option<&'a String>
;
表达式
循环可以通过生命周期标签来控制多层跳转,有点类似Kotlin 里的forEach@ 这种标签写法;
1
2
3
4
5
6
7
8‘search:
for room in apartment {
for spot in room.hiding_spots {
if spot.containsKey(keys) {
break 'search;
}
}
}关于泛型的方法调用注意:通常用于函数调用或者方法调用的语法不能用于泛型,如
Vec<i32>::with_capacity(1000);
这种是错误的,正确写法是Vec::<i32>::with_capacity(1000);
,当前,还可以让编译器自己推断,从而省略泛型的写法Vec::with_capacity(3000);
;Rust中的转换一般是显示的,除了解引用强制转换,有三种情况
- &String 类型的值会被自动转换为&str;
- &Vec
类型的值会被自动转换为&[i32]; - &Box
类型的值会自动转换为&Chessboard;
错误处理
处理特定业务类型的错误代码示例:
1
2
3
4
5
6
7
8
9
10
11
12loop {
match compile_project() {
Ok(()) => return Ok(()),
Err(err) => {
if let Some(msg) == err.downcast_ref::<MissSemicolonError>() {
// xxxx
...
continue;
}
}
}
}
包和模块
- 没有声明pub的都是模块私有的;
结构体
- 结构体中所有的变量都必须声明为pub,才能使用结构体表达式构建;否则只能使用对外暴露的API方法进行构建(如
Vec::new();
); - 结构体类型:命名字段结构体(
struct Person { name: String }
)、类元组结构体(struct Bounds(usize, usize);
)和类基元结构体(struct OneType;
); - impl的方法实现中,如果第一个入参是
self
,而不是引用,则会在调用方法后转移所有权;
枚举与模式
枚举的类型和结构体一样有三种;并且一个枚举定义中可以同时包含这三种类型的变体;
模式匹配中,会将结构体中的所有权转移到匹配的变量上,这样原结构体就会变成未初始化状态,因此需要使用
ref
关键字来获取其引用,示例如下:1
2
3
4
5
6match account {
Account { ref name, ref language, ..} => {
ui.greet(name, language);
ui.show_setting(&account);
}
}因为在模式匹配中使用ref关键字,所以name和language两个变量仅复制了account中这两个变量的值,而非转移了其所有权,所以在后面的代码仍然可以使用**&account**;
匹配某个范围的值使用的是**…**;
在模式匹配中添加if条件时,不能将变量的值进行转移,如
Some(point) if self.distance_to(point) < 10
这个表达式中,后面的条件实际上转移了point
的所有权,就会导致后面的分支判断无法使用point
了,因此需要修改为借用,即Some(ref point) if self.distance_to(point) < 10
;可修改的为ref mut
;x @ pattern
匹配给定的pattern,但是成功之后,不是基于匹配值的元素来创建变量,而是把匹配值整个转移或者复制到一个变量x中;两个概念的解释:可驳模式(refutable pattern)和不可驳模式(irrefutable pattern),类似Kotlin中的解构就是不可驳模式,其双向的值是确定清晰可转换的,如
let (x, y) = point;// point = (3, 4)
,而match就是一种可驳模式,因为对于一个需要match的值来说,其结果可能是Ok(x)
也可能是Err(err)
,他双向的匹配是一个大于的范围;
特型(Trait)
特型目标,指的是使用
trait
声明的变量,其只能接受引用,不能直接接受变量,原因是本质上,trait
是一个引用,它指向的是实现该trait
的具体实现类;特型目标支持普通引用自动转换,即一个引用是某个
trait
的实现,在方法或者函数调用时,传入其引用可以自动转换为该trait
类型;特型的
impl
实现方法中只能包含特型的实现,如果需要其他的实现,需要定义其他的impl
块;扩展特型:指的是当前的特型仅为一个已有的类型添加新的方法;
关联类型,声明迭代器的item的类型限制的写法如下:
1
2
3
4
5
6
7
8
9
10
11
12fn dump<I>(iter: I)
where I : Iterator, I::Item:Debug
{
...
}
// 或者
fn dum<I>(iter: I)
where I : Iterator<Item=String>
{
...
}Default特型是有默认值的类型;
Add<Output=i32>
这句的理解是,Add
是+
操作符的trait定义,定义了两种数值类型的加法,其定义源码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16pub trait Add<Rhs = Self> {
/// The resulting type after applying the `+` operator.
type Output;
/// Performs the `+` operation.
///
/// # Example
///
/// ```
/// assert_eq!(12 + 1, 13);
/// ```
fn add(self, rhs: Rhs) -> Self::Output;
}可以看到是trait中定义的type类型,这个在rust中被称为关联类型,如迭代器的trait中定义的关联类型就是item,那这个的使用场景可以这么理解,如
v[i]+s[i];
假设这个是某个循环或者函数中的片段,v和s分别是两个数值类型的切片,且在当前的片段中,无法推断出v[i]和s[i]的值的具体类型(在泛型方法中,这个就很常见了),这样执行+
就会有问题,比如v[i]是一个i32而s[i]是一个f32,这样他们肯定无法执行+
,所以这里会有问题,解决的方法就是对其值类型进行type限制,查看一个完整实例:1
2
3
4
5
6
7
8
9fn dot<N>(v1: &[N], v2: &[N]) -> N
where N: Add<Output=N> + Mul<Output=N> + Default + Copy
{
let mut total = N::default();
for i in 0..v1.len() {
total = total + v1[i] * v2[i];
}
total
}这个例子中,泛型N如果仅仅是实现了Add特型的类型,就会存在很多类型,如i32和f32,都实现了,这样就没法保证
v1[i]
和v2[i]
的值是同一种类型,因此需要限定他们的Output的类型都是同一种;
实用特型
特型 | 简介 |
---|---|
Drop | 相当于*C++*中的解构函数,会在清除的时候被动调用 |
Sized | 固定大小的Trait,大多数类型是固定大小的(不太理解这个) |
Clone | 复制一个独立的副本,简单实现只需要添加#[derive(Clone)] 即可 |
Copy | 基于编译器的语义,可以实现实体类型的复制(对象的复制需要定义的字段都能复制才行),逐位拷贝 |
Deref与DerefMut | 实现这两个Trait可以自定义引用*和解引用.操作符上的自定义行为 |
Default | 具备了默认值的Trait,如数值类型默认值是0,字符串默认值是空的 |
AsRef与AsMut | 引用和可修改引用 |
Borrow与BorrowMut | 借用和可修改借用 |
From与Into | 类型转换特型 |
ToOwned | 给定一个引用,产生其引用目标的所有型副本 |
drop()
会在值被清除的时候,被隐式调用,该方法无法被显式调用;drop()
在面对需要使用unsafe
资源进行释放时,是很有用的;- 实现了Drop,则不能实现Copy;
- Copy和Clone一般一起使用
#[derive(Clone, Copy)]
; - Copy和Clone的区别;
- 实现Copy需要考虑清楚,隐式复制的代价很大的;
- 自定义解引用在应对泛型时,需要注意解引用后的类型和解引用前的类型是否有泛型上的差别,否则需要强转,具体解释看Rust程序设计239页解释;
- str和*[T]*类型都是非固定大小的,而
&str
这种引用是固定大小的; - Rust中闭包的性能是没有开销的,编译器会显示内联优化,减少函数和方法调用;
- 对于任意类型的T和U,如果
T: AsRef<U>
,则&T: AsRef<U>
也成立; - Borrow的限制:只有当
&T
与它所借用的值具有相同的散列和比较特性时,一个类型才可以实现*Borrow*; - Clone一个*&T必须返回一个类型T*的值;
闭包
fn(&City)->bool
是闭包的一种类型,仅接受函数;Fn(&City)->bool
是闭包的一种特型, 包括函数和闭包;对
Fn
和FnOnce
的理解:如果一个闭包中,只有普通的代码,没有传值进行所有权的转移,则其应该是
Fn
特型的闭包;1
2
3
4
5
6
7
8
9
10
11let my_str3 = "hello3".to_string();
let block3 = || {
let a = 1;
let b = 2;
if a + b == my_str3.len() {
println!("equal");
} else {
println!("not equal");
}
};
call_twice(block3);如果一个闭包中,存在了所有权的转移,那么这个闭包仅能调用一次,其是
FnOnce
特型的闭包,如果对其调用多次,会报错,因为不符合所有权系统的规则(试想一个函数入参是所有权转移,然后会存在返回值,这个返回值的所有权和这个入参一致,如果调用了两次,就会把一个值的所有权赋值给两个变量,这是不符合所有权系统的规则的);1
2
3
4let my_str = "hello".to_string();
let block = || drop(my_str); // this block's trait is FnOnce,beacuse drop receive a var, not a ref
block();
block(); // error如果一个闭包中,仅存在借用,那么这个闭包还是
Fn
特型的闭包;1
2
3let my_str2 = "hello2".to_string();
let block2 = || println!("this is {}", &my_str2); // this block's trait is Fn,println receive a ref;
call_twice(block2);如果一个闭包中,即存在所有权转移,又存在借用,那这个还是
FnOnce
特型的闭包;1
2
3
4
5
6let my_str4 = "hello2".to_string();
let block_fn = || {
println!("this is {} 2", &my_str4);
drop(my_str4);
};
call_twice(block_fn); // error
总结下来就是,
FnOnce
闭包是闭包设计中实现所有权的一个方式。需要注意的一点是,在调用FnOnce
的闭包时,闭包本身也会被使用掉,这个设计和所有权是如出一辙的。
迭代器
可迭代类型指的是实现了
IntoIterator
trait的类型,可以通过into_iter()
方法取得其迭代器;使用
into_iter()
方法时,需要加上借用,因为这个方法是返回的所有权,而iter()和iter_mut()
方法返回的迭代器不需要加上借用;值传递的集合,在迭代时,消费者会取得其所有权,在迭代器迭代完成后,集合就会清空;
vec使用
join()
方法可以在每个item直接添加一个元素;对于消费的理解,
["earth", "water", "air", "fire"].iter().map(|elt| println!("{}", elt));
这句代执行后实际不会打印任何值,因为这句代码里没有产生消费,当在最后加上.next()
方法时,会打印第一个值,因为仅消费了一个,这一点和Kotlin是有很大区别的,在Kotlin中调用map()
方法就意味着已经开始产生消费了,这里可以这样修改:1
2
3
4
5
6
7let s = ["earth", "water", "air", "fire"].iter().map(|elt| {
println!("map:{}", elt);
elt
});
for item in s {
println!("for:{}", item)
}这里使用
for.. in
,实际上是连续调用了next()
方法来产生迭代值,不过这里可以猜猜打印的结果是什么。答案是**map:earth\nfor:earth\nmap:water\nfor:water….**,是的,最终调用
next()
时,会将map()
的操作一起执行出来;scan()
方法,类似map()
,区别是其传递给闭包的是可修改的值,并在接受到None值时,会提前终止迭代;take()和take_while()
使用方法上和Kotlin没有区别,都是获取指定数目和指定条件的数据;skip()和skip_while()
使用方法同Kotlin;peekable()
,可预测的迭代器,当我们需要对迭代的下一项进行判断才决定消费时,非常有用;需要练习这个;fuse()
可以在第一次返回None后一直返回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
27struct Flaky(bool);
impl Iterator for Flaky {
type Item = &'static str;
fn next(&mut self) -> Option<Self::Item> {
if self.0 {
self.0 = false;
Some("Totoal")
} else {
self.0 = true;
None
}
}
}
fn test_fuse() {
let mut flaky = Flaky(true);
assert_eq!(flaky.next(), Some("Totoal"));
assert_eq!(flaky.next(), None);
assert_eq!(flaky.next(), Some("Totoal"));
let mut not_flaky = Flaky(true).fuse();
assert_eq!(not_flaky.next(), Some("Totoal"));
assert_eq!(not_flaky.next(), None);
assert_eq!(not_flaky.next(), None);
}by_ref()
借用迭代器的可修改引用,因为迭代后还可以继续使用原迭代器,这个方法不会取得原迭代器的所有权;但是迭代后该消费的迭代还是会消费掉,也就是说调用了这个方法,你迭代后,还可以继续迭代,因为所有权还在,其他的迭代方法获取所有权,迭代后原迭代器就不能使用了;cycle()
,返回一个无休止重复的底层迭代器;
集合
标准集合
集合 | 说明 |
---|---|
Vec |
可增数组 |
VecDeque |
双端队列 |
LinkedList |
双向链表 |
BinaryHeap |
最大堆 |
HashMap<K,V> where K : Eq + Hash | 键-值散列表 |
BTreeMap<K,V> | 有序键-值表 |
HashSet |
散列表 |
BTreeSet |
有序集 |
join()
方法需要传入一个Separator类型的参数,其作用是将元素加入分割器后返回,而&T
是实现了Separator的类型,因此[[1, 2], [3, 4]].join(&100)
的结果是[1, 2, 100, 3, 4]
;注意机器类型无法使用join()
;未实现对应的trait;join()
和contact()
是用于处理数组的数组;
并发
- 在Rust中,move关键字会将当前的变量转移到子线程中,这种转移的开销很小;
- 在Rust中,子线程的
panic
会被主线程当做一个错误进行处理,而不是一个panic
;不过这个错误需要我们显示的使用Result::Err来进行处理;