当前位置: 首页 > news >正文

【Rust智能指针】Rust智能指针原理剖析与应用指导

在这里插入图片描述

✨✨ 欢迎大家来到景天科技苑✨✨

🎈🎈 养成好习惯,先赞后看哦~🎈🎈

🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。
🏆《博客》:Rust开发,Python全栈,Golang开发,云原生开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。

所属的专栏:Rust语言通关之路
景天的主页:景天科技苑

在这里插入图片描述

文章目录

  • Rust智能指针
    • 一、智能指针概述
      • 1.1 什么是智能指针
      • 1.2 Rust中常见的智能指针类型
    • 二、`Box<T>`:最简单的智能指针
      • 2.1 Box的基本用法
      • 2.2 使用Box实现递归类型
      • 2.3 Box解引用
      • 2.4 实现Deref trait
        • 2.4.1 解引用实现
        • 2.4.2 解引用强制多态
      • 2.5 drop trait清理代码
        • 2.5.1 实现drop trait
        • 2.5.2 drop提前释放
    • 三、`Rc<T>`:引用计数智能指针
      • 3.1 Rc的基本用法
      • 3.2 共享所有权场景
      • 3.3 Rc的限制
    • 四、`RefCell<T>`:内部可变性模式
      • 4.1 RefCell的基本用法
      • 4.2 内部可变性:不可变值的可变借用
      • 4.3 RefCell的运行时检查
    • 五、`Arc<T>`:线程安全的引用计数
      • 5.1 Arc的基本用法
      • 5.2 Arc与Mutex结合使用
    • 六、智能指针的高级用法
      • 6.1 自定义智能指针
      • 6.2 引用循环与内存泄漏
      • 6.3 弱引用(`Weak<T>`)
      • 6.4 创建树形数据结构:带有子结点的 Node
    • 七、智能指针的性能考量
      • 7.1 各种智能指针的性能特点
      • 7.2 何时使用何种智能指针
    • 八、智能指针组合使用
    • 九、常见问题与解决方案
      • 9.1 循环引用导致内存泄漏
      • 9.2 线程间共享数据的选择
      • 9.3 性能优化技巧
    • 十、总结

Rust智能指针

Rust语言以其内存安全和零成本抽象的特性而闻名,其中智能指针是实现这些特性的重要组成部分。
与C++中的智能指针类似,Rust的智能指针提供了比普通引用更丰富的功能,但在实现和使用上有着Rust特有的设计哲学。
智能指针是Rust中一种特殊的数据结构,它们不仅包含指针功能,还附加了额外的元数据和功能。Rust标准库提供了多种智能指针类型,每种都有其特定的用途。

一、智能指针概述

1.1 什么是智能指针

指针是一个包含内存地址的变量。这个地址指向一些其他的数据。
在Rust中,智能指针是一种数据结构,它不仅像普通指针一样指向某个内存地址,还拥有额外的元数据和功能。
最明显的,它们拥有一个引用计数。引用计数记录智能指针总共有多少个所有者,并且没有任何所有者时清除数据。
普通引用和智能引用的一个额外区别是:引用只是借用数据的指针,而智能指针则是拥有它们指向的数据。
智能指针通常使用结构体来实现。智能指针区别于常规结构体的显著特征在于智能指针通常实现Deref和Drop trait,这使得它们能够:
自动解引用(通过Deref)
在离开作用域时自动清理资源(通过Drop)

1.2 Rust中常见的智能指针类型

Rust标准库提供了多种智能指针,主要包括:
Box<T>:用于在堆上分配值
Rc<T>:引用计数指针,允许多重所有权
Arc<T>:原子引用计数指针,线程安全版本
RefCell<T>:提供内部可变性的运行时借用检查
Mutex<T>和RwLock<T>:用于线程间共享数据的同步原语

二、Box<T>:最简单的智能指针

2.1 Box的基本用法

Box是最简单的智能指针,它允许你将值存储在 堆上 而不是 栈上。留在栈上的则是指向堆数据的指针。
其类型为Box<T>

除了数据被储存在堆上而不是栈上之外,box 没有性能损失,不过也没有很多额外的功能。他们多用于如下场景:

  • 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
  • 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
  • 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候

本文的内容中主要展示第一种应用场景。作为对另外两个情况更详细的说明:
在第二种情况中,转移大量数据的所有权可能会花费很长的时间,因为数据在栈上进行了拷贝。
为了改善这种情况下的性能,可以通过 box 将这些数据储存在堆上。接着,只有少量的指针数据在栈上被拷贝。
第三种情况被称为 trait 对象(trait object)

fn main() {// Box智能指针,将数据存储在堆上// 存储在堆上的数据,在离开作用域时自动释放// Box::new(5) 创建一个指向整数5的Box// println!("b = {}", b); 打印b的值,b存放在栈上,5存放在堆上,b指向5所在的内存let b = Box::new(5);println!("b = {}", b);
}

这里定义了变量 b ,其值是一个指向被分配在堆上的值 5 的 Box 。
这个程序会打印出 b = 5 ;在这个例子中,我们可以像数据是储存在栈上的那样访问 box 中的数据。
正如任何拥有数据所有权的值那样,当像 b 这样的 box 在 main 的末尾离开作用域时,它将被释放。
这个释放过程作用于 box 本身(位于栈上)和它所指向的数据(位于堆上)。
将一个单独的值存放在堆上并不是很有意义,所以像 这样单独使用 box 并不常见。
将像单个 i32 这样的值储存在栈上,也就是其默认存放的地方在大部分使用场景中更为合适。

2.2 使用Box实现递归类型

Rust 需要在编译时知道类型占用多少空间。一种无法在编译时知道大小的类型是 递归类型(recursive type),其值的一部分可以是相同类型的另一个值。
这种值的嵌套理论上可以无限的进行下去,所以 Rust 不知道递归类型需要多少空间。
不过 box 有一个已知的大小,所以通过在循环类型定义中插入 box,就可以创建递归类型了。
Rust需要在编译时知道类型的大小,而递归类型的大小无法在编译时确定。Box可以帮助解决这个问题。

cons list,一个函数式编程语言中的常见类型,来展示这个(递归类型)概念。
除了递归之外,我们将要定义的 cons list 类型是很直白的,所以这个例子中的概念在任何遇到更为复杂的涉及到递归类型的场景时都很实用。
cons list 是一个每一项都包含两个部分的列表:当前项的值和下一项。其最后一项值包含一个叫做 Nil 的值并没有下一项。

cons list 是一个来源于 Lisp 编程语言及其方言的数据结构。
在 Lisp 中, cons 函数(“construct function" 的缩写)利用两个参数来构造一个新的列表,他们通常是一个单独的值和另一个列表。
cons 函数的概念涉及到更通用的函数式编程术语;“将 x 与 y 连接” 通常意味着构建一个新的容器而将 x 的元素放在新容器的开头,其后则是容器 y 的元素。
cons list 通过递归调用 cons 函数产生。代表递归的终止条件(base case)的规范名称是 Nil ,它宣布列表的终止。
注意这不同于 “null” 或 “nil” 的概念,他们代表无效或缺失的值。

注意虽然函数式编程语言经常使用 cons list,但是它并不是一个 Rust 中常见的类型。
大部分在 Rust 中需要列表的时候, Vec 是一个更好的选择。
其他更为复杂的递归数据类型 确实 在 Rust 的很多场景中很有用,
不过通过以 cons list作为开始,我们可以探索如何使用 box 毫不费力的定义一个递归数据类型。

因为 Box 是一个指针,我们总是知道它需要多少空间:指针的大小并不会根据其指向的数据量而改变。
所以可以将 Box 放入 Cons 成员中而不是直接存放另一个 List 值。
Box 会指向另一个位于堆上的 List 值,而不是存放在 Cons 成员中。
从概念上讲,我们仍然有一个通过在其中 “存放” 其他列表创建的列表,不过现在实现这个概念的方式更像是一个项挨着另一项,而不是一项包含另一项。

//使用Box实现递归类型
#[derive(Debug)]
#[allow(dead_code)]
enum List {Cons(i32, Box<List>),   //list是不确定大小的,但是智能指针Box是确定大小的,因此可以编译通过Nil,
}use List::{ Cons, Nil };fn main() {let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));println!("{:?}", list);
}

在这里插入图片描述

Cons 成员将会需要一个 i32 的大小加上储存 box 指针数据的空间。
Nil 成员不储存值,所以它比 Cons 成员需要更少的空间。现在我们知道了任何 List 值最多需要一个 i32 加上 box 指针数据的大小。
通过使用 box ,打破了这无限递归的连锁,这样编译器就能够计算出储存 List 值需要的大小了。下图展示了现在 Cons 成员看起来像什么:
在这里插入图片描述

因为 Cons 存放一个 Box 所以 List 不是无限大小的了

