Rust智能指针
https://course.rs/advance/smart-pointer/intro.html
Box 堆对象分配
Box指针拥有内存对象的独占使用权
(一)使用场景
1. 使用 Box 将数据存储在堆上
fn main() {
let a = Box::new(3);
println!("a = {}", a); // a = 3
// 下面一行代码将报错
// let b = a + 1; // cannot add `{integer}` to `Box<{integer}>`
}
-
println! 可以正常打印出 a 的值,是因为它隐式地调用了 Deref 对智能指针 a 进行了解引用
-
最后一行代码 let b = a + 1 报错,是因为在表达式中,我们无法自动隐式地执行 Deref 解引用
-
a 持有的智能指针将在作用域结束(main 函数结束)时,被释放掉,这是因为** Box
实现了 Drop 特征**
2. 避免栈上数据的拷贝
当栈上数据转移所有权时,实际上是把数据拷贝了一份,最终新旧变量各自拥有不同的数据,因此所有权并未转移。
而堆上则不然,底层数据并不会被拷贝,转移所有权仅仅是复制一份栈中的指针,再将新的指针赋予新的变量,然后让拥有旧指针的变量失效,最终完成了所有权的转移:
fn main() {
// 在栈上创建一个长度为1000的数组
let arr = [0;1000];
// 将arr所有权转移arr1,由于 `arr` 分配在栈上,因此这里实际上是直接重新深拷贝了一份数据
let arr1 = arr;
// arr 和 arr1 都拥有各自的栈上数组,因此不会报错
println!("{:?}", arr.len());
println!("{:?}", arr1.len());
// 在堆上创建一个长度为1000的数组,然后使用一个智能指针指向它
let arr = Box::new([0;1000]);
// 将堆上数组的所有权转移给 arr1,由于数据在堆上,因此仅仅拷贝了智能指针的结构体,底层数据并没有被拷贝
// 所有权顺利转移给 arr1,arr 不再拥有所有权
let arr1 = arr;
println!("{:?}", arr1.len());
// 由于 arr 不再拥有底层数组的所有权,因此下面代码将报错
// println!("{:?}", arr.len());
}
3. 将动态大小类型转变为Sized固定大小类型
Rust 需要在编译时知道类型占用多少空间,如果一种类型在编译时无法知道具体的大小,那么被称为动态大小类型 DST。
函数式语言中常见的Cons List,它的每个节点包含一个 i32 值,还包含了一个新的 List,因此这种嵌套可以无限进行下去,Rust 认为该类型是一个 DST 类型,并给予报错:
enum List {
Cons(i32, List),
Nil,
}
//error[E0072]: recursive type `List` has infinite size //递归类型 `List` 拥有无限长的大小 --> src/main.rs:3:1
可以使用Box
enum List {
Cons(i32, Box<List>),
Nil,
}
现在Cons的第二个参数是一个Box指针,大小是固定的,从而完成了从DST到Sized类型的华丽转变。
4. 特征对象——实现不同类型组成的数组。
trait Draw {
fn draw(&self);
}
struct Button {
id: u32,
}
impl Draw for Button {
fn draw(&self) {
println!("这是屏幕上第{}号按钮", self.id)
}
}
struct Select {
id: u32,
}
impl Draw for Select {
fn draw(&self) {
println!("这个选择框贼难用{}", self.id)
}
}
fn main() {
let elems: Vec<Box<dyn Draw>> = vec![Box::new(Button { id: 1 }), Box::new(Select { id: 2 })];
for e in elems {
e.draw()
}
}
以上代码将不同类型的 Button 和 Select 包装成 Draw 特征的特征对象,放入一个数组中,Box
其实,特征也是 DST 类型,而特征对象在做的就是将 DST 类型转换为固定大小类型。
(二)Box内存布局
1. Vec的内存布局
之前提到过 Vec 和 String 都是智能指针,从上图可以看出,该智能指针存储在栈中,然后指向堆上的数组数据。
(stack) (heap)
┌──────┐ ┌───┐
│ vec1 │──→│ 1 │
└──────┘ ├───┤
│ 2 │
├───┤
│ 3 │
├───┤
│ 4 │
└───┘
2. Vec<Box的内存布局
可以看出智能指针 vec2 依然是存储在栈上,然后指针指向一个堆上的数组,该数组中每个元素都是一个 Box 智能指针,最终 Box 智能指针又指向了存储在堆上的实际值。
(heap)
(stack) (heap) ┌───┐
┌──────┐ ┌───┐ ┌─→│ 1 │
│ vec2 │──→│B1 │─┘ └───┘
└──────┘ ├───┤ ┌───┐
│B2 │───→│ 2 │
├───┤ └───┘
│B3 │─┐ ┌───┐
├───┤ └─→│ 3 │
│B4 │─┐ └───┘
└───┘ │ ┌───┐
└─→│ 4 │
└───┘
(三)Box::leak
使用场景:当需要一个在运行期初始化的值,但是可以全局有效,也就是和整个程序活得一样久,那么就可以使用 Box::leak。
fn main() {
let s = gen_static_str();
println!("{}", s);
}
fn gen_static_str() -> &'static str{
let mut s = String::new();
s.push_str("hello, world");
Box::leak(s.into_boxed_str())
}
Rc 单线程共享只读
(一)认知Rc——Reference counting
1. 为什么要引用计数(reference counting)?
通过记录一个数据被引用的次数来确定该数据是否正在被使用。当引用次数归零时,就代表该数据不再被使用,因此可以被清理释放。
2. Rc::clone
- Rc::clone 克隆了一份智能指针,对应内存对象引用计数+1
use 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))
}
- 这里的 clone 仅仅复制了智能指针并增加了引用计数,并没有克隆底层数据,因此 a 和 b 是共享了底层的字符串 s,这种复制效率是非常高的。
3. 借用规则
Rc
(二)简单总结
- Rc/Arc 是不可变引用,你无法修改它指向的值,只能进行读取,如果要修改,需要配合后面章节的内部可变性 RefCell 或互斥锁 Mutex
- 一旦最后一个拥有者消失,则资源会自动被回收,这个生命周期是在编译期就确定下来的
- Rc 只能用于同一线程内部,想要用于线程之间的对象共享,你需要使用 Arc
- Rc
是一个智能指针,实现了 Deref 特征,因此你无需先解开 Rc 指针 ,再使用里面的 T,而是可以直接使用 T。
(三)Rc的多线程问题
-
Rc
不能在线程间安全的传递,实际上是因为它没有实现 Send 特征,而该特征是恰恰是多线程间传递数据的关键 -
由于 Rc
需要管理引用计数,但是该计数器并没有使用任何并发原语,因此无法实现原子化的计数操作,最终会导致计数错误。
Arc 多线程计数安全共享只读
认知Arc
Arc 是 Atomic Rc 的缩写,顾名思义:原子化的 Rc
原因在于原子化或者其它锁虽然可以带来的线程安全,但是都会伴随着性能损耗,而且这种性能损耗还不小。因此 Rust 把这种选择权交给你,毕竟需要线程安全的代码其实占比并不高,大部分时候我们开发的程序都在一个线程内。
Rc 和 Arc 的区别在于,后者是原子化实现的引用计数,因此是线程安全的,可以用于多线程中共享数据。
这两者都是只读的,如果想要实现内部数据可修改,必须**配合内部可变性 RefCell 或者互斥锁 Mutex **来一起使用。
Cell 和 RefCell提供内部可变性
(一)用途
可以在拥有不可变引用的同时修改目标数据。内部可变性的实现是因为 Rust 使用了** unsafe **来做到这一点。
(二)Cell使用
Cell 和 RefCell 在功能上没有区别,区别在于 Cell
use std::cell::Cell;
fn main() {
let c = Cell::new("asdf");
let one = c.get();
c.set("qwer");
let two = c.get();
println!("{},{}", one, two);
}
如果改成:
use std::cell::Cell;
fn main() {
let c = Cell::new(String::from("fdjka"));
let one = c.get();
c.set("qwer");
let two = c.get();
println!("{},{}", one, two);
}
编译器会报错:
4 | let one = c.get();
| ^^^
--> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/alloc/src/string.rs:367:1
|
= note: doesn't satisfy `String: Copy`
(三)RefCell使用
- RefCell解决的问题
由于 Cell 类型针对的是实现了 Copy 特征的值类型,因此在实际开发中,Cell 使用的并不多,因为我们要解决的往往是可变、不可变引用共存导致的问题,此时就需要借助于 RefCell 来达成目的。
Rust 规则 | 智能指针带来的额外规则 |
---|---|
一个数据只有一个所有者 | Rc/Arc让一个数据可以拥有多个所有者 |
要么多个不可变借用,要么一个可变借用 | RefCell实现编译期可变、不可变引用共存 |
违背规则导致编译错误 | 违背规则导致运行时panic |
以下代码打破了Rust规则,在编译期不会报任何错误,可以顺利运行程序,但是依然会因为违背了借用规则导致了运行期 panic。
use 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);
}
- RefCell 简单总结
- 与 Cell 用于可 Copy 的值不同,RefCell 用于引用
- RefCell 只是将借用规则从编译期推迟到程序运行期,并不能帮你绕过这个规则
- RefCell 适用于编译期误报或者一个引用被在多处代码使用、修改以至于难于管理借用关系时
- 使用 RefCell 时,违背借用规则会导致运行期的 panic
(四)Cell vs RefCell
- Cell 只适用于 Copy 类型,用于提供值,而 RefCell 用于提供引用
- Cell 不会 panic,而 RefCell 会。
(五)Rc + RefCell组合使用
前者可以实现一个数据拥有多个所有者,后者可以实现数据的可变。
- 例子
use 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(", on yeah!");
println!("{:?}\n{:?}\n{:?}", s, s1, s2);
}
由于 Rc 的所有者们共享同一个底层的数据,因此当一个所有者修改了数据时,会导致全部所有者持有的数据都发生了变化。
RefCell { value: "我很善变,还拥有多个主人, on yeah!" }
RefCell { value: "我很善变,还拥有多个主人, on yeah!" }
RefCell { value: "我很善变,还拥有多个主人, on yeah!" }
- 性能
-
性能损耗:两者结合在一起使用的性能其实非常高,大致相当于没有线程安全版本的 C++ std::shared_ptr 指针,事实上,C++ 这个指针的主要开销也在于原子性这个并发原语上,毕竟线程安全在哪个语言中开销都不小。
-
内存损耗:
两者结合的数据结构与下面类似。从对内存的影响来看,仅仅多分配了三个usize/isize,并没有其它额外的负担。
struct Wrapper<T> {
// Rc
strong_count: usize,
weak_count: usize,
// Refcell
borrow_count: isize,
// 包裹的数据
item: T,
}
-
CPU损耗:解引用、改变引用计数带来的消耗。
-
CPU缓存misss
(六)通过 Cell::from_mut 解决借用冲突
在 Rust 1.37 版本中新增了两个非常实用的方法:
- Cell::from_mut,该方法将 &mut T 转为 &Cell
- Cell::as_slice_of_cells,该方法将 &Cell<[T]> 转为 &[Cell
]
(七)总结
Cell 和 RefCell 都为我们带来了内部可变性这个重要特性,同时还将借用规则的检查从编译期推迟到运行期,但是这个检查并不能被绕过,该来早晚还是会来,RefCell 在运行期的报错会造成 panic。
RefCell 适用于编译器误报或者一个引用被在多个代码中使用、修改以至于难于管理借用关系时,还有就是需要内部可变性时。
从性能上看,RefCell 由于是非线程安全的,因此无需保证原子性,性能虽然有一点损耗,但是依然非常好,而 Cell 则完全不存在任何额外的性能损耗。
Rc 跟 RefCell 结合使用可以实现多个所有者共享同一份数据,非常好用,但是潜在的性能损耗也要考虑进去,建议对于热点代码使用时,做好 benchmark。
标签:Box,Cell,智能,let,Rc,RefCell,Rust,指针 From: https://www.cnblogs.com/qiangz/p/17114260.html