
介绍
在我们讨论什么是智能指针之前,让我们试着为指针是什么设定一个明确的基础,编程中的指针通常是指向另一条数据位置的数据,例如,您的家庭地址指向您居住的地方。智能指针就像常规指针,直接指向数据的位置,但具有额外的功能,包括将多个所有者分配到一个值、内部可变性等。
Ps.&在Rust中引用也可以被视为指针,因为它指向一个数据的引用。
在本文中,我们将研究Rust中常见的智能指针以及我们如何使用它们。Rust中的一些常见智能指针包括Box、Rc、Arc、Refcell和Mutex。
盒子
Box智能指针通常用于将数据分配给堆,因此使用Box智能指针,您可以将通常在堆栈上分配给堆的i32,当您有大型数据,由于大小有限,您不想存储在堆栈上时,这通常会很有帮助。
以下是您使用Box智能指针的方法:
// 我们的i32将分配到堆上,而不是堆栈上
let heap_allocated_i32 = Box::new(1);
以下是使用Box存储大数据的方法:
struct LargeData {
数据:[i32; 1000000],//包含100万个元素的数组
}
fn main(){
let boxed_data = Box::new(LargeData{
数据:[0; 1000000],//用零初始化数组
});
}
通常,类型为[i32; 1000000]的变量将存储在堆栈上,但由于其大小,效率低下,这就是为什么我们想将其存储在堆上。
您想要使用Box的另一个原因是创建递归数据结构,如二叉树和链接列表。递归数据结构,如树,通常是自我引用,因此很难在Rust中编译时确定其大小。Box智能指针通过将固定大小的指针存储在堆栈上的数据来帮助绕过这种情况,而实际数据驻留在堆上。这种方法可以创建递归结构,而无需提前知道其大小。
这是二叉树的简单实现,带有Box
#[derive(Debug)]
struct TreeNode {
值:T,
左:Option<Box<TreeNode>>,
右:Option<Box<TreeNode>>,
}
impl TreeNode {
fn new(value: T) -> TreeNode {
树节点{
价值,
左:无,
右:没有,
}
}
}
fn main() {
let left_leaf = TreeNode::new("left leaf");
let right_leaf = TreeNode::new("right leaf");
让根 = TreeNode {
值:“根”,
left: Some(Box::new(left_leaf)),
右:一些(Box::new(right_leaf)),
};
println!("{:#?}", root);
}
Box智能指针和普通指针的另一个重要区别是,Box智能指针是自有指针,当您DropBox时,它会Drop它包含的T。
RC(参考计数)
Rust编译器遵循一条规则,即每个变量都应该有一个所有者,但使用RC智能指针,我们可以搞乱该规则。顾名思义,引用计数(RC)智能指针可以计数有多少变量拥有它包装的数据,当该数据的所有者数量为零时,数据将从内存中重新分配。
这里有一个例子:
#[derive(Debug)]
//这就是我们将Rc纳入范围的方式
使用std::rc::Rc;
结构人{
名称:字符串,
年龄:u32,
}
fn main() {
let person1 = Rc::new(Person {
名称:“Alice”.to_string(),
年龄:25岁,
});
// 克隆Rc指针以创建额外的引用
let person2 = Rc::clone(&person1);
let person3 = Rc::clone(&person1);
println!("姓名:{},年龄:{}",person1.name,person1.age);
println!("名称:{},年龄:{}",person2.name,person2.age);
println!("姓名:{},年龄:{}",person3.name,person3.age);
println!("参考计数:{}",Rc::strong_count(&person1));
}
值得注意的是,Rc上的克隆方法不会克隆它包装的数据,而是制作另一个指向堆上数据的Rc。
弧(原子参考计数)
Arc智能指针就像Rc智能指针一样,但有一点好处,它是线程安全的。这只是意味着Arc指针允许我们赋予多个变量对某项数据的所有权,同时能够在多个线程中访问它。让我们尝试一下之前带有多个线程的Rc代码示例,看看它是如何表现的:
使用std::thread;
使用std::rc::Rc;
结构人{
名称:字符串,
年龄:u32,
}
fn main() {
let person = Rc::new(Person {
名称:“Alice”.to_string(),
年龄:25岁,
});
let person_clone1 = Rc::clone(&person);
let person_clone2 = Rc::clone(&person);
let thread1 = thread::spawn(移动 || {
println!("线程1:名称={},年龄={}",person_clone1.name,person_clone1.age);
// 模拟线程1中正在完成的一些工作
线程::sleep_ms(1000);
});
let thread2 = thread::spawn(move || {
println!("线程2:名称={},年龄={}",person_clone2.name,person_clone2.age);
// 模拟线程2中正在完成的一些工作
线程::sleep_ms(1000);
});
thread1.join().unwrap();
thread2.join().unwrap();
println!("参考计数:{}",Rc::strong_count(&person));
}
当我们运行此代码时,我们最终会遇到以下错误:
error[E0277]:`Rc`无法在线程之间安全发送
--> src/main.rs:18:33
我们收到这个错误是因为Rc不是线程安全的,仅适用于单个线程。
如果我们希望能够使用Arc智能指针在多个线程之间共享数据,我们将如何做到这一点:
使用std::thread;
使用std::sync::Arc;
结构人{
名称:字符串,
年龄:u32,
}
fn main() {
let person = Arc::new(Person {
名称:“Alice”.to_string(),
年龄:25岁,
});
let person_clone1 = Arc::clone(&person);
let person_clone2 = Arc::clone(&person);
let thread1 = thread::spawn(移动 || {
println!("线程1:名称={},年龄={}",person_clone1.name,person_clone1.age);
// 模拟线程1中正在完成的一些工作
线程::sleep_ms(1000);
});
let thread2 = thread::spawn(move || {
println!("线程2:名称={},年龄={}",person_clone2.name,person_clone2.age);
// 模拟线程2中正在完成的一些工作
线程::sleep_ms(1000);
});
thread1.join().unwrap();
thread2.join().unwrap();
// 嘿,Curly,你知道为什么这是一个吗?我知道这与线程有关
println!("参考计数:{}",Arc::strong_count(&person));
}
虽然Arc更适合多线程情况,但在处理单线程时,它比Rc慢。
RefCell(参考单元)
我们看到了如何通过分别在单个线程和多个线程中将多个所有者分配给一个值来扰乱Rc和Arc的所有权规则。使用RefCell智能指针,我们可以通过突变不可变引用来弯曲借阅规则,这种模式在Rust中通常被称为内部可变性。
Rust中的借阅规则之一意味着你不能对不可变值进行可变引用,所以当我们尝试这样的事情时:
fn main(){
让a:i32 = 14;
*&mut a += 1;
println!("{}", a);
}
我们会收到这样的错误:
错误[E0596]:不能借用`a`作为可变的,因为它没有被声明为可变的
--> src/main.rs:3:3
|
3 | *&mut a += 1;
| ^^^^^^^ 不能借用为可变的
|
帮助:考虑将此更改为可变的
|
2 | let mut a:i32 = 14;
| +++
当然,我们可以按照编译器说的去做,通过添加mut a: i32制作a可变的,但如果我们不能呢?然后我们必须像这样使用RefCell智能指针:
使用std::cell::RefCell;
fn main(){
let a: RefCell = RefCell::new(14);
*a.borrow_mut() += 1;
println!("{}", *a.borrow());
}
注意:对于RefCell智能指针,您可以将.borrow和.borrow_mut方法分别视为&和&mut
让我们看看一个简单的现实场景,我们需要使用RefCell智能指针和内部可变性模式。想象一下,您想为数据类型实现一个特征,该特征的方法之一将不可变的引用作为其参数,如&self,但您希望能够突变此参数,因为Rust借用,您将无法正常执行此,但值得庆幸的是,我们的工具包中有RefCell。
例如,假设这是我们想要为我们的数据类型实现的特征:
特征计数器{
fn增量(&self);
fn get(&self) -> i32;
}
这是我们的数据类型,叫做Count
使用std::cell::RefCell;
结构计数{
值:RefCell,
}
impl计数{
fn new() -> 自我 {
计数{
值:RefCell::new(0),
}
}
}
以下是我们如何为我们的Count类型实现方法:
impl计数计数器{
fn increment(&self){
// 我们用借贷_mut 方法对 `&self` 进行可变引用
let mut value = self.value.borrow_mut();
// 然后突变可变引用
*值 += 1;
}
fn get(&self) -> i32 {
*self.value.borrow()
}
}
我们的整个代码,包括测试main功能,应该如下:
使用std::cell::RefCell;
特征计数器{
fn增量(&self);
fn get(&self) -> i32;
}
结构计数{
值:RefCell,
}
impl计数{
fn new() -> 自我 {
计数{
值:RefCell::new(0),
}
}
}
impl计数计数器{
fn increment(&self){
// 我们用借贷_mut 方法对 `&self` 进行可变引用
let mut value = self.value.borrow_mut();
// 然后突变可变引用
*值 += 1;
}
fn get(&self) -> i32 {
*self.value.borrow()
}
}
fn main() {
let count = Count::new();
计数。增量();
计数。增量();
println!("Count: {}", count.get(); // 输出将是 "Count: 2"
}
Mutex(相互排除)
当我们希望能够安全地突变多个线程中的共享数据时,Mutex智能指针很有帮助。由于完整的首字母缩写意味着“相互排除”,每个线程可以在突变时锁定一个值,直到它超出范围,每个线程放在共享值上的锁定可以防止其他线程突变。让我们看一个例子:
使用std::sync::Mutex;
fn main(){
// 用 Mutex 包装整数
let value = Mutex::new(0);
// 将`value`锁定到此变量
let mut value_changer = value.lock().unwrap();
// 顺差值,然后将包装的整数增加1
*value_changer += 1;
println!("{}", value_changer); // 输出:1
}
在本例中,我们将整数包装在Mutex,并将其分配给一个名为value的变量,稍后我们将Mutex锁定到value_changer变量,然后在下一行上增加它。随着value_changer对值的锁定,没有其他变量会变异甚至访问它。举个例子:
使用std::sync::Mutex;
fn main(){
//相同的代码
let value = Mutex::new(0);
let mut value_changer = value.lock().unwrap();
*value_changer += 1;
//看这里!
println!("{:?}", &value); // Mutex { data: , poisoned: false, ..}
}
When we try to output value we get Mutex { data:
使用std::sync::Mutex;
fn main(){
//相同的代码
let value = Mutex::new(0);
let mut value_changer = value.lock().unwrap();
*value_changer += 1;
//这与超出范围的变量相同
std::mem::drop(value_changer);
//看这里!
println!("{:?}", value); // Mutex { data: 1, poisoned: false, ..}
}
请注意,当value_changer超出范围时,value可以访问Mutex,这与thread结束后删除变量的方式类似,因此当您在线thread中锁定Mutex时,只有该线程才能访问该值并能够突变它。我们这样做是因为我们希望在多个线程中使用数据时能够保护数据,以防止竞争条件,并利用我们的并发程序是线程安全的。
结论
在这篇文章中,我们研究了Rust中的智能指针,它们是什么,以及我们如何利用它们来发挥我们的优势。我们研究了Rust中一些常见的智能指针,包括Box、Rc、Arc、RefCell和Mutex。我们还看到了如何使用智能指针将数据直接分配到堆,创建二叉树和链接列表等递归数据结构,扰乱所有权规则,并在Rust中实现内部可变性模式。最后,我们研究了如何在多个线程中使用Mutex智能指针来保护数据,并防止竞争条件,以使我们的并发程序线程安全。