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<i32>的内存布局

可以看出智能指针 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 是指向底层数据的不可变的引用,因此你无法通过它来修改数据,这也符合 Rust 的借用规则:要么存在多个不可变借用,要么只能存在一个可变借用。

(二)简单总结

  • 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 适用于 T 实现 Copy 的情况:


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使用

  1. 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);
}
  1. RefCell 简单总结
  • 与 Cell 用于可 Copy 的值不同,RefCell 用于引用
  • RefCell 只是将借用规则从编译期推迟到程序运行期,并不能帮你绕过这个规则
  • RefCell 适用于编译期误报或者一个引用被在多处代码使用、修改以至于难于管理借用关系时
  • 使用 RefCell 时,违背借用规则会导致运行期的 panic

(四)Cell vs RefCell

  • Cell 只适用于 Copy 类型,用于提供值,而 RefCell 用于提供引用
  • Cell 不会 panic,而 RefCell 会。

(五)Rc + RefCell组合使用

前者可以实现一个数据拥有多个所有者,后者可以实现数据的可变。

  1. 例子
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!" }
  1. 性能
  • 性能损耗:两者结合在一起使用的性能其实非常高,大致相当于没有线程安全版本的 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 版本中新增了两个非常实用的方法:

  1. Cell::from_mut,该方法将 &mut T 转为 &Cell
  2. Cell::as_slice_of_cells,该方法将 &Cell<[T]> 转为 &[Cell]

(七)总结

Cell 和 RefCell 都为我们带来了内部可变性这个重要特性,同时还将借用规则的检查从编译期推迟到运行期,但是这个检查并不能被绕过,该来早晚还是会来,RefCell 在运行期的报错会造成 panic。

RefCell 适用于编译器误报或者一个引用被在多个代码中使用、修改以至于难于管理借用关系时,还有就是需要内部可变性时。

从性能上看,RefCell 由于是非线程安全的,因此无需保证原子性,性能虽然有一点损耗,但是依然非常好,而 Cell 则完全不存在任何额外的性能损耗。

Rc 跟 RefCell 结合使用可以实现多个所有者共享同一份数据,非常好用,但是潜在的性能损耗也要考虑进去,建议对于热点代码使用时,做好 benchmark。

Rust智能指针的更多相关文章

  1. Rust 智能指针(一)

    Rust 智能指针(一) 1.Box<T> Box<T>是指向堆中的指针. fn main() { let box = Box::new(3); println!(" ...

  2. 探讨 Rust 智能指针 | Vol.17

    分享主题:<探讨 Rust 智能指针>| Vol. 17 分享讲师:苏林 分享时间: 周日晚上 2021-11-14 20:30-21:30 腾讯会议地址: https://meeting ...

  3. Rust 智能指针(二)

    1. Rc<T> 引用计数指针 Rc<T> 是引用计数指针,可以使用clone使得指针所指向的数据具有多个所有者. enum List { Cons(i32, Rc<Li ...

  4. Rust入坑指南:智能指针

    在了解了Rust中的所有权.所有权借用.生命周期这些概念后,相信各位坑友对Rust已经有了比较深刻的认识了,今天又是一个连环坑,我们一起来把智能指针刨出来,一探究竟. 智能指针是Rust中一种特殊的数 ...

  5. [易学易懂系列|rustlang语言|零基础|快速入门|(21)|智能指针]

    [易学易懂系列|rustlang语言|零基础|快速入门|(21)|智能指针] 实用知识 智能指针 我们今天来讲讲Rust中的智能指针. 什么是指针? 在Rust,指针(普通指针),就是保存内存地址的值 ...

  6. 转:C++ 智能指针的正确使用方式

    转:https://www.cyhone.com/articles/right-way-to-use-cpp-smart-pointer/#comments C++11 中推出了三种智能指针,uniq ...

  7. c++11新特性实战(二):智能指针

    c++11添加了新的智能指针,unique_ptr.shared_ptr和weak_ptr,同时也将auto_ptr置为废弃(deprecated). 但是在实际的使用过程中,很多人都会有这样的问题: ...

  8. enote笔记法使用范例(2)——指针(1)智能指针

    要知道什么是智能指针,首先了解什么称为 “资源分配即初始化” what RAII:RAII—Resource Acquisition Is Initialization,即“资源分配即初始化” 在&l ...

  9. C++11 shared_ptr 智能指针 的使用,避免内存泄露

    多线程程序经常会遇到在某个线程A创建了一个对象,这个对象需要在线程B使用, 在没有shared_ptr时,因为线程A,B结束时间不确定,即在A或B线程先释放这个对象都有可能造成另一个线程崩溃, 所以为 ...

  10. C++智能指针

    引用计数技术及智能指针的简单实现 基础对象类 class Point { public: Point(int xVal = 0, int yVal = 0) : x(xVal), y(yVal) { ...

随机推荐

  1. 使用IDM从Google 云端硬盘链接上下载超大文件

    1.将原始文件以快捷方式存放到自己的网盘中. 2.进入自己的网盘,找到存放好的目标文件快捷方式,点击右键,选择下载. 3.如果电脑上IDM且浏览器装有IDM插件,会弹出下载框,点击下载即可. 4.然后 ...

  2. 代码随想录算法训练营day14 | leetcode 层序遍历 226.翻转二叉树 101.对称二叉树 2

    层序遍历 /** * 二叉树的层序遍历 */ class QueueTraverse { /** * 存放一层一层的数据 */ public List<List<Integer>&g ...

  3. .NET依赖注入之一个接口多个实现

    前言 最近又在项目中碰到需要将原本单实现的接口改造成多个实现的场景,这里记录一下常见的几种改法. 假设已经存在如下接口ICustomService和其实现CustomService,由于只有一种实现, ...

  4. Mysql去重获取最新的一条数据

    Mysql去重获取最新的一条数据 select * from yjzt_kindergartens r where id in (select max(id) from yjzt_kindergart ...

  5. python 获取近几周日期

    import datetimedef get_Next_day(count): today = datetime.datetime.today().date() for i in range(coun ...

  6. Idea2020.2.3 创建JavaWeb项目(部署Tomcat)方法

    1.创建项目不再是Java Enterprise了,而是先New 一个普通Java项目! 2.创建项目后,选择Run->Edit Configuration->左上角加号->Tomc ...

  7. (jmeter笔记)jmeter用正则匹配响应数据,满足两个条件

    方法1.用正则匹配响应数据,满足两个条件 \{[^}]+?"Id":(\d*)[^}]+?"Picking"[^}]+?\} 方法2. \{"Pick ...

  8. 32位机转化11位手机号以及BLE与USB的切换

    目录 用现有的资源,去实现本应该用更多资源来实现的需求,是一件很有意思的事情.不是说提倡这样使用,而是换一种思路解决问题比较新奇,或是在很多限制既定的情况下可以应急. 比如说,582m芯片,默认用32 ...

  9. 前端实现电子签名(web、移动端)通用组件(canvas实现)

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  10. K8S的 POD 生命周期

    pod的生命周期是从创建至终止的这段时间范围 Pod的创建 1.用户通过kubectl或其他api客户端提交需要创建的pod信息给apiServer 2.apiServer开始生成pod对象的信息,并 ...