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. 内网安全之:MS14-068 Kerberos 域用户提权漏洞

    内网安全之:MS14-068 Kerberos 域用户提权漏洞 目录 内网安全之:MS14-068 Kerberos 域用户提权漏洞 0 漏洞说明 (MS14-068:CVE-2014-6324) 1 ...

  2. Win下依据端口排查步骤

    Win下依据端口排查步骤 目录 Win下依据端口排查步骤 1 排查步骤 2 参考 今天突然发现自己电脑出现7680端口的内网连接,决定看看这个端口是做什么用的 1 排查步骤 确认7680端口连接情况与 ...

  3. SQL语句中 left join 后用 on 还是 where,区别大了!

    前天写SQL时本想通过 A left B join on and 后面的条件来使查出的两条记录变成一条,奈何发现还是有两条. 后来发现 join on and 不会过滤结果记录条数,只会根据and后的 ...

  4. select去除默认样式

    select { /*Chrome同Firefox与IE里面的右侧三角显示的样式不同*/ border: solid 1px #ddd; /*将默认的select选择框样式清除*/ appearanc ...

  5. 桌面应用(.exe)设置窗口默认最大化、全屏(electron)

    设置窗口默认最大化.全屏(electron) 一.默认最大化 win = new BrowserWindow({show: false}) win.maximize() win.show() 二.默认 ...

  6. 【win+r】快速打开软件

    第一种设置方法: ①把要启动的文件夹路径添加到系统环境变量里面去 ②按win+r就可以快速启动了 第二种设置方法: 是直接把(程序的)快捷方式,放到安装目录的 Windows\System32\ 文件 ...

  7. 第八周作业-N67044-张铭扬

    1. 完成ftp的mysql虚拟用户 数据库服务器:10.0.0.152 FTP服务器:10.0.0.156 #配置数据库服务器 [root@centos8 ~]# yum -y install ma ...

  8. RDD编程练习

    一.filter,map,flatmap练习: 1.读文本文件生成RDD lines 2.将一行一行的文本分割成单词 words 3.全部转换为小写 4.去掉长度小于3的单词 5.去掉停用词 6.练习 ...

  9. 了解ASP (一)

    1. ASP是什么? ASP 指 Active Server Pages (动态服务器页面), 是在 IIS 中运行的程序.ASP 文件的扩展名是 ".asp" 2. ASP与HT ...

  10. 下载nodejs和vue

    下载nodejs https://nodejs.org/en 下载或更新npm npm install cnpm -g npm install -g vue 全局安装 创建一个基于 "web ...