Skip to content

smart pointer

智能指针是一种数据结构,在 Rust 中,它们的主要功能是拥有它们指向的数据。它们还可以拥有其他功能,例如引用计数。

智能指针的“智能”之处在于它们具有额外的元数据和功能。

这句话意味着,与传统的裸指针(raw pointers)相比,智能指针不仅仅是一个指向数据的地址。智能指针还包含其他的信息和能力。

元数据

  1. 引用计数:例如,在RcArc智能指针中,元数据会记录有多少个所有者共享此数据。这样,当最后一个所有者被丢弃时,数据可以被安全地删除。
  2. 借用标志:例如,在RefCell中,元数据会跟踪当前的借用状态,例如是否有活跃的可变借用,以及有多少个不可变借用。

额外的元数据: 这可以包括如引用计数(在Rc和Arc中)的信息,表示有多少个所有者共享这个数据。

功能

**智能指针通常有一些内置的方法和功能。**例如:

Drop特性:当智能指针离开其作用域时,Drop特性允许它执行一些清理工作,例如释放内存。 Deref特性:使得智能指针可以被解引用,从而让它们表现得像常规的引用。 RefCell的borrow和borrow_mut方法:这些方法允许在运行时借用或可变借用内部数据,提供动态的借用检查。 因此,当我们说智能指针是“智能”的时候,我们是指它们不仅仅是一个简单的指针,它们还有其他的附加信息和功能,这些信息和功能使得它们在内存管理、数据共享和并发中更为强大和灵活。

Rust 中的智能指针通常通过结构体来实现,并实现了DerefDrop特性。

Deref特性允许智能指针实例表现得像常规引用,使得可以读写指针指向的数据。Drop特性则允许我们定制当智能指针离开作用域时的代码,常常用于释放资源。

&T 和 &mut T

  1. &T (不可变引用)

    • 当你只想读取数据,而不想修改它时,使用不可变引用。
    • 你可以同时拥有多个不可变引用指向相同的数据。
    • 不可变引用和可变引用不能同时存在。
  2. &mut T (可变引用)

    • 当你想修改引用的数据时,使用可变引用。
    • 在同一时间,对于给定的数据,只能存在一个可变引用。这避免了数据竞争。
    • 如果你有一个可变引用,你不能同时拥有任何不可变引用。
       let mut data = 5;
    
       // 创建一个不可变引用
       let ref1 = &data;
       let ref2 = &data; // 这是允许的,因为它们都是不可变引用
    
       // 创建一个可变引用
       // let mut_ref = &mut data; // 这将会导致编译错误,因为我们已经有了不可变引用
    
       // 释放不可变引用
       drop(ref1);
       drop(ref2);
    
       // 现在我们可以创建一个可变引用
       let mut_ref = &mut data;
       *mut_ref += 1; // 使用可变引用修改数据

    即“在任何时候,要么只有一个可变引用,要么只有多个不可变引用”,以确保引用安全性和避免数据竞争

    Box< T>

    Box<T> 是 Rust 中的一个智能指针,它允许你在堆上分配值。它是一个包含堆分配数据的指针的结构。当 Box<T> 离开其作用域时,其析构函数被调用,堆上的数据以及箱子本身所使用的堆内存都被清理,确保没有内存泄漏。

    以下是关于 Box<T> 的一些关键点:

    1. 堆上分配Box<T> 提供了一个简单的方法在堆上分配数据。例如,大型数据结构或者那些实现时不知道大小的类型,如递归数据结构。
    2. 所有权Box<T> 提供了所有权语义,这意味着它遵循 Rust 的所有权规则,例如数据移动的概念。
    3. 用于抽象Box<T> 常常与特性对象一起使用,为不同类型提供统一的接口。
    4. 递归数据结构:例如,定义递归的链表或树时,Box<T> 是必要的,因为在 Rust 中,类型的大小在编译时必须是已知的,而 Box 允许我们绕过这一限制。

    以下是使用 Box<T> 的一些示例:

    // 在堆上分配一个整数
    let b = Box::new(5);
    println!("b = {}", *b);
    
    // 定义递归数据结构
    enum List<T> {
        Cons(T, Box<List<T>>),
        Nil,
    }
    
    let list: List<i32> = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));

    在上述示例中,Box 允许我们在堆上分配一个整数,并定义一个递归的 List 枚举。没有 Box,我们无法定义这种递归数据结构,因为 Rust 需要在编译时知道 List<T> 的确切大小,而递归定义意味着大小是未知的。使用 Box 解决了这个问题,因为 Box 的大小是固定的(它是一个指针大小)。

std::rc::Rc< T>

std::rc::Rc<T> 是 Rust 中的一个引用计数智能指针,用于跟踪一个值的引用数量。当引用数量降至零时,值及其所有相关资源都会被清理。Rc stands for "Reference Counting".

以下是关于 std::rc::Rc<T> 的一些关键点:

共享所有权:Rc<T> 允许多个指针同时拥有同一个数据的所有权。每当一个新的引用到某个数据被创建时,引用计数会增加,每当一个引用被丢弃时,引用计数会减少。

