posts - 21,comments - 0,views - 13619

概念

指针(Pointer)是编程语言中的一类数据类型及其对象或变量,用来表示或存储一个内存地址,这个地址的值直接指向(points to)存在该地址的对象值。Rust 中最常见的指针类型就是引用(reference),引用由 & 符号指示,并借用他们指向的值。除了引用以外,没有任何特殊功能。
智能指针(Smart pointer)是一类数据结构,它在模拟指针的同时也拥有额外的元数据和功能。例如自动内存管理和边界检查。使用智能指针的目的是为了减少因滥用指针而导致的错误,同时能够保证效率。智能指针可以使内存释放自动化,防止内存泄露。当某个对象的最后一个(或者唯一)所有者被销毁时,由智能指针控制的对象会自动销毁(这有点类似引用计数)。

Rust 中的智能指针

Rust 中,智能指针通常使用结构体实现,智能指针和普通的结构体区别在于智能指针实现了 std::ops::Derefstd::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> 指针使用引用计数来决定是否销毁一个数据,我们很容易使用 RcRefCell 创建循环引用,最终这些引用计数都无法被归零,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> 指针

Mutexmutual exclusion ),意为互斥,用于保护共享数据, 保证共享数据在任意时刻只能被一个线程访问。通常,线程要访问数据时,首先要获取Mutex(lock)来表明其希望访问某些数据。锁是Mutex中的数据结构,用于记录数据访问权。可以说,Mutex 是通过锁系统来保护 (guarding) 其共享数据的。

Mutex 需要分两步使用:

  1. 在使用数据前先尝试获取锁;
  2. 处理完被保护的数据后,解锁数据。

得益于 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);
}

使用 ArcMutex 实现多线程之间共享数据

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

posted on   y1j2x34  阅读(1071)  评论(0编辑  收藏  举报
(评论功能已被禁用)
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示