box 只提供了间接存储和堆分配;他们并没有任何其他特殊的功能,比如我们将会见到的其他智能指针。
他们也没有这些特殊功能带来的性能损失,所以他们可以用于像 cons list 这样间接存储是唯一所需功能的场景。
Box 类型是一个智能指针,因为它实现了 Deref trait,它允许 Box 值被当作引用对待。
当 Box 值离开作用域时,由于 Box 类型 Drop trait 的实现,box 所指向的堆数据也会被清除。

2.3 Box解引用

实现解引用特征Deref trait允许我们重载运算符。

//实现Deref trait允许我们重载解引用运算符
// let a: A = A::new(); //前提,A实现了Deref trait
// let b = &a;
// let c = *b; //解引用fn main() {let x = 5;let y = &x;//判断assert_eq!(5, x); //断言通过assert_eq!(5, *y); //解引用,断言通过//Box解引用,a定义在栈上,b定义在堆上let a = 5;let b = Box::new(a);assert_eq!(5, a);assert_eq!(5, *b); //Box可以自动解引用,断言通过
}

2.4 实现Deref trait

2.4.1 解引用实现

我们自定义个Box

//实现Debug trait
//定义个泛型结构体
struct MyBox<T>(T);//实现new方法
impl<T> MyBox<T> {fn new(x: T) -> MyBox<T> {MyBox(x)}
}fn main() {//使用MyBoxlet a = 5;let b = MyBox::new(5);assert_eq!(5, a);assert_eq!(5, *b);   //这里报错
}

当我们想要像Box一样使用时,会报错
提示我们创建的类型不能解引用
这是由于标准库中的Box实现了,Deref trait。我们创建的类型,没有实现Deref trait
在这里插入图片描述

我们自己实现下
看下标准库Deref这个trait
实现这个trait,只需要实现deref这个一个方法即可
在这里插入图片描述

//自定义Box实现Deref trait允许我们重载解引用运算符
//定义个泛型结构体
struct MyBox<T>(T);//实现new方法
impl<T> MyBox<T> {fn new(x: T) -> MyBox<T> {MyBox(x)}
}//实现Deref trait
//引入标准库的Deref trait
use std::ops::Deref;
impl<T> Deref for MyBox<T> {type Target = T;//实现Deref,只需要实现deref这一个方法//返回值是一个引用,指向MyBox内部的值fn deref(&self) -> &Self::Target {&self.0 //根据字段的个数来返回,我们得MyBox只有一个字段,所以返回&self.0}
}fn main() {//使用MyBoxlet a = 5;let b = MyBox::new(5);assert_eq!(5, a);//当我们的MyBox实现了Deref trait后,就可以自动解引用了assert_eq!(5, *b);
}

再次解引用,就不会报错了
在这里插入图片描述

2.4.2 解引用强制多态

解引用强制多态(deref coercions)是 Rust 出于方便的考虑作用于函数或方法的参数的。其将实现了 Deref 的类型的引用转换为 Deref 所能够将原始类型转换的类型的引用。
解引用强制多态发生于当作为参数传递给函数或方法的特定类型的引用不同于函数或方法签名中定义参数类型的时候,
这时会有一系列的 deref 方法调用会将提供的类型转换为参数所需的类型。
解引用强制多态的加入使得 Rust 程序员编写函数和方法调用时无需增加过多显式使用 & 和 * 的引用和解引用。
这个功能也使得我们可以编写更多同时作用于引用或智能指针的代码。

解引用强制多态
第一步,编译器会调用MyBox的deref方法,返回&String
第二步,编译器会调用String的deref方法,返回&str
这个过程叫做解引用多态

解引用多态与可变性交互
Rust 在发现类型和 trait 实现满足以下三种情况时会进行解引用强制多态:
1.当T:Deref<Target = U>时,从&T可以得到&U
2.当T:DerefMut<Target = U>时,从&mut T可以得到&mut U
3.当T:Deref<Target = U>时,从&mut T可以得到&U。反之则不行

头两个情况除了可变性之外是相同的:
第一种情况表明如果有一个 &T ,而 T 实现了返回 U 类型的 Deref ,则可以直接得到 &U 。
第二种情况表明对于可变引用也有着相同的行为。
最后一个情况有些微妙:Rust 也会将可变引用强转为不可变引用。
但是反之是 不可能 的:不可变引用永远也不能强转为可变引用。
因为根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译)。
将一个可变引用转换为不可变引用永远也不会打破借用规则。
将不可变引用转换为可变引用则需要数据只能有一个不可变引用,而借用规则无法保证这一点。
因此,Rust 无法假设将不可变引用转换为可变引用是可能的。

//解引用多态
//自定义Box实现Deref trait允许我们重载解引用运算符
//定义个泛型结构体
struct MyBox<T>(T);//实现new方法
impl<T> MyBox<T> {fn new(x: T) -> MyBox<T> {MyBox(x)}
}//解引用多态与可变性交互
//1.当T:Deref<Target = U>时,从&T可以得到&U
//2.当T:DerefMut<Target = U>时,从&mut T可以得到&mut U
//3.当T:Deref<Target = U>时,从&mut T可以得到&U。反之则不行,没有&T到&mut U的转换//实现Deref trait
//引入标准库的Deref trait
use std::ops::Deref;
impl<T> Deref for MyBox<T> {type Target = T;//实现Deref,只需要实现deref这一个方法//返回值是一个引用,指向MyBox内部的值fn deref(&self) -> &Self::Target {&self.0 //根据字段的个数来返回,我们得MyBox只有一个字段,所以返回&self.0}
}//定义个函数,参数是一个字符串切片
//第一条 T:Deref<Target = U>时,从&T可以得到&U
fn hello(name: &str) {println!("Hello, {}!", name);
}//测试下第3条
//实现DerefMut trait
use std::ops::DerefMut;
impl<T> DerefMut for MyBox<T> {fn deref_mut(&mut self) -> &mut Self::Target {&mut self.0}
}//定义个函数,参数是一个可变字符串切片
fn hello_mut(name: &mut str) {println!("Hello, {}!", name);
}fn main() {//创建个MyBox实例let m = MyBox::new(String::from("Rust"));//调用hello函数,传入MyBox实例的引用//第一步,编译器会调用m的deref方法,返回&String//第二步,编译器会调用String的deref方法,返回&str//这个过程叫做解引用多态hello(&m);//测试下第2条//从&mut T可以得到&mut U//创建个可变的MyBox实例let mut m = MyBox::new(String::from("Rust"));//调用hello函数,传入MyBox实例的可变引用//第一步,编译器会调用m的deref_mut方法,返回&mut String//第二步,编译器会调用String的deref_mut方法,返回&mut str//这个过程叫做解引用多态hello_mut(&mut m);//测试下第3条//从&mut T可以得到&U//创建个可变的MyBox实例let mut m = MyBox::new(String::from("Rust"));//调用hello函数,传入MyBox实例的可变引用//第一步,编译器会调用m的deref_mut方法,返回&mut String//第二步,编译器会调用String的deref方法,返回&str//这个过程叫做解引用多态hello_mut(&mut m);
}

在这里插入图片描述

2.5 drop trait清理代码

2.5.1 实现drop trait

类似于其他语言的析构函数
对于智能指针模式来说另一个重要的 trait 是 Drop 。 Drop 允许我们在值要离开作用域时执行一些代码。
可以为任何类型提供 Drop trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。
我们在智能指针上下文中讨论Drop 是因为其功能几乎总是用于实现智能指针。

#![allow(unused_variables)]
#[allow(dead_code)]
//drop trait
//类似于其他语言的析构函数,当值离开作用域时,自动调用的函数
#[derive(Debug)]
struct Dog {name: String,
}//为Dog实现Drop trait
impl Drop for Dog {//实现Drop trait只需要实现drop方法,Drop trait中只定义了一个drop方法//实现trait的方法,要看下trait中定义的方法是怎么定义的//fn drop(&mut self);//这里为什么使用&mut self,因为drop方法会修改self的值,比如将self的值设置为Nonefn drop(&mut self) {println!("Dropping Dog with name `{}`!", self.name);}
}fn main() {let c = Dog {name: String::from("旺财"),};//手动调用drop方法//c.drop(); //报错,drop方法是私有的,不能手动调用//手动调用drop方法// drop(c);//局部作用域里面的变量在离开作用域时,自动调用drop方法{let c = Dog {name: String::from("大黄"),};}println!("CustomSmartPointer created.");
}

先调用大黄的drop函数,执行main函数中的println!,最后在main函数作用域结束时,执行旺财的drop函数
在这里插入图片描述

2.5.2 drop提前释放

通过 std::mem::drop 提早丢弃值
Rust 当值离开作用域时自动插入 drop 调用,不能直接禁用这个功能。
被打印到屏幕上,它展示了 Rust 在实例离开作用域时自动调用了 drop 。
通常也不需要禁用 drop ;整个 Drop trait 存在的意义在于其是自动处理的。有时可能需要提早清理某个值。
比如当使用智能指针管理锁时;你可能希望强制运行 drop 方法来释放锁以便作用域中的其他代码可以获取锁。