只在单线程环境中使用:Rc<T> 不是线程安全的。如果需要在并发或并行情境中使用引用计数智能指针,应考虑使用 std::sync::Arc<T>。

避免循环引用:如果两个 Rc<T> 值相互引用,将形成循环,这会导致内存泄漏。为解决这个问题,Rust 提供了 std::rc::Weak<T>。

以下是使用 std::rc::Rc<T> 的示例:

use std::rc::Rc;

let foo = Rc::new(vec![1.0, 2.0, 3.0]);

// 增加两个引用,使引用计数增加到 3
let a = foo.clone();
let b = foo.clone();

// 当 `foo`、`a` 和 `b` 都离开作用域后,数据会被清理

在上述示例中,我们创建了一个 Rc 指向一个向量。然后,我们调用了 clone 来创建两个额外的引用。这不会深度复制数据,而只是增加引用计数。当所有三个 Rc 指针离开作用域并被丢弃时,它们所指向的数据也会被丢弃,因为此时引用计数将为零。

std::rc::Weak<T>

std::rc::Weak<T> 是 Rust 中的一个弱引用智能指针,它与 std::rc::Rc<T> (一个强引用计数智能指针)一同工作。Weak<T> 并不会增加引用计数,所以它不会阻止其所指向的值被丢弃。这种特性使得 Weak<T> 特别有用于避免循环引用,这在使用 Rc<T> 时可能会出现。

以下是关于 std::rc::Weak<T> 的一些关键点:

  1. 避免循环引用:例如,如果你有两个 Rc<T> 值,它们互相引用,它们之间会形成一个循环,导致内存泄露。但如果其中一个是 Weak<T>,这就避免了循环,因为弱引用不会增加引用计数。
  2. 不会阻止值被丢弃Weak<T> 本身并不会阻止其引用的值被丢弃。只有当对应的 Rc<T> 存在时,Weak<T> 才能够被升级为一个有效的 Rc<T>
  3. 升级:可以尝试使用 Weak<T>upgrade 方法将其升级为一个 Option<Rc<T>>。如果对应的 Rc<T> 还存在,upgrade 会返回 Some,否则返回 None

以下是一个使用 std::rc::Rc<T>std::rc::Weak<T> 的示例,展示了如何避免循环引用:

use std::rc::{Rc, Weak};

struct Node {
    value: i32,
    next: Option<Rc<Node>>,
    prev: Option<Weak<Node>>,
}

fn main() {
    let first = Rc::new(Node {
        value: 1,
        next: None,
        prev: None,
    });

    let second = Rc::new(Node {
        value: 2,
        next: None,
        prev: Some(Rc::downgrade(&first)),
    });

    // 这里我们没有创建循环引用,因为 `first` 的 `prev` 是一个 `Weak<Node>`。
}

在上述示例中,我们创建了两个 Nodefirst 节点引用 second 作为下一个节点,但使用 Weak<T> 引用作为前一个节点,从而避免了循环引用。

总的来说,std::rc::Weak<T> 是在共享所有权情境下,用于避免循环引用和内存泄露的一个工具。

强引用和弱引用

强引用和弱引用是两种不同的引用类型,它们在内存管理和垃圾收集上的行为有所不同。这两种引用主要在某些编程语言中使用,尤其是那些支持引用计数的语言(如 Rust、Objective-C)。下面是它们之间的主要区别和特点:

强引用(Strong Reference):

  1. 引用计数增加:当你创建一个强引用,它所指向的对象的引用计数会增加。
  2. 防止对象被释放:只要对象有至少一个强引用,它就不会被释放。
  3. 常见使用:在大多数情况下,当你创建一个引用或指针,你实际上是创建一个强引用。
  4. 可能导致的问题:强引用可能导致循环引用,从而导致内存泄漏。例如,如果两个对象相互强引用,即使没有其他引用它们,它们也不会被释放。

弱引用(Weak Reference):

  1. 引用计数不变:当你创建一个弱引用,它所指向的对象的引用计数不会增加。
  2. 不会阻止对象被释放:即使存在弱引用,对象仍然可以被释放。
  3. “安全”引用:弱引用可以被视为一种“安全”引用,因为它不会导致循环引用和内存泄漏。
  4. 需要“升级”:为了访问弱引用所指向的对象,通常需要将其“升级”为强引用。如果该对象已经被释放,则升级会失败。

在 Rust 中,std::rc::Rc<T>std::rc::Weak<T> 分别代表强引用和弱引用。使用 Rc::clone 会增加强引用计数,而 Rc::downgrade 会创建一个弱引用。要从弱引用访问对象,你需要调用 Weak::upgrade,这会尝试返回一个 Option<Rc<T>>

"计数" 是指 "引用计数"(Reference Counting)。引用计数是一种内存管理技术,用于跟踪一个对象在内存中被多少个引用所指向。每当一个新的引用指向对象时,计数增加;每当一个引用不再指向对象或被销毁时,计数减少。当计数降至0时,意味着没有任何引用再指向该对象,因此该对象可以被安全地回收。

