Zj_W1nd's BLOG

Rust?Rust!(3)——自用整合终极版

2024/02/05

说实话没有开发需求在看这些新机制的时候没有动力了,打算搞个Web应用或者什么试试看?
子模块内容对父模块全部不可见,但是子模块可以访问其所有上级模块内容。特殊
生命周期:引用一个比自己活得久的变量才会通过编译
特征对象依托,反正知道闭包什么的复杂的类型直接先用Box包裹一下
Reborrow再借用:对一个可变引用可以再次借用,只要在再次借用的变量的生命周期内不适用上级的引用就不会报错。许多传入了(可变)引用参数的函数在函数内调用方法的时候不报错是因为这个规则的存在

https://github.com/LearningOS/rust-rustlings-2024-autumn-ZjW1nd

闭包

闭包:FnOnce,FnMut和Fn,依次代表调用时直接拿走闭包所有权,捕获可变引用和获取不可变引用。这三个东西是trait,实现FnMut必须要实现FnOnce,实现Fn必须要实现FnMut。所以约束的时候先无脑声明Fn然后看编译器。实际上:

  • 所有的闭包都自动实现了 FnOnce 特征,因此任何一个闭包都至少可以被调用一次
  • 没有移出所捕获变量的所有权的闭包自动实现了 FnMut 特征
  • 不需要对捕获变量进行改变的闭包自动实现了 Fn 特征返回闭包要用特征对象(每一个闭包都是独一无二的类型,即使他们的定义相同)
    同样的定义如果遇到了生命周期问题尽量改成函数,编译器无法在闭包上应用像函数那样智能的消除规则。

智能指针

Box

Box智能指针:用jemalloc管理的智能指针,强制将其包裹的对象分配在堆上,然后在栈上保留一份指针。这种操作主要是为了处理动态大小的对象(如递归的类型),因为Box实际上某种意义上更改了数据类型以及其底层操作和存储逻辑,会少很多方法。但是许多标准方法自动提供了自动Deref解引用。
Box指针作为右值的时候,栈上的指针发生了copy,但是堆上的数据发生了所有权的转移。即let a=b之后,原Box指针b无法再访问数据。在Rust的表达式中无法进行自动的解引用,需要用*手动解引用
Box::leak函数:消费掉Box并强制目标从内存中泄露(??),可以用于将运行中生成的值转为’static周期。

Box::from_raw()和Box::into_raw可以操作unsafe裸指针
rust中的智能指针本质上是一种比直接引用更复杂的数据类型(一个结构体),但实现了deref和drop方法。Box只是最简单的一种,仅仅只是将值强制分配在堆上而已。其实其他有GC的语言本质上也是这样,只不过是自动的

deref特性

解引用发生了什么:指针解引用的时候会变成*(p.deref()),deref方法会返回一个常规的引用而不是值,从而避免智能指针一被解引用,其中指向的数据所有权就被拿走(那太抽象了)。而且deref()只会替换调用一次。
隐式转换:对于实现了Deref特征的数据类型,在做参数的时候编译器会根据函数/方法的签名决定是否进行自动隐式的deref转换。比如String类型会自动转为&str这样:

1
2
3
4
5
6
7
8
fn main() {
let s = String::from("hello world");
display(&s)
}

fn display(s: &str) {
println!("{}",s);
}

可以连续隐式转换,参数是引用类型的时候才会触发。另外要记住,Deref特征只为“引用”类型实现,也就是deref函数的self参数实际上是:self: &Self的意思。总之,记住多个引用的时候每调用一次deref会少一个&就行了(引用归一化)。Deref还能将可变引用隐式转换为不可变引用。还有DerefMut: Deref这个特征,允许将可变引用转为另一个可变引用

Drop特征赛高!如果实现了Drop特征,编译器在变量离开作用域时会自动插入代码执行drop函数。作用域内的变量按逆序释放,结构体内的字段按定义顺序(而非创建顺序)依次释放。几乎所有数据类型都有drop特征,它会传入self的可变引用。
drop使用可变引用就会引入一个问题:编译器不允许我们手动调用Drop::drop析构函数(显然会导致UAF)。手动释放使用的是std::mem::drop函数,它会拿走self的所有权。然后在其内部再调用Drop::drop进行释放