如果我们直接通过对象调用对象.drop()会报错

#![allow(unused_variables)]
#[allow(dead_code)]
//drop trait
//类似于其他语言的析构函数,当值离开作用域时,自动调用的函数
#[derive(Debug)]
struct Dog {name: String,
}//为Dog实现Drop trait
impl Drop for Dog {//实现Drop trait只需要实现drop方法,Drop trait中只定义了一个drop方法//实现trait的方法,要看下trait中定义的方法是怎么定义的//fn drop(&mut self);//这里为什么使用&mut self,因为drop方法会修改self的值,比如将self的值设置为Nonefn drop(&mut self) {println!("Dropping Dog with name `{}`!", self.name);}
}fn main() {let c = Dog {name: String::from("旺财"),};//手动调用drop方法c.drop(); //报错,不能手动调用//手动调用drop方法// drop(c);//局部作用域里面的变量在离开作用域时,自动调用drop方法println!("CustomSmartPointer created.");
}

如果我们直接通过对象c.drop()调用是会报错的
在这里插入图片描述

错误信息表明不允许显式调用 drop 。
错误信息使用了术语 析构函数(destructor),这是一个清理实例的函数的通用编程概念。
析构函数 对应创建实例的 构造函数。Rust 中的 drop 函数就是这么一个析构函数。

正确调用drop方法:
Rust 不允许我们显式调用 drop 因为 Rust 仍然会在 main 的结尾对值自动调用 drop ,这会导致一个 double free 错误,因为 Rust 会尝试清理相同的值两次。
因为不能禁用当值离开作用域时自动插入的 drop ,并且不能显示调用 drop ,
如果我们需要提早清理值,可以使用std::mem::drop 函数。
std::mem::drop 函数不同于 Drop trait 中的 drop 方法。可以通过传递希望提早强制丢弃的值作为参数。

#![allow(unused_variables)]
#[allow(dead_code)]
//drop trait
//类似于其他语言的析构函数,当值离开作用域时,自动调用的函数
#[derive(Debug)]
struct Dog {name: String,
}//为Dog实现Drop trait
impl Drop for Dog {//实现Drop trait只需要实现drop方法,Drop trait中只定义了一个drop方法//实现trait的方法,要看下trait中定义的方法是怎么定义的//fn drop(&mut self);//这里为什么使用&mut self,因为drop方法会修改self的值,比如将self的值设置为Nonefn drop(&mut self) {println!("Dropping Dog with name `{}`!", self.name);}
}fn main() {let c = Dog {name: String::from("旺财"),};//手动调用drop方法// c.drop(); //报错,不能通过对象来调用drop方法,因为不能禁用当值离开作用域时自动调用drop方法//手动调用drop方法//可以通过std::mem::drop函数来调用drop方法drop(c);//局部作用域里面的变量在离开作用域时,自动调用drop方法println!("CustomSmartPointer created.");
}

提早调用drop成功
在这里插入图片描述

使用 Drop trait 实现指定的代码在很多方面都使得清理值变得方便和安全:比如可以使用它来创建我们自己的内存分配器!
通过 Drop trait 和 Rust 所有权系统,就无需担心之后清理代码,因为 Rust 会自动考虑这些问题。
如果代码在值仍被使用时就清理它会出现编译错误,因为所有权系统确保了引用总是有效的,这也就保证了 drop 只会在值不再被使用时被调用一次。

三、Rc<T>:引用计数智能指针

3.1 Rc的基本用法

Rc(Reference Counting)允许数据有多个所有者,通过引用计数管理生命周期。
大部分情况下所有权是非常明确的:可以准确的知道哪个变量拥有某个值。
然而,有些情况单个值可能会有多个所有者。
例如,在图数据结构中,多个边可能指向相同的结点,而这个结点从概念上讲为所有指向它的边所拥有。
结点直到没有任何边指向它之前都不应该被清理。
在 Rust 中,Rc<T>(引用计数智能指针)本身不能直接修改其内部的数据,因为它的设计目的是为了共享所有权而不是可变访问。因为相同位置的多个可变引用,可能会造成数据不一致
默认情况下,Rc<T> 提供的是不可变共享访问。

为了启用多所有权,Rust 有一个叫做 Rc<T> 的类型。
其名称为 引用计数(reference counting)的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。
如果某个值有零个引用,就代表没有任何有效引用并可以被清理。

可以将其想象为客厅中的电视。当一个人进来看电视时,他打开电视。其他人也可以进来看电视。
当最后一个人离开房间时,他关掉电视因为它不再被使用了。
如果某人在其他人还在看的时候就关掉了电视,正在看电视的人肯定会抓狂的!

Rc<T> 用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的那一部分会最后结束使用它的时候。
如果确实知道哪部分会结束使用的话,就可以令其成为数据的所有者同时正常的所有权规则就可以在编译时生效。
注意 Rc<T> 只能用于单线程场景;
需要为 Rc 增加 use 语句因为它不在 prelude 中
Rust默认的标准库包含很多内容,其中prelude是自动导入的部分,是每个Rust程序都包含的标准库部分
可以自动导入的库如下
在这里插入图片描述

//引用计数智能指针Rc
use std::rc::Rc;fn main() {let rc1 = Rc::new(42);//获取rc1的引用计数println!("Reference count: {}", Rc::strong_count(&rc1));//克隆rc1,增加引用计数let rc2 = Rc::clone(&rc1); // 增加引用计数//获取引用计数println!("Reference count: {}", Rc::strong_count(&rc1));println!("Reference count: {}", Rc::strong_count(&rc2));
}

每次调用Rc::clone , Rc 中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理
在这里插入图片描述

示例2:

//引用计数智能指针Rc
use std::rc::Rc;//定义链表
enum List {Cons(i32, Rc<List>),Nil,
}use List::{ Cons, Nil };fn main() {//创建个Rc智能指针,指向list1let list1 = Rc::new(Cons(1, Rc::new(Cons(2, Rc::new(Cons(3, Rc::new(Nil)))))));// 创建个Rc智能指针,指向list1// let rc1 = Rc::clone(&list1);//克隆也可以这样写let rc1 = list1.clone();// 再创建个Rc智能指针,指向rc1let rc2 = Rc::clone(&list1);//打印引用计数println!("rc1 count: {}", Rc::strong_count(&rc1));println!("rc2 count: {}", Rc::strong_count(&rc2));println!("list1 count: {}", Rc::strong_count(&list1));
}

在这里插入图片描述

不过在这里 Rust 的习惯是使用 Rc::clone 。 Rc::clone 的实现并不像大部分类型的 clone 实现那样对所有数据进行深拷贝。
Rc::clone 只会增加引用计数,这并不会花费多少时间。
深拷贝可能会花费很长时间,所以通过使用 Rc::clone 进行引用计数,
可以明显的区别可能会对运行时性能有巨大影响的深拷贝和不分配内存的对运行时性能影响相对较小的增加引用计数拷贝。

Rc<T> 允许通过不可变引用来只读的在程序的多个部分共享数据。

当离开作用域范围,引用计数器会在drop trait的作用下自动减一

//引用计数智能指针Rc
use std::rc::Rc;//定义链表
#[allow(dead_code)]
enum List {Cons(i32, Rc<List>),Nil,
}use List::{ Cons, Nil };fn main() {//创建个Rc智能指针,指向list1let list1 = Rc::new(Cons(1, Rc::new(Cons(2, Rc::new(Cons(3, Rc::new(Nil)))))));// 创建个Rc智能指针,指向list1// let rc1 = Rc::clone(&list1);//克隆也可以这样写let rc1 = list1.clone();// 再创建个Rc智能指针,指向rc1let rc2 = Rc::clone(&list1);//打印引用计数println!("rc1 count: {}", Rc::strong_count(&rc1));println!("rc2 count: {}", Rc::strong_count(&rc2));println!("list1 count: {}", Rc::strong_count(&list1));{let rc3 = Rc::clone(&list1);println!("rc3 count: {}", Rc::strong_count(&rc3));}//离开rc3的作用域,引用计数器减1//Drop trait 的实现当 Rc 值离开作用域时自动减少引用计数。println!("rc1 count: {}", Rc::strong_count(&rc1));println!("rc2 count: {}", Rc::strong_count(&rc2));// println!("rc3 count: {}", Rc::strong_count(&rc3));println!("list1 count: {}", Rc::strong_count(&list1));
}

在这里插入图片描述

3.2 共享所有权场景

Rc常用于需要多个部分共享数据但不确定哪部分最后使用数据的场景。