在 Rust 的 std::rc::Rc<T>(强引用)和 std::rc::Weak<T>(弱引用)中:

  1. 强引用计数Rc<T> 实现了强引用计数。每当你使用 Rc::clone 创建一个新的 Rc<T> 引用时,强引用计数会增加。当 Rc<T> 引用离开其作用域并被丢弃时,强引用计数会减少。当强引用计数降至0时,该对象会被销毁并释放其内存。
  2. 弱引用计数Weak<T> 不会影响强引用计数,但它有自己的计数器,叫做弱引用计数。弱引用主要用于避免循环引用,它允许引用一个对象,但不阻止该对象被销毁。当所有的强引用都被丢弃,对象会被销毁,即使还有弱引用存在。如果尝试通过弱引用访问对象,你需要“升级”它,这可能会成功(返回 Some(Rc<T>))或失败(返回 None),取决于相关对象是否仍然存在。

std::cell::Cell<T>和std::cell::RefCell<T>

std::cell::Cell<T> 和 std::cell::RefCell<T> 都是 Rust 的标准库中提供的用于实现内部可变性的类型,但它们适用于不同的场景并有不同的行为。让我们来比较它们:

Cell<T>:

值语义: Cell<T> 适用于那些实现了 Copy trait 的类型。这意味着你不能直接获取到存储在 Cell 中的值的引用,而是获取其值的副本。 不可借用为引用: 你不能从 Cell<T> 获取一个可变或不可变引用,但你可以使用 set 和 get 方法对其进行读写。 用途: 对于小的 Copy 类型,例如 i32、bool 等,Cell 是一个很好的选择。

RefCell<T>:

引用语义: RefCell<T> 允许你在运行时借用其内部数据为可变或不可变引用。但这是在运行时进行检查的,如果你违反了借用规则(例如,同时持有多个可变引用),程序会 panic。 动态借用检查: 当你尝试借用数据时,RefCell 会在运行时检查是否违反了 Rust 的借用规则,如果违反了,它会 panic。 用途: 当你需要在逻辑上保持不可变性,但在某些情况下需要修改内部数据时,RefCell 是一个不错的选择。它通常与 Rc 一起使用来创建多个拥有者的可变数据。 总之,这两个类型都提供了一种绕过 Rust 的借用检查器的方法,但要确保在运行时遵循借用规则。Cell<T> 更适用于简单的 Copy 类型,而 RefCell<T> 提供了更为灵活的内部可变性,但带有运行时的借用检查成本。

*const T 和 *mut T

*const T 和 *mut T 是 Rust 中的原生指针类型。它们提供了一个对底层内存的不安全、未经检查的引用。这两种指针类型通常在与 C 代码互操作或编写低级代码时使用。

*const T:

不可变原生指针:它表示一个指向某种类型 T 的常量指针。这意味着你理论上不应该通过这个指针修改所指向的数据。 只读:虽然这是一个原生指针,但从其语义上,你应该把它当作一个只读指针。 *mut T:

可变原生指针:它表示一个指向某种类型 T 的可变指针,允许你修改所指向的数据。 读写:你可以通过这个指针读取和修改数据。 需要注意的是,尽管有 const 和 mut 两种类型,但在底层它们都是普通的指针,区别只在于它们的语义。Rust 的安全保证并不适用于这些指针,因此在使用它们时必须非常小心。通常,你只在需要绕过 Rust 的安全保证时才使用它们,并且需要确保手动管理相关的所有安全性问题。

此外,与 Rust 中的引用(例如 &T 和 &mut T)不同,原生指针不遵循 Rust 的借用规则。这意味着你可以拥有多个可变原生指针指向同一块内存,或者同时拥有可变和不可变原生指针,而编译器不会阻止这种行为。因此,使用原生指针时必须非常小心,确保不违反这些规则,否则可能会导致未定义的行为。

自动解引用

自动解引用(Deref Coercion)是 Rust 语言的一个功能,它允许 Rust 在调用方法时自动为指针或智能指针解引用。

当你调用一个对象上的方法时,Rust 会自动为你解引用(或连续解引用)该对象,直到找到一个实现了该方法的类型。这一过程是由 Rust 中的 Deref 特性和 DerefMut 特性支持的。

举个例子来说,如果我们有一个 &Box<T> 类型的引用,我们可以直接在其上调用 T 类型的方法,而无需手动解引用它。这是因为 Box<T> 实现了 Deref 特性,返回一个 &T 类型的引用。

struct MyStruct {
    value: i32,
}

impl MyStruct {
    fn my_method(&self) {
        println!("Called my_method with value: {}", self.value);
    }
}

fn main() {
    let my_box = Box::new(MyStruct { value: 42 });
    // 这里我们在 Box<MyStruct> 上直接调用了 my_method,而没有手动解引用
    my_box.my_method();
}

在上面的例子中,Rust 为我们自动解引用了 my_boxBox<MyStruct>MyStruct,因此我们可以直接调用 my_method。如果没有自动解引用,我们需要这样写:(*my_box).my_method();

这种自动解引用特别有用,因为它使得与智能指针(如 Box, Rc, Arc 等)或其他实现了 Deref/DerefMut 特性的类型一起工作变得更为简洁。

\