概念
指针(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
浙公网安备 33010602011771号