use std::rc::Rc;#[derive(Debug)]
#[allow(dead_code)]
struct Node {value: i32,edges: Vec<Rc<Node>>,
}fn main() {let node1 = Rc::new(Node {value: 1,edges: Vec::new(),});let node2 = Rc::new(Node {value: 2,edges: vec![Rc::clone(&node1)],});let node3 = Rc::new(Node {value: 3,edges: vec![Rc::clone(&node1), Rc::clone(&node2)],});println!("node1: {:?}", node1);println!("node2: {:?}", node2);println!("node3: {:?}", node3);
}

在这里插入图片描述

3.3 Rc的限制

仅适用于单线程
只有不可变引用(无法修改数据)
循环引用会导致内存泄漏
尝试修改Rc的引用,会报错,因为相同位置的多个可变引用,可能会造成数据不一致。Rust避免这种情况的出现
在这里插入图片描述

四、RefCell<T>:内部可变性模式

4.1 RefCell的基本用法

RefCell提供 运行时检查 的借用规则(通常Rust都是在编译时检查借用规则),允许在不可变引用下修改数据。RefCell代表其数据的唯一所有权
内部可变性(Interior mutability)是 Rust 中的一个设计模式,
它允许你即使在有不可变引用时改变数据,这通常是借用规则所不允许的。
为此,该模式在数据结构中使用 unsafe 代码来模糊 Rust 通常的可变性和借用规则。
当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。
所涉及的 unsafe 代码将被封装进安全的 API 中,而外部类型仍然是不可变的。

不同于 Rc<T>RefCell<T> 代表其数据的唯一的所有权。那么是什么让 RefCell<T> 不同于像 Box<T> 这样的类型呢?
我们回忆一下借用规则:

  1. 在任意给定时间,只能 拥有如下中的一个:
    一个可变引用。
    任意数量的不可变引用。
  2. 引用必须总是有效的。

对于引用和 Box<T> ,借用规则的不可变性作用于编译时。
对于 RefCell<T> ,这些不可变性作用于 运行时。
对于引用,如果违反这些规则,会得到一个编译错误。而对于 RefCell<T> ,违反这些规则会 panic! 。
在编译时检查借用规则的好处是这些错误将在开发过程的早期被捕获同时对没有运行时性能影响,因为所有的分析都提前完成了。
为此,在编译时检查借用规则是大部分情况的最佳选择,这也正是其为何是 Rust 的默认行为。

相反在运行时检查借用规则的好处是特定内存安全的场景是允许的,而它们在编译时检查中是不允许的。
静态分析,正如 Rust 编译器,是天生保守的。
代码的一些属性则不可能通过分析代码发现:其中最著名的就是 停机问题(HaltingProblem)。
因为一些分析是不可能的,如果 Rust 编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。
如果 Rust 接受不正确的程序,那么人们也就不会相信 Rust 所做的保证了。
然而,如果 Rust 拒绝正确的程序,会给程序员带来不便,但不会带来灾难。
RefCell<T> 正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。
类似于 Rc<T>RefCell<T> 只能用于单线程场景。
如果尝试在多线程上下文中使用 RefCell<T> ,会得到一个编译错误。

如下为选择 Box<T>Rc<T>RefCell<T> 的理由:

Rc<T> 允许相同数据有多个所有者,提供只读的所有权共享; Box<T> 和 RefCell<T> 有单一所有者。
Box<T> 允许在编译时执行不可变(或可变)借用检查;
Rc<T> 仅允许在编译时执行不可变借用检查;RefCell<T> 允许在运行时执行不可变(或可变)借用检查。
因为 RefCell<T> 允许在运行时执行可变借用检查,所以我们可以在即便 RefCell<T> 自身是不可变的情况下修改其内部的值。

最后一个理由便是指 内部可变性 模式。让我们看看何时内部可变性是有用的,并讨论这是如何成为可能的。

use std::cell::RefCell;fn main() {let cell = RefCell::new(42);{let mut num = cell.borrow_mut(); // 可变借用*num += 1;} // 借用在这里离开作用域//在作用域中修改的值,可以在作用域外访问println!("cell = {:?}", cell.borrow()); // 不可变借用
}

在这里插入图片描述

4.2 内部可变性:不可变值的可变借用

借用规则的一个推论是当有一个不可变值时,不能可变的借用它。
如果尝试改变其值,编译时会得到错误
cannot borrow immutable local variable x as mutable

然而,特定情况下在值的方法内部能够修改自身是很有用的;而不是在其他代码中,此时值仍然是不可变。
值方法外部的代码不能修改其值。 RefCell<T> 是一个获得内部可变性的方法。
RefCell<T> 并没有完全绕开借用规则,编译器中的借用检查器允许内部可变性并相应的在运行时检查借用规则。
如果违反了这些规则,会得到 panic! 而不是编译错误。

RefCell<T> 在运行时检查借用规则
当创建不可变和可变引用时,我们分别使用 & 和 &mut 语法。
对于 RefCell<T> 来说,则是 borrow 和 borrow_mut 方法,这属于 RefCell<T> 安全 API 的一部分。
borrow 方法返回 Ref 类型的智能指针, borrow_mut 方法返回 RefMut 类型的智能指针。
这两个类型都实现了 Deref 所以可以当作常规引用对待。
RefCell<T> 记录当前有多少个活动的 Ref 和 RefMut 智能指针。
每次调用 borrow , RefCell<T> 将活动的不可变借用计数加一。
当 Ref 值离开作用域时,不可变借用计数减一。
就像编译时借用规则一样, RefCell<T> 在任何时候只允许有多个不可变借用或一个可变借用。

如果我们尝试违反这些规则,相比引用时的编译时错误, RefCell<T> 的实现会在运行时 panic! 。

在运行时捕获借用错误而不是编译时意味着将会在开发过程的后期才会发现错误 ———— 甚至有可能发布到生产环境才发现。
还会因为在运行时而不是编译时记录借用而导致少量的运行时性能惩罚。
然而,使用 RefCell 使得在只允许不可变值的上下文中编写修改自身以记录消息的 mock 对象成为可能。
虽然有取舍,但是我们可以选择使用 RefCell<T> 来获得比常规引用所能提供的更多的功能。

结合 Rc<T> 和 RefCell<T> 来拥有多个可变数据所有者
RefCell<T> 的一个常见用法是与 Rc<T> 结合。
回忆一下 Rc<T> 允许对相同数据有多个所有者,不过只能提供数据的不可变访问。
如果有一个储存了 RefCell<T> 的 Rc<T> 的话,就可以得到有多个所有者 并且 可以修改的值了!
可变数据的前提是Rc<T>里面套RefCell<T>

使用 Rc<RefCell<i32>> 创建可以修改的 List

//RefCell<T>
use std::cell::RefCell;//引用计数智能指针Rc
use std::rc::Rc;//定义链表
#[allow(dead_code)]
#[derive(Debug)]
enum List {Cons(Rc<RefCell<i32>>, Rc<List>),Nil,
}use List::{ Cons, Nil };fn main() {let value = Rc::new(RefCell::new(5));let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));let c = Cons(Rc::new(RefCell::new(7)), Rc::clone(&a));println!("a before = {:?}", a);println!("b before = {:?}", b);println!("c before = {:?}", c);//我们可以修改内部的值,解引用//修改value内部的值*value.borrow_mut() += 10;println!("a after = {:?}", a);println!("b after = {:?}", b);println!("c after = {:?}", c);
}

在这里插入图片描述

这里创建了一个 Rc<RefCell<i32> 实例并储存在变量 value 中以便之后直接访问。
接着在 a 中用包含 value 的 Cons 成员创建了一个 List 。
需要克隆 value 以便 a 和 value 都能拥有其内部值 5 的所有权,而不是将所有权从 value 移动到 a 或者让 a 借用 value 。
我们将列表 a 封装进了 Rc<T> 这样当创建列表 b 和 c 时,他们都可以引用 a 。
一旦创建了列表 a 、 b 和 c ,我们将 value 的值加 10。
为此对 value 调用了 borrow_mut ,这里使用了解引用功能(“ -> 运算符到哪去了?”)来解引用 Rc<T> 以获取其内部的 RefCell<T> 值。
borrow_mut 方法返回RefMut<T> 智能指针,可以对其使用解引用运算符并修改其内部值。

这是非常巧妙的!通过使用 RefCell<T> ,我们可以拥有一个表面上不可变的 List ,不过可以使用 RefCell<T> 中提供内部可变性的方法来在需要时修改数据。
RefCell<T> 的 运行时借用规则检查 也确实保护我们免于出现数据竞争,而且我们也决定牺牲一些速度来换取数据结构的灵活性。
标准库中也有其他提供内部可变性的类型,比如 Cell<T> ,它有些类似( RefCell<T> )除了相比提供内部值的引用,其值被拷贝进和拷贝出 Cell<T>

案例2:

