概念
指针(Pointer)是编程语言中的一类数据类型及其对象或变量,用来表示或存储一个内存地址,这个地址的值直接指向(points to)存在该地址的对象值。Rust 中最常见的指针类型就是引用(reference),引用由 &
符号指示,并借用他们指向的值。除了引用以外,没有任何特殊功能。
智能指针(Smart pointer)是一类数据结构,它在模拟指针的同时也拥有额外的元数据和功能。例如自动内存管理和边界检查。使用智能指针的目的是为了减少因滥用指针而导致的错误,同时能够保证效率。智能指针可以使内存释放自动化,防止内存泄露。当某个对象的最后一个(或者唯一)所有者被销毁时,由智能指针控制的对象会自动销毁(这有点类似引用计数)。
Rust 中的智能指针
Rust 中,智能指针通常使用结构体实现,智能指针和普通的结构体区别在于智能指针实现了 std::ops::Deref
和 std::ops::Drop
trait。 Deref
让智能指针结构体的实例能够拥有像引用一样可以使用 *
号‘解引用’。而 Drop
trait 则可以让我们自定义 drop()
方法,这类似于其他语言的析构函数, 当结构体实例超出作用域范围时会被自动调用。
Rust 标准库中的智能指针
std::boxed::Box<T>
: 相当于 C++11 中的unique_ptr
, 用于在堆上分配值,独占内存,不共享数据;std::rc::Rc<T>
: reference counter, 相当于 C++11 中的shared_ptr
,以引用计数的方式共享内存,其数据可以有多个所有者。Arc<T>
,atomic reference counter, 可被多线程操作,但只能只读。std::rc::Weak
:, 相当于 C++11 中的weak_ptr
, 不以引用技术的方式共享内存。Mutex<T>
,互斥指针,能保证修改的时候只有一个线程参与。Vec<T>
和String
Box<T>
指针
在 Rust 中,为了尽可能抛弃指针,所有数据默认都是存储在栈上的,但是如果要把数据存储在堆上,在堆上开辟内存,就要使用指针。
Rust 提供了 Box<T>
指针,它可以对数据装箱并分配在堆上,在栈上保留指向堆数据的指针,Box 为这个分配提供了所有权,当超出作用域时,数据便会被丢弃。
Box::new()
创建 Box, 将数据从栈移动到堆中:
fn main() {
let value = 15; // 数值存储在栈上
let boxed_value = Box::new(value); // 使用Box后, 数值存储在堆上
println!("boxed_value = {}", boxed_value);
}
访问 Box<T>
指针存储的数据
use std::cmp::{ PartialEq };
struct Vec2D {
x: i32,
y: i32
}
impl PartialEq<Vec2D> for Vec2D {
fn eq(&self, other: &Self) -> bool {
self.x == other.x && self.y == other.y
}
}
fn main() {
let normal = Box::new(Vec2D { x: 1, y: 1});
/*l1*/println!("{}", normal.x);
/*l2*/println!("{}", (*normal).x);
/*l3*/println!("{}", *normal == Vec2D { x: 1, y: 1 });
/*l4*/// println!("{}", normal == Vec2D { x: 1, y: 1 }); // Error: type mismatch
}
最终输出:
1
1
true
要访问 Box<T>
存储在堆上的数据, 必须使用解引用操作符*
解引用, l1 之所以能工作, 是因为使用.
运算符时,Rust编译器帮我们自动解引用,所以实际编译后的代码和l2相同。
使用Box
指针创建递归类型
Rust 需要在编译时知道一个类型会占用多少空间, 而递归类型是无法在编译时知道大小的,它的值的嵌套关系理论上是可以无限进行的,而Box<T>
指针拥有固定的大小,我们可以利用这个特性实现递归类型。
use self.List::*;
#[derive(Debug)]
enum List<T> {
Cons(T, Box<List<T>>),
Nil
}
fn main() {
let list: List<i32> = Cons(1, Box::new(Cons(2, Box::new(Nil))));
println("{:?}", list);
}
所有权
Box<T>
指针同时只能有一个变量拥有对其指向数据的所有权,并且同时只能存在一个可变引用或多个不可变引用。
fn main() {
let a = Box::new(0);
let b = a;
// 报错: value borrowed here after move
println!("a = {}", a);
// cannot borrow `b` as immutable because it is also borrowed as mutable
let c = &mut b;
let d = &b;
println!("{}, {}", c, d);
}
Rc<T>
指针
相比Box<T>
指针的单一所有权,Rc<T>
指针则实现了多重所有权以共享内存,该指针会跟踪某个值的引用次数,以确定该值是否仍在使用中,如果引用次数为0,则表示可以清除该值, 这有点类似于引用计数法 GC, 而因为要在运行时多记录一个引用计数,这会引起一定的消耗。
在需要共享内存的时候,调用 clone()
方法获取所有权(不会深度复制),增加引用计数:
use std::rc::Rc;
#[derive(Debug)]
struct P (i32);
impl Drop for P {
fn drop(&mut self) {
println!("drop P");
}
}
impl Clone for P {
fn clone(&self) -> Self {
println!("You will never see this message");
P(self.0)
}
}
fn main() {
let value = Rc::new(P(12));
let v1 = value.clone(); // 引用计数 +1, 不会克隆实例
let closure1 = move || {
println!("{:?}", v1);
};
let v2 = value.clone(); // 引用计数 +1
let closure2 = move || {
println!("{:?}", v2);
};
closure1();
closure2();
}
Rc<T>
只适用于单线程场景,如果要在多线程场景中使用线程安全的智能指针,需要使用 Arc<T>
。 如下面的示例,将无法通过编译:
use std::rc::Rc;
use std::thread;
fn main() {
let value = Rc::new(10);
let handle = thread::spawn(move || {
println!("value = {}", value);
});
handle.join();
}
编译错误:
error[E0277]: `Rc
` cannot be sent between threads safely
Arc<T>
指针
Arc代表 “Atomic Rc”, 原子化的 Rc<T>
智能指针,Arc 使用线程安全的原子操作进行引用计数,只是原子操作相比普通的内存访问需要额外的开销。
Arc 和 Rc 一样,通过 clone 方法产生新实例增加引用计数共享所有权, 要求只能读,不能修改。如果需要对数据进行修改,单独使用 Arc 和 Rc 则无法满足需求,需要配合其他数据类型一起使用,比如 RefCell<T>
和 Mutex<T>
。
use std::{cell::RefCell, sync::Arc};
fn main() {
let mut b = Box::new(5);
*b = 6;
println!("{}", b); // 6
let mut a = Arc::new(1);
*mut a = 5;// cannot assign. trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Arc<i32>`
println!("{}", a);
let r = Arc::new(RefCell::new(1));
*r.borrow_mut() = 5;
println!("r = {}", r.borrow());
}
循环引用
由于 Rc<T>
指针使用引用计数来决定是否销毁一个数据,我们很容易使用 Rc
和 RefCell
创建循环引用,最终这些引用计数都无法被归零,Rc<T>
所持有的数据也不会被释放清零。
use std::{rc::Rc, cell::RefCell};
struct Node {
next: Option<Rc<RefCell<Node>>>
}
impl Drop for Node {
fn drop(&mut self) {
println!("Dropping Node");
}
}
fn main() {
let first = Rc::new(RefCell::new(Node { next: None }));
let second = Rc::new(RefCell::new(Node { next: None }));
let third = Rc::new(RefCell::new(Node { next: None }));
(*first).borrow_mut().next = Some(Rc::clone(& second));
(*second).borrow_mut().next = Some(Rc::clone(& third));
(*third).borrow_mut().next = Some(Rc::clone(& first));
}
上面例子中, first
, second
, third
离开作用域后,按道理会输出三次 Dropping Node
。实际上啥也没有。Rust 不是绝对的安全,使用 Rc<T>
和 RefCell<T>
就可以创建循环引用,导致引用计数都不会被清零,最终出现内存泄露。
Weak<T>
指针
Weak 非常类似于 Rc,但是与 Rc 持有所有权不同,Weak 不持有所有权,它仅仅保存一份指向数据的弱引用:如果你想要访问数据,需要通过 Weak 指针的 upgrade 方法实现,该方法返回一个类型为 Option<Rc<T>>
的值。Weak<T>
引用不记入所有权,因此无法阻止所引用的内存值被释放,而且保证引用关系存在, 引用值存在是返回 Some
, 不存在就返回 None
。
使用 Weak<T>
可以解决循环引用问题,上面的循环引用问题改成用 Weak<T>
实现如下:
use std::{rc::{Rc, Weak}, cell::RefCell};
struct Node {
next: Option<Rc<RefCell<Node>>>,
head: Option<Weak<RefCell<Node>>>
}
impl Drop for Node {
fn drop(&mut self) {
println!("Dropping Node");
}
}
fn main() {
let first = Rc::new(RefCell::new(Node { next: None, head: None }));
let second = Rc::new(RefCell::new(Node { next: None, head: None }));
let third = Rc::new(RefCell::new(Node { next: None, head: None }));
(*first).borrow_mut().next = Some(Rc::clone(& second));
(*second).borrow_mut().next = Some(Rc::clone(& third));
(*third).borrow_mut().head = Some(Rc::downgrade(& first));
}
Mutex<T>
指针
Mutex
( mutual exclusion ),意为互斥,用于保护共享数据, 保证共享数据在任意时刻只能被一个线程访问。通常,线程要访问数据时,首先要获取Mutex
的锁(lock)来表明其希望访问某些数据。锁是Mutex
中的数据结构,用于记录数据访问权。可以说,Mutex
是通过锁系统来保护 (guarding) 其共享数据的。
Mutex
需要分两步使用:
- 在使用数据前先尝试获取锁;
- 处理完被保护的数据后,解锁数据。
得益于 Rust 类型系统和所有权, 我们不用担心加锁和解锁问题:
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m
.lock() // 使用 lock 方法获取锁,这个调用会阻塞线程,直到其它线程释放该 Mutex 的锁为止
.unwrap(); // 如果有其它线程拥有锁,但是那个线程 panic,那么 unwrap 时也会 panic
// 现在可以使用 Mutex 中的数据, 并视为可变引用去修改它
*num = 6; // 虽然 m 是不可变的, 但是 Mutex 提供了内部可变性, 我们可以获取内部值的可变引用
// lock 方法返回一个 MutexGuard 的智能指针,这个指针实现了 Deref 来指向其内部数据,也提供了 Drop 实现在离开作用域时自动释放锁。
}
println!("m = {:?}", m);
}
使用 Arc
和 Mutex
实现多线程之间共享数据
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!("counter Result: {}", *counter.lock().unwrap());
}
参考资料:
https://course.rs/advance/circle-self-ref/circle-reference.html
https://web.mit.edu/rust-lang_v1.26.0/arch/amd64_ubuntu1404/share/doc/rust/html/std/sync/struct.Arc.html#method.get_mut
https://cloud.tencent.com/developer/article/1586996
https://mytechshares.com/2021/08/17/smart-pointer-rc-weak-arc/
https://skyao.io/learning-rust/std/sync/arc.html#:~:text=线程安全的引用计数,同时增加一个引用计数。
https://coolshell.cn/articles/20845.html#Rust的智能指针
https://www.cnblogs.com/Evsward/p/rust-one.html
https://en.wikipedia.org/wiki/Smart_pointer
https://doc.rust-lang.org/book/ch15-00-smart-pointers.html
https://www.twle.cn/c/yufei/rust/rust-basic-smart-pointers.html
https://willendless.github.io/编程语言/2021/02/28/rust自动解引用/
https://kaisery.github.io/trpl-zh-cn/ch15-01-box.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