1VN所有权:智能指针Rc和Arc

解决多线程和图等需要多个变量持有一个数据的问题。Rc(Reference counting)引用计数智能指针,它可以复制很多副本。参考:

1
2
3
4
5
6
7
8
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("hello, world"));//引用计数+1
let b = Rc::clone(&a);//引用计数+1,浅拷贝

assert_eq!(2, Rc::strong_count(&a));
assert_eq!(Rc::strong_count(&a), Rc::strong_count(&b))
}

rc的clone方法是浅拷贝,仅复制指针并增加引用计数。rc内部是数据的不可变借用,无法通过rc修改数据。修改见多线程,智能指针比引用好用很多,同线程多个读可以多用rc。多线程用Arc(原子化,线程安全),接口一样。

Cell/RefCell

使用内部可变性Cell/RefCell智能指针允许在拥有不可变引用的同时修改数据,Cell/RefCell内部是用了unsafe特性实现的,但是封装后完全safe。

1
2
3
4
5
6
7
8
use std::cell::Cell;
fn main() {
let c = Cell::new("asdf");
let one = c.get();//one是一个c的不可变借用
c.set("qwer");//修改值,正常情况下编译器是不允许的
let two = c.get();
println!("{},{}", one, two);
}

Cell是针对实现了Copy特征的类型,RefCell实现了其他的类型。

试了一下,Cell没啥用,c如果声明为mut直接改也可以,实现了Copy直接写赋值就行。对于堆上的数据,借用规则在运行的时候仍然是生效的。使用RefCell只是在编译的时候不会报错,将问题推迟到了运行时(panic)。RefCell的使用只是在你坚信代码正确而编译器发电的时候,或者某个引用被大量的使用和修改难以管理的时候。

Weak

weak: 不持有所有权的RC,这玩意不保证引用关系的存在(不持有所有权所以数据不在了也无所谓)。使用weak的upgrade()方法会返回一个Option<RC(T)>类型,有值是Some(RC(T)),没有就返回None。用weak和RC结合能够避免链表的循环引用问题发生,即子节点和父节点之间的引用使用weak和rc交替。

NonNull

这个智能指针能够操作裸指针,它包裹的指针能够保证不为空,解引用需要unsafe声明,可以用Option<NonNull<Box<...>>>这种东西加上一些模式匹配和结构来使用类似C不过更安全的指针思想。Rust会强迫你在每一个裸指针处思考,不灵活但是安全。看不懂 自引用和链表一坨大便

并发

  • thread::spawn开新线程,参数中传入一个闭包代码块作为新线程执行的内容

  • thread::spawn返回一个JoinHandle结构,调用join方法可以强制当前线程阻塞等待JoinHandle指向的线程结束

  • Arc::clone线程安全的复制数据

  • Mutex互斥锁,也是智能指针,new了之后包裹的内容需要先使用.lock()申请锁才能写入,此时其他线程都无法写入
    async和await,先略

生命周期

一般来说手写比较没必要,结构体成员用引用或者函数返回引用的时候要用。rust编译器有三条生命周期消除规则:

  • 首先为函数的每个参数设置一个单独的生命周期

  • 如果只有一个输入的生命周期,则其会返回给全部的输出生命周期

  • 如果有多个输入生命周期而其中一个是&self,那么self的生命周期会返回给所有的输出生命周期所以,生命周期在多个引用非self输入的构造方法,或者在结构体和枚举中使用引用类型才会用到。

特征接口Trait

一类方法,标注这个特性的类型就能使用对应的方法,也可以用trait来约束泛型。

CATALOG
  1. 1. 闭包
  2. 2. 智能指针
    1. 2.1. Box
    2. 2.2. deref特性
    3. 2.3. 1VN所有权:智能指针Rc和Arc
    4. 2.4. Cell/RefCell
    5. 2.5. Weak
    6. 2.6. NonNull
  3. 3. 并发
  4. 4. 生命周期
  5. 5. 特征接口Trait