use std::rc::Rc;
use std::cell::RefCell;#[derive(Debug)]
#[allow(dead_code)]
struct Node {value: i32,edges: Vec<Rc<RefCell<Node>>>,
}fn main() {let node1 = Rc::new(RefCell::new(Node {value: 1,edges: Vec::new(),}));let node2 = Rc::new(RefCell::new(Node {value: 2,edges: vec![Rc::clone(&node1)],}));// 修改node1的值node1.borrow_mut().value = 10;println!("node1: {:?}", node1.borrow());println!("node2: {:?}", node2.borrow());
}

在这里插入图片描述

4.3 RefCell的运行时检查

RefCell在运行时检查借用规则,违反规则会导致panic:
编译通过,运行时导致panic

use std::cell::RefCell;fn main() {let cell = RefCell::new(42);let _borrow1 = cell.borrow(); // 不可变借用let _borrow2 = cell.borrow_mut(); // 这里会panic:已经有一个不可变借用
}

在这里插入图片描述

五、Arc<T>:线程安全的引用计数

5.1 Arc的基本用法

Arc(Atomic Reference Counting)是Rc的线程安全版本,使用原子操作管理引用计数。

use std::sync::Arc;
use std::thread;fn main() {let arc = Arc::new(42);for _ in 0..10 {let arc_clone = Arc::clone(&arc);thread::spawn(move || {println!("Value in thread: {}", arc_clone);});}thread::sleep(std::time::Duration::from_secs(1));
}

在这里插入图片描述

5.2 Arc与Mutex结合使用

Arc通常与Mutex或RwLock结合使用,以实现线程间共享可变数据。

use std::sync::{ Arc, Mutex };
use std::thread;fn main() {let counter = Arc::new(Mutex::new(0));let mut handles = vec![];for _ in 0..10 {let counter = Arc::clone(&counter);let handle = thread::spawn(move || {let mut num = counter.lock().unwrap();*num += 1;});handles.push(handle);}for handle in handles {handle.join().unwrap();}println!("Result: {}", *counter.lock().unwrap());
}

在这里插入图片描述

六、智能指针的高级用法

6.1 自定义智能指针

我们可以通过实现Deref和Drop trait来创建自定义智能指针。

use std::ops::Deref;
use std::fmt;struct MyBox<T>(T);impl<T> MyBox<T> {fn new(x: T) -> MyBox<T> {MyBox(x)}
}impl<T> Deref for MyBox<T> {type Target = T;fn deref(&self) -> &T {&self.0}
}impl<T> Drop for MyBox<T> {fn drop(&mut self) {println!("Dropping MyBox!");}
}impl<T: fmt::Display> fmt::Display for MyBox<T> {fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {write!(f, "MyBox({})", self.0)}
}fn main() {let x = MyBox::new(42);println!("{}", *x); // 通过Deref解引用println!("{}", x); // 使用Display trait
} // 这里调用drop

在这里插入图片描述

6.2 引用循环与内存泄漏

Rust 的内存安全保证使其 难以 意外的制造永远也不会被清理的内存(被称为 内存泄露(memory leak)),但并不是不可能。
完全的避免内存泄露并不是同在编译时拒绝数据竞争一样为 Rust 的保证之一,这意味着内存泄露在 Rust 被认为是内存安全的。
这一点可以通过 Rc 和 RefCell 看出:有可能会创建个个项之间相互引用的引用。
这会造成内存泄露,因为每一项的引用计数将永远也到不了 0,其值也永远也不会被丢弃。

制造引用循环
让我们看看引用循环是如何发生的以及如何避免它。以下示例中的 List 枚举和 tail 方法的定义开始:

//引用循环
use std::rc::Rc;
use std::cell::RefCell;
use List::{ Cons, Nil };#[derive(Debug)]
#[allow(dead_code)]
enum List {Cons(i32, RefCell<Rc<List>>),Nil,
}//为List实现tail方法
impl List {//有可能是为空的,所以返回值是Optionfn tail(&self) -> Option<&RefCell<Rc<List>>> {match *self {Cons(_, ref item) => Some(item),Nil => None,}}
}
fn main() {//定义个链表alet a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));//查看a引用计数println!("a initial rc count = {}", Rc::strong_count(&a));//查看a的下一个节点println!("a next item = {:?}", a.tail());//定义个链表b,b的下一个节点是alet b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));//查看a,b引用计数println!("a rc count after b creation = {}", Rc::strong_count(&a));println!("b initial rc count = {}", Rc::strong_count(&b));//查看b的下一个节点println!("b next item = {:?}", b.tail());//修改a的下一个节点为bif let Some(link) = a.tail() {//通过borrow_mut获取可变引用,并修改a的下一个节点为b*link.borrow_mut() = Rc::clone(&b);}println!("b rc count after changing a = {}", Rc::strong_count(&b));println!("a rc count after changing a = {}", Rc::strong_count(&a));//查看a的下一个节点// println!("a next item = {:?}", a.tail());  //死循环,会造成栈溢出
}

在这里插入图片描述

这里在变量 a 中创建了一个 Rc 实例来存放初值为 5, Nil 的 List 值。接着在变量 b 中创建了存放包含值 10 和指向列表 a 的 List 的另一个 Rc 实例。
最后,修改 a 使其指向 b 而不是 Nil ,这就创建了一个循环。
为此需要使用 tail 方法获取 a 中 RefCell 的引用,并放入变量 link 中。
接着使用 RefCell 的 borrow_mut 方法将其值从存放 Nil 的 Rc 修改为 b 中的 Rc 。

可以看到将 a 修改为指向 b 之后, a 和 b 中都有的 Rc 实例的引用计数为 2。
在 main 的结尾,Rust 会尝试首先丢弃 b ,这会使 a 和 b 中 Rc 实例的引用计数减一。
然而,因为 a 仍然引用 b 中的 Rc , Rc 的引用计数是 1 而不是 0,所以 Rc 在堆上的内存不会被丢弃。
其内存会因为引用计数为 1 而永远停留。

此时,如果我们查看a.tail()或b.tail()都会出现死循环,造成栈溢出
因为a,b是首尾相连的。持续交替打印
在这里插入图片描述

循环引用如图所示
在这里插入图片描述

这个特定的例子中,创建了引用循环之后程序立刻就结束了。这个循环的结果并不可怕。
如果在更为复杂的程序中并在循环里分配了很多内存并占有很长时间,这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。
创建引用循环并不容易,但也不是不可能。
如果你有包含 Rc 的 RefCell 值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;你无法指望 Rust 帮你捕获它们。
创建引用循环是一个程序上的逻辑 bug,你应该使用自动化测试、代码评审和其他软件开发最佳实践来使其最小化。
另一个解决方案是重新组织数据结构使得一些引用有所有权而另一些则没有。
如此,循环将由一些有所有权的关系和一些没有所有权的关系,而只有所有权关系才影响值是否被丢弃。
在这个案例 中,我们总是希望 Cons 成员拥有其列表,所以重新组织数据结构是不可能的。

