logo
Updated

Rust Smart Pointer

Rc<T>, the Reference Counted Smart Pointer

在Rust编程语言中,如何处理多个变量共同拥有一个值的情况。通常情况下,值的所有权很明确,但在某些情况下,比如图数据结构中,多个边可能指向同一个节点,这个节点被所有指向它的边共同拥有。为了实现这种多重所有权,Rust提供了一种叫做Rc<T>的类型,这是“引用计数”(reference counting)的缩写。Rc<T>通过跟踪对一个值的引用数量来确定这个值是否还在使用中。如果对一个值的引用数量降到零,那么这个值就可以被清理掉,而不会导致任何引用失效。Rc<T>类型适用于当我们想要在堆上分配一些数据供程序的多个部分读取,并且在编译时无法确定哪个部分最后完成使用时。需要注意的是,Rc<T>类型仅适用于单线程场景,在讨论并发时,会介绍如何在多线程程序中进行引用计数。

Two lists that share ownership of a third list

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

这段 Rust 代码中会出错的原因是在创建变量 bc 的时候,都试图使用已经在之前的代码中被移动(move)的变量 a。在 Rust 中,移动(move)操作会转移所有权,导致原所有权的变量不能再被使用,因此在这里会出现错误。要避免这种情况,可以考虑使用 clone() 方法来创建新的 a 的副本,或者重新设计代码逻辑以避免所有权问题。

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}
fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

RefCell<T> and the Interior Mutability Pattern

内部可变性是Rust语言中的一种设计模式,它允许在存在不可变引用的情况下修改数据,这通常是被Rust的借用规则所禁止的。这种模式通过在数据结构内部使用不安全代码来弯曲Rust通常的变更和借用规则。不安全代码向编译器表明我们手动检查规则,而不是依赖编译器来检查。我们可以在能够确保运行时遵守借用规则的情况下使用内部可变性模式,即使编译器不能保证这一点。涉及的不安全代码随后被封装在安全的API中,而外部类型仍然是不可变的。

Here is a recap of the reasons to choose Box<T>, Rc<T>, or RefCell<T>:

Rc<T> enables multiple owners of the same data; Box<T> and RefCell<T> have single owners.

Box<T> allows immutable or mutable borrows checked at compile time; Rc<T> allows only immutable borrows checked at compile time; RefCell<T> allows immutable or mutable borrows checked at runtime.

Because RefCell<T> allows mutable borrows checked at runtime, you can mutate the value inside the RefCell<T> even when the RefCell<T> is immutable.

Reference Cycles Can Leak Memory

Rust通过其所有权模型来管理内存,其中包括所有权规则、借用检查和生命周期概念,以确保在编译时防止数据竞争和无效引用。Rust的Rc<T>RefCell<T>类型提供了引用计数和内部可变性,使得可以共享数据并在运行时借用可变引用,但这也可能导致循环引用和内存泄漏。

Rust处理内存泄漏的策略并不是直接防止它们的发生,而是提供了工具和模式来帮助程序员避免这些情况。例如,可以使用Weak<T>弱指针来避免循环引用,因为弱指针不会增加引用计数,从而允许循环中的值被丢弃。此外,Rust社区鼓励使用自动化测试、代码审查和其他软件开发实践来识别和修复可能导致内存泄漏的逻辑错误。

总的来说,Rust通过以下方式处理内存泄漏问题:

编译时检查:Rust编译器通过借用检查器强制执行内存安全规则,尽管它不直接防止内存泄漏,但它确保了引用的有效性。

智能指针Rc<T>RefCell<T>等智能指针类型允许多个所有权和运行时可变性,但使用它们时需要更加小心。

弱引用:通过使用Weak<T>类型来创建不增加引用计数的弱引用,从而避免循环引用。

代码组织:重新组织数据结构,使得一些引用表示所有权,而另一些则不表示所有权,这样可以有助于打破潜在的循环。

开发实践:鼓励使用测试、代码审查等方法来识别可能的内存泄漏。

因此,尽管Rust的内存安全保证减少了意外内存泄漏的可能性,但它并不是完全防止内存泄漏。程序员仍需要负责避免编写可能导致内存泄漏的代码。

在Rust中,所谓的“安全的内存泄漏”指的是内存泄漏的情况不会导致未定义行为或程序崩溃。Rust保证了即使发生了内存泄漏,程序的其他部分仍然是安全的,不会读写已经释放的内存,也不会导致数据竞争。这是因为Rust的内存安全规则确保了即便内存没有被正确回收,所有的内存访问依然是有效的。

在Rust中,内存泄漏通常是通过错误地使用Rc<T>RefCell<T>等类型造成的。例如,如果你创建了两个Rc<T>值,它们互相持有对方的强引用(strong reference),那么这两个值就会形成一个引用循环(reference cycle),导致它们的引用计数永远不会降到零,从而无法被自动清理。但是,这种情况下的内存泄漏被认为是“安全”的,因为它不会违反Rust的内存安全原则。

尽管“安全的内存泄漏”不会直接导致程序错误,但它仍然可能是一个问题,因为长期存在的内存泄漏可能会消耗大量内存资源,导致程序效率低下或者在极端情况下耗尽系统内存。因此,在实际开发中,仍然需要通过代码审查、测试和使用弱引用(Weak<T>)等手段来避免内存泄漏。