6.3 弱引用(Weak<T>

Rc和Arc都支持弱引用Weak,它不会增加引用计数,用于解决循环引用问题。
避免引用循环:将 Rc 变为 Weak

到目前为止,我们已经展示了调用 Rc::clone 会增加 Rc 实例的 strong_count ,和 Rc 实例只在其 strong_count 为 0 时才会被清理。
也可以通过调用 Rc::downgrade 并传递 Rc 实例的引用来创建其值的 弱引用(weak reference)。
调用Rc::downgrade 时会得到 Weak 类型的智能指针。
不同于将 Rc 实例的 strong_count 加一,调用 Rc::downgrade 会将weak_count 加一。
Rc 类型使用 weak_count 来记录其存在多少个 Weak 引用,类似于 strong_count 。
其区别在于weak_count 无需计数为 0 就能使 Rc 实例被清理,只需要strong_count为0就可以了。
强引用代表如何共享 Rc 实例的引用。弱引用并不代表所有权关系。
他们不会造成引用循环,因为任何引入了弱引用的循环一旦所涉及的强引用计数为 0 就会被打破。

因为 Weak 引用的值可能已经被丢弃了,为了使用 Weak 所指向的值,我们必须确保其值仍然有效。
为此可以调用Weak 实例的 upgrade 方法,这会返回 Option<Rc> 。
如果 Rc 值还未被丢弃则结果是 Some ,如果 Rc 已经被丢弃则结果是 None 。
因为 upgrade 返回一个 Option ,我们确信 Rust 会处理 Some 和 None 的情况,并且不会有一个无效的指针。

//弱引用
use std::rc::{ Weak, Rc };
use std::cell::RefCell;
use List::{ Cons, Nil };#[derive(Debug)]
#[allow(dead_code)]
//将List定义成Weak
enum List {Cons(i32, RefCell<Weak<List>>),Nil,
}//为List实现tail方法
impl List {//有可能是为空的,所以返回值是Optionfn tail(&self) -> Option<&RefCell<Weak<List>>> {match self {Cons(_, item) => Some(item),Nil => None,}}
}
fn main() {//定义个链表alet a = Rc::new(Cons(5, RefCell::new(Weak::new())));//查看a引用计数println!("a initial rc count = {}", Rc::strong_count(&a));//查看a的下一个节点println!("a next item = {:?}", a.tail());//定义个链表blet b = Rc::new(Cons(10, RefCell::new(Weak::new())));//查看a,b引用计数println!("a rc count after b creation = {}", Rc::strong_count(&a));println!("b initial rc count = {}", Rc::strong_count(&b));//修改下一个节点之前,查看a,b弱引用计数println!("b weak count after b creation = {}", Rc::weak_count(&b));println!("a weak count after b creation = {}", Rc::weak_count(&a));//查看b的下一个节点println!("b next item = {:?}", b.tail());//修改b的下一个节点为aif let Some(link) = b.tail() {//通过borrow_mut获取可变引用,并修改b的下一个节点为a*link.borrow_mut() = Rc::downgrade(&a);}//修改a的下一个节点为bif let Some(link) = a.tail() {//通过borrow_mut获取可变引用,并修改a的下一个节点为b*link.borrow_mut() = Rc::downgrade(&b);}println!("b rc count after changing a = {}", Rc::strong_count(&b));println!("a rc count after changing a = {}", Rc::strong_count(&a));//修改下一个节点之后,查看a,b弱引用计数println!("b weak count after changing a = {}", Rc::weak_count(&b));println!("a weak count after changing a = {}", Rc::weak_count(&a));//查看a的下一个节点,是weak,所以不会造成引用循环println!("a next item = {:?}", a.tail());
}

在这里插入图片描述

6.4 创建树形数据结构:带有子结点的 Node

use std::rc::{ Rc, Weak };
use std::cell::RefCell;//树形结构
#[derive(Debug)]
#[allow(dead_code)]
struct Node {value: i32,parent: RefCell<Weak<Node>>,children: RefCell<Vec<Rc<Node>>>,
}fn main() {let leaf = Rc::new(Node {value: 3,parent: RefCell::new(Weak::new()), //我们目前不知道leaf的父节点是谁,所以先设置为Weak::new()children: RefCell::new(Vec::new()), //leaf目前没有子节点});//查看leaf的父节点,upgrade方法返回Option<Rc<Node>>println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());//定义个branch节点,branch的子节点是leaflet branch = Rc::new(Node {value: 5,parent: RefCell::new(Weak::new()),children: RefCell::new(vec![Rc::clone(&leaf)]),});//修改leaf的父节点为branch*leaf.parent.borrow_mut() = Rc::downgrade(&branch);println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

在这里插入图片描述

查看强引用和弱引用计数

use std::rc::{ Rc, Weak };
use std::cell::RefCell;//树形结构
#[derive(Debug)]
#[allow(dead_code)]
struct Node {value: i32,parent: RefCell<Weak<Node>>,children: RefCell<Vec<Rc<Node>>>,
}fn main() {let leaf = Rc::new(Node {value: 3,parent: RefCell::new(Weak::new()), //我们目前不知道leaf的父节点是谁,所以先设置为Weak::new()children: RefCell::new(Vec::new()), //leaf目前没有子节点});//查看leaf的父节点,upgrade方法返回Option<Rc<Node>>println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());//查看leaf的引用计数println!("leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf));//定义个branch节点,branch的子节点是leaflet branch = Rc::new(Node {value: 5,parent: RefCell::new(Weak::new()),children: RefCell::new(vec![Rc::clone(&leaf)]),});//查看branch的引用计数println!("branch strong = {}, weak = {}", Rc::strong_count(&branch), Rc::weak_count(&branch));//修改leaf的父节点为branch*leaf.parent.borrow_mut() = Rc::downgrade(&branch);println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());//查看leaf,branch的引用计数println!("leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf));println!("branch strong = {}, weak = {}", Rc::strong_count(&branch), Rc::weak_count(&branch));
}

在这里插入图片描述

七、智能指针的性能考量

7.1 各种智能指针的性能特点

Box<T>
分配/释放堆内存的开销
访问数据需要一次解引用

Rc<T>/Arc<T>
额外的引用计数存储
Arc使用原子操作,比Rc更慢
克隆Rc/Arc比克隆数据本身更快

RefCell<T>
运行时借用检查开销
内部使用unsafe代码,但对外提供安全接口

7.2 何时使用何种智能指针

在这里插入图片描述

八、智能指针组合使用

常见组合模式:
Rc<RefCell<T>> - 单线程多所有权可变数据

use std::rc::Rc;
use std::cell::RefCell;let shared_vec = Rc::new(RefCell::new(vec![1, 2, 3]));
let clone1 = Rc::clone(&shared_vec);
clone1.borrow_mut().push(4);

Arc<Mutex<T>> - 多线程多所有权可变数据

use std::sync::{Arc, Mutex};
use std::thread;let data = Arc::new(Mutex::new(0));
let mut handles = vec![];for _ in 0..10 {let data = Arc::clone(&data);let handle = thread::spawn(move || {let mut num = data.lock().unwrap();*num += 1;});handles.push(handle);
}

九、常见问题与解决方案

9.1 循环引用导致内存泄漏

问题:使用Rc和RefCell可能导致循环引用,从而造成内存泄漏。
解决方案:
使用Weak代替Rc作为父节点的引用
重新设计数据结构避免循环
使用Rc::downgrade创建弱引用

9.2 线程间共享数据的选择

问题:在多线程环境中如何选择合适的智能指针组合?
解决方案:
只读共享数据:Arc<T>(当T是Sync时)
可写共享数据:Arc<Mutex<T>>或Arc<RwLock<T>>
避免过度使用锁,考虑消息传递(如std::sync::mpsc)

9.3 性能优化技巧

避免不必要的Rc/Arc克隆
减少锁的持有时间
对于频繁读取、少量写入的场景,优先考虑RwLock而非Mutex
考虑使用Cow(Copy on Write)智能指针避免不必要的复制

十、总结

Rust的智能指针系统提供了强大而灵活的内存管理工具,每种智能指针都有其特定的用途和适用场景:
Box<T>:最简单的堆分配,用于已知大小的数据
Rc<T>/Arc<T>:共享所有权,分别用于单线程和多线程
RefCell<T>/Mutex<T>/RwLock<T>:内部可变性模式
Weak<T>:解决循环引用问题

通过合理组合这些智能指针,可以在保证内存安全的同时实现复杂的数据结构和并发模式。理解每种智能指针的语义和性能特点是编写高效、安全Rust代码的关键。
在实际开发中,建议:

优先考虑所有权和借用,只在必要时使用智能指针
从简单开始(如Box),根据需要逐步升级到更复杂的智能指针
多线程环境下始终使用线程安全的类型(Arc+Mutex/RwLock)
注意性能影响,特别是在热点代码路径中
通过本文的学习和实践,相信大家应该能够熟练运用Rust的各种智能指针来解决实际开发中的内存管理和数据共享问题。

相关文章:

【Rust智能指针】Rust智能指针原理剖析与应用指导

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…...

C++ - 仿 RabbitMQ 实现消息队列(3)(详解使用muduo库)

C - 仿 RabbitMQ 实现消息队列&#xff08;3&#xff09;&#xff08;详解使用muduo库&#xff09; muduo库的基层原理核心概念总结&#xff1a;通俗例子&#xff1a;餐厅模型优势体现典型场景 muduo库中的主要类EventloopMuduo 的 EventLoop 核心解析1. 核心机制&#xff1a;事…...

Java异常处理全解析:从基础到自定义

目录 &#x1f680;前言&#x1f914;异常的定义与分类&#x1f4af;运行时异常&#x1f4af;编译时异常&#x1f4af;异常的基本处理 &#x1f31f;异常的作用&#x1f427;自定义异常&#x1f4af;自定义运行时异常&#x1f4af;自定义编译时异常 ✍️异常的处理方案&#x1…...

C++初阶-vector的模拟实现2

目录 1.vector已经实现的代码总结 2.vector::resize的模拟实现 3.vector::vector(const vector& v)拷贝构造函数的模拟实现 4.vector::operator(const vector& x)的模拟实现&#xff08;原始写法&#xff09; 5.vector::swap的模拟实现 6.vector::operator(const …...

【图数据库】--Neo4j 安装

目录 1.Neo4j --概述 2.JDK安装 3.Neo4j--下载 3.1.下载资源包 3.2.创建环境变量 3.3.运行 Neo4j 是目前最流行的图形数据库(Graph Database)&#xff0c;它以节点(Node)、关系(Relationship)和属性(Property)的形式存储数据&#xff0c;专门为处理高度连接的数据而设计。…...

elementui初学1

当然可以&#xff01;下面是从零开始创建一个最简单的 Element UI 程序的完整流程&#xff0c;基于 Vue 2 Element UI&#xff08;如果你想用 Vue 3&#xff0c;请告诉我&#xff0c;我可以给你 Element Plus 的版本&#xff09;。 ✅ 一、准备环境 确保你已经安装了&#xf…...

lanqiaoOJ 4185:费马小定理求逆元

【题目来源】 https://www.lanqiao.cn/problems/4185/learning/ 【题目描述】 给出 n&#xff0c;p&#xff0c;求 。其中&#xff0c; 指存在某个整数 0≤a<p&#xff0c;使得 na mod p1&#xff0c;此时称 a 为 n 的逆元&#xff0c;即 。数据保证 p 是质数且 n mod p≠0…...

计算机视觉与深度学习 | Python实现CEEMDAN-ISOS-VMD-GRU-ARIMA时间序列预测(完整源码和数据)

以下是结合CEEMDAN、ISOS-VMD、GRU和ARIMA的时间序列预测的Python完整实现方案。本方案包含完整的代码、数据生成逻辑和实现细节说明。 完整代码实现 import numpy as np import pandas as pd from PyEMD import CEEMDAN from vmdpy import VMD from scipy.optimize import di…...

前端开发遇到 Bug,怎么办?如何利用 AI 高效解决问题

前端开发遇到 Bug&#xff0c;怎么办&#xff1f;如何利用 AI 高效解决问题 作为前端开发者&#xff0c;遇到 Bug 几乎是日常。无论是样式错乱、功能异常&#xff0c;还是接口数据不对&#xff0c;Bug 总能让人头疼。但随着人工智能&#xff08;AI&#xff09;技术的发展&…...

博主总结框架

1.博主总结框架 1.1 计算机基础类&#xff08;数据结构、计算机网络、操作系统等&#xff09; &#xff08;1&#xff09;数据结构 &#xff08;2&#xff09;操作系统 &#xff08;3&#xff09;计算机网络 &#xff08;4&#xff09;其他 物联网入门框架 1.2 计算机图形…...

国产化Excel处理组件Spire.XLS for .NET系列教程:通过 C# 将 TXT 文本转换为 Excel 表格

在数据处理和管理场景中&#xff0c;将原始文本文件&#xff08;TXT&#xff09;高效转换为结构化的 Excel 电子表格是一项常见要求。对于那些需要自动生成报表或者处理日志文件的开发人员而言&#xff0c;借助 C# 实现 TXT 到 Excel 的转换工作&#xff0c;可以简化数据组织和…...

网络安全--PHP第一天

目标 熟悉信息传递架构 基于phpstydy-mysql-php 前置条件 需要先在数据库中创建相应的库和表名并配置表的结构 该文件为数据库配置文件 名字为config.php <?php $dbip localhost;//连接数据库的地址 远程连接需要输入ip等 $dbuser root;//连接数据库的用户 $dbpass ro…...

结构型:组合模式

目录 1、核心思想 2、实现方式 2.1 模式结构 2.2 实现案例 3、优缺点分析 4、适用场景 1、核心思想 目的&#xff1a;将总是在重复、迭代地显示的某种自相似性的结构&#xff08;部分与整体结构特征相似&#xff09;&#xff0c;例如树形结构&#xff0c;以统一的方式处…...

Node.js多版本安装工具NVM详细使用教程

一、nvm 简介 nvm&#xff08;Node Version Manager&#xff09;是一个用于管理多个 Node.js 版本的命令行工具&#xff0c;允许开发者在单个系统中轻松切换、安装和卸载不同版本的 Node.js。它是前端和后端开发中处理 Node.js 版本兼容性问题的核心工具之一。 二、nvm 安装 …...

深度解析 Java 中介者模式:重构复杂交互场景的优雅方案

一、中介者模式的核心思想与设计哲学 在软件开发的历史长河中&#xff0c;对象间的交互管理一直是架构设计的核心难题。当多个对象形成复杂的网状交互时&#xff0c;系统会陷入 "牵一发而动全身" 的困境。中介者模式&#xff08;Mediator Pattern&#xff09;作为行…...

(八)深度学习---计算机视觉基础

分类问题回归问题聚类问题各种复杂问题决策树√线性回归√K-means√神经网络√逻辑回归√岭回归密度聚类深度学习√集成学习√Lasso回归谱聚类条件随机场贝叶斯层次聚类隐马尔可夫模型支持向量机高斯混合聚类LDA主题模型 一.图像数字化表示及建模基础 二.卷积神经网络CNN基本原…...

深入剖析原型模式:原理、实现与应用实践

在软件开发的世界里,设计模式如同建筑师手中的蓝图,为复杂系统的构建提供了行之有效的解决方案。其中,原型模式(Prototype Pattern)作为创建型设计模式的重要一员,以其独特的对象创建方式,在提高代码复用性、增强系统灵活性等方面发挥着关键作用。本文将深入剖析原型模式…...

【论文阅读 | CVPR 2024 |RSDet:去除再选择:一种用于 RGB - 红外目标检测的由粗到精融合视角】

论文阅读 | CVPR 2024 |RSDet:去除再选择&#xff1a;一种用于 RGB - 红外目标检测的由粗到精融合视角 1.摘要&&引言2. 方法2.1 “由粗到细”融合策略2.2 冗余光谱去除模块&#xff08;RSR&#xff09;2.3 动态特征选择模块&#xff08;DFS&#xff09;2.4 去除与选择检…...

WinForms 应用中集成 OpenCvSharp 实现基础图像处理

引言 欢迎关注dotnet研习社&#xff0c;今天我们要讨论的主题是WinForms 应用中集成 OpenCvSharp 实现基础图像处理。 在常规的图像处理软件开发中&#xff0c;图像处理功能是这些应用程序的核心组成部分。无论是简单的照片编辑工具&#xff0c;还是复杂的计算机视觉应用&…...

apache http client连接池实现原理

在java开发中我们经常会涉及到http 请求接口&#xff0c;一般有几种方式&#xff1a; java自带的 HttpURLConnectionokHttpClientapache http client 一般我们使用apache http client会比较多点&#xff0c;在代码中会进行如下调用方式&#xff1a; private static class Htt…...

adb抓包

目录 抓包步骤 步骤 1: 获取应用的包名 步骤 2: 查看单个应用的日志 步骤 3: 使用日志级别过滤器 步骤 4: 高级日志过滤 可能的原因&#xff1a; 解决方案&#xff1a; 额外提示&#xff1a; 日志保存 抓包步骤 连接设备 adb devices 步骤 1: 获取应用的包名 首先…...

C语言---结构体 、联合体、枚举

一、初识结构体 1、结构体类型 结构体和数组都是集合&#xff0c;但是结构体有成员&#xff0c;类型可以不同&#xff1b;数组有成员&#xff0c;类型相同。 int main() {struct tag{member--list //一个或者多个成员&#xff0c;成员变量}variable--list;//可以省略&#x…...

Web Workers 使用指南

文章目录 前言基础使用高级特性 使用 ES Modules实际应用场景图像处理大数据处理轮询任务 性能优化技巧现代开发方式使用 worker-loader (Webpack) Vite中的Worker使用 限制与注意事项DOM限制:通信限制:同源策略:最佳实践 前言 Web Workers 是浏览器提供的 JavaScript 多线程解…...

JVM 与容器化部署调优实践(Docker + K8s)

&#x1f4cc; 文章目录 &#x1f4d8; 前言1️⃣ 容器环境下 JVM 面临的新挑战2️⃣ JVM 的容器资源感知机制详解3️⃣ JVM 内存调优&#xff1a;如何正确使用堆内存4️⃣ JVM CPU 调优&#xff1a;GC 与编译线程控制5️⃣ Kubernetes 典型配置误区与对策6️⃣ 实战案例&#…...

Android OkHttp控制链:深入理解网络请求的流程管理

OkHttp作为Android和Java平台上广泛使用的HTTP客户端&#xff0c;其核心设计之一就是"控制链"(Chain)机制。本文将深入探讨OkHttp控制链的工作原理、实现细节以及如何利用这一机制进行高级定制。 一、什么是OkHttp控制链 OkHttp控制链是一种责任链模式的实现&#…...

《易经》的数学表达:初级版和高级版

《易经》的数学表达&#xff0c; 一、初级版&#xff0c;可基于以下框架构建&#xff0c; 涵盖符号系统、结构代数及变换规则&#xff1a; 此框架将《易经》抽象为离散数学结构&#xff0c;兼容符号逻辑、概率论与群论&#xff0c;为算法化占断、卦象拓扑分析及跨文化比较提供…...

卷积神经网络基础(十)

之前我们学习了SGD、Momentum和AdaGrad三种优化方法&#xff0c;今天我们将继续学习Adam方法。 6.1.6 Adam 我们知道Momentum参照的是小球在碗中滚动的物理规则进行移动而实现的&#xff0c;AdaGrad为参数的每个元素适当地调整更新步伐。那如果我们将这两种方法融合在一起会不…...

怎么把cursor(Cursor/ollama)安装到指定路径

使用PowerShell命令 打开电脑开始菜单&#xff0c;输入powerShell&#xff0c;使用管理员权限打开powerShell窗口&#xff0c;使用cd命令到cursor或ollama安装包的下载目录&#xff0c;如我的Cursor所在的目录为D:\environment\cursor\soft&#xff0c;输入以下 cd E:\downloa…...

第21天-pyttsx3语音播放功能

示例1:语音参数控制(语速/音量/音调) import pyttsx3def speech_demo():engine = pyttsx3.init()# 获取当前语音参数print("默认语速:", engine.getProperty(rate))print("默认音量:", engine.getProperty(volume))print("可用语音:", engin…...

Multi-Query Attention:传统自注意力( Self-Attention)优化显存和加速方案

本文导读&#xff1a;Multi-Query Attention&#xff08;MQA&#xff09;是 Google Research 2022 年提出的一项轻量化注意力技术&#xff0c;通过“多查询、单键值”的设计&#xff0c;把自注意力层的 KV 缓存从 O(hnd) 降到 O(nd)&#xff0c;在不牺牲模型精度的前提下大幅节…...

学习路之uniapp--unipush2.0推送功能--服务端推送消息

学习路之uniapp--unipush2.0推送功能--服务端推送消息 一、二、三、 一、 二、 三、...

如何使用AI搭建WordPress网站

人工智能正迅速成为包括网页设计在内的许多行业在其功能设置中添加的一种工具。在数字设计和营销领域&#xff0c;许多成熟的工具都在其产品中添加了人工智能功能。WordPress 也是如此。作为目前最流行的网站建设工具之一&#xff0c;WordPress 的人工智能插件越来越多也就不足…...

Java 项目管理工具:Maven 与 Gradle 的深度对比与选择

Java 项目管理工具&#xff1a;Maven 与 Gradle 的深度对比与选择 在 Java 开发领域&#xff0c;项目管理工具对于项目的构建、依赖管理等起着至关重要的作用。Maven 和 Gradle 是目前最主流的两款工具&#xff0c;它们各自有着独特的优势和适用场景。本文将对 Maven 与 Gradl…...

Elasticsearch简单集成java框架方式。

Elasticsearch 在 Java 中最常用的客户端是什么&#xff1f;如何初始化一个 RestHighLevelClient&#xff1f;如何用 Spring Boot 快速集成 Elasticsearch&#xff1f;Spring Data Elasticsearch 如何定义实体类与索引的映射&#xff1f; 最常用的 Java 客户端 目前官方推荐使用…...

50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | Hidden Search Widget (交互式搜索框)

&#x1f4c5; 我们继续 50 个小项目挑战&#xff01;—— Hidden Search Widget 组件 仓库地址&#xff1a;https://github.com/SunACong/50-vue-projects 项目预览地址&#xff1a;https://50-vue-projects.vercel.app/ ✨ 组件目标 点击按钮展开隐藏的搜索框 再次点击按钮…...

python爬虫和逆向:百度翻译数据采集的几种方式

更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、官方API方式(推荐)1.1 百度翻译开放平台API二、网页版逆向方式(代码可直接运行)2.1 拿到js加密方法2.2 python解密代码三、浏览器自动化方式3.1 Selenium自动化操作3.2 Playwright自动化四、移动端API逆向4.1 分…...

spring5-配外部文件-spEL-工厂bean-FactoryBean

spring配外部文件 我们先在Spring里配置一个数据源 1.导c3p0包,这里我们先学一下hibernate持久化框架&#xff0c;以后用mybites. <dependency><groupId>org.hibernate</groupId><artifactId>hibernate-core</artifactId><version>5.2.…...

Ubuntu部署私有Gitlab

这个东西安装其实挺简单的&#xff0c;但是因为我这边迁移了数据目录和使用自己安装的 nginx 代理还是踩了几个坑&#xff0c;所以大家可以注意下 先看下安装 # 先安装必要组件 sudo apt update sudo apt install -y curl openssh-server ca-certificates tzdata perl# 添加gi…...

Activiti 7建表语句及注释

Activiti数据库表Oracle兼容DM建表语句及字段注释。 附件下载版地址点这里 --通用属性表 create table ACT_GE_PROPERTY (NAME_ NVARCHAR2(64),VALUE_ NVARCHAR2(300),REV_ INTEGER,primary key (NAME_) );COMMENT ON TABLE ACT_GE_PROPERTY IS 通用属性表;COMMENT ON COLUMN …...

React中使用 Ant Design Charts 图表

// 引入 Ant Design Charts 的柱状图组件 Column import { Column } from ant-design/charts;// 定义函数组件 App&#xff0c;用于展示柱状图 function App() {// 数据源&#xff1a;每个对象代表一个柱子&#xff0c;包含类型&#xff08;type&#xff09;和销售额&#xff0…...

佰力博科技与您探讨压电材料的原理与压电效应的应用

压电材料的原理基于正压电效应和逆压电效应&#xff0c;即机械能与电能之间的双向转换特性。 压电材料的原理源于其独特的晶体结构和电-机械耦合效应&#xff0c;具体可分为以下核心要点&#xff1a; 1. ‌正压电效应与逆压电效应的定义‌ ‌正压电效应‌&#xff1a;当压电…...

vscode打开vue + element项目

好嘞&#xff0c;我帮你详细整理一个用 VS Code 来可视化开发 Vue Element UI 的完整步骤&#xff0c;让你能舒服地写代码、预览界面、调试和管理项目。 用 VS Code 可视化开发 Vue Element UI 全流程指南 一、准备工作 安装 VS Code 官网下载安装&#xff1a;https://code…...

一步步解析 HTTPS

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;…...

使用vue2做一个生成二维码的案例【可当组件使用】

最近有个需求需要用前端来生成一个二维码&#xff0c;就封装了一个简单的组件&#xff0c;这篇文章来分享给大家。 使用的技术&#xff1a; Vue2 Ant Design Vue QRCodeJS2 node版本&#xff1a;16.20 组件样式&#xff1a; 大家可以根据自己的需求来调整代码。 依赖安装&am…...

Hadoop-HA高可用集群启动nameNode莫名挂掉,排错解决

Hadoop-HA高可用集群启动nameNode莫名挂掉&#xff0c;排错解决 nameNode错误日志 2025-05-21 16:14:12,218 INFO org.apache.hadoop.ipc.Client: Retrying connect to server: node01/192.168.191.111:8485. Already tried 5 time(s); retry policy is RetryUpToMaximumCoun…...

从零基础到最佳实践:Vue.js 系列(2/10):《模板语法与数据绑定》

Vue.js 模板语法与数据绑定&#xff1a;从基础到实践 关键点 Vue.js 的模板语法使用 HTML 结合特殊指令&#xff08;如 v-bind、v-on&#xff09;&#xff0c;实现动态界面。插值&#xff08;{{ }}&#xff09;显示数据&#xff0c;指令控制 DOM 行为&#xff0c;双向绑定简化…...

第二章:Android常用UI控件

1、介绍&#xff1a; 控件是界面组成的主要元素&#xff0c;界面中的控件有序排放和完美组合&#xff0c;便可在用户眼前呈现出丰富多彩的页面。 2、常用控件&#xff1a; 一、TextView控件&#xff1a; 在使用手机时&#xff0c;经常会看见一些文本信息&#xff0c;这些文本…...

LeetCode 1004. 最大连续1的个数 III

LeetCode 1004题 “最大连续1的个数 III” 是一道关于数组和滑动窗口的问题。题目描述如下&#xff1a; 题目描述 给定一个由若干 0 和 1 组成的数组 nums&#xff0c;以及一个整数 k。你可以将最多 k 个 0 翻转为 1。返回经过翻转操作后&#xff0c;数组中连续 1 的最大个数…...

Flink CDC 3.4 发布, 优化高频 DDL 处理,支持 Batch 模式,新增 Iceberg 支持

引言 Apache Flink 社区很开心地宣布&#xff0c;在经过4个月的版本开发之后&#xff0c;Flink CDC 3.4.0 版本已经正式发布。Flink CDC 是流行的流式数据集成框架&#xff0c;CDC 3.4.0 版本强化了框架对于高频表结构变更的支持&#xff0c;框架支持了 batch 执行模式&#x…...

NIFI的处理器:JSLTTransformJSON 2.4.0

该处理器使用JSLT转换FlowFile JSON有效负载的格式。使用转换后的内容创建新的FlowFile&#xff0c;并将其路由到“成功”关系。如果JSLT转换失败&#xff0c;则将原始FlowFile路由到“失败”关系。 需要注意的是&#xff0c;编译JSLT转换可能相当昂贵。理想情况下&#xff0c…...