Skip to content

16.5 智能指针

Rust 语言提供了所有权、默认 move 语义、借用、生命周期、内部可变性等基础概念。 但这些并不是 Rust 全部的内存管理方式,在这些概念的基础上,我们还能继续抽象、封装更多的内存管理方式,而且保证内存安全。

智能指针(Smart Pointer)是 Rust 中的一种数据结构,它拥有常规指针所具有的功能,即对数据进行引用和解引用,同时还附加上了一些额外的元数据和功能。 智能指针实现了各种 Rust 语言的安全性保障机制,这些机制可以在编译期捕获错误,并能够避免内存泄漏和使用未初始化的值等问题。

Rust 标准库中提供了多种智能指针类型,其中两种最常见的是Box<T>Rc<T>

16.5.1 引用计数

到目前为止,我们接触到的示例中都是一块内存总是只有唯一的一个所有者。当这个变量绑定自身消亡的时候,这块内存就会被释放。引用计数智能指针给我们提供了另外一种选择:一块不可变内存可以有多个所有者,当所有的所有者消亡后,这块内存才会被释放。

Rust 中提供的引用计数指针有std::rc::Rc<T>类型和std::sync::Arc<T>类型。Rc 类型和 Arc 类型的主要区别是:Rc 类型的引用计数是普通整数操作,只能用在单线程中;Arc 类型的引用计数是原子操作,可以用在多线程中。这一点是通过编译器静态检查保证的。Arc 类型的讲解可以参见第四部分相关章节,本章主要关注 Rc 类型。

首先我们用示例展示 Rc 智能指针的用法:

rust
use std::rc::Rc;

struct SharedValue {
    value : i32
}

fn main() {
    let shared_value : Rc<SharedValue> = Rc::new(SharedValue { value : 42 });

    let owner1 = shared_value.clone();
    let owner2 = shared_value.clone();

    println!("value : {} {}", owner1.value, owner2.value);
    println!("address : {:p} {:p}", &owner1.value, &owner2.value);
}

编译运行,结果显示:

txt
value : 42 42
address : 0x13958abdf20 0x13958abdf20

这说明,owner1 owner2 里面包含的数据不仅值是相同的,而且地址也是相同的。这正是 Rc 的意义所在。

从示例中可以看到,Rc 指针的创建是调用Rc::new静态函数,与 Box 类型一致(将来会允许使用 box 关键字创建)。如果要创建指向同样内存区域的多个 Rc 指针,需要显式调用 clone 函数。请注意,Rc 指针是没有实现 Copy trait 的。如果使用直接赋值方式,会执行 move 语义,导致前一个指针失效,后一个指针开始起作用,而且引用计数值不变。如果需要创造新的 Rc 指针,必须手工调用 clone()函数,此时引用计数值才会加 1。当某个 Rc 指针失效,会导致引用计数值减 1。当引用计数值减到 0 的时候,共享内存空间才会被释放。

这没有违反我们前面讲的“内存安全”原则,它内部包含的数据是“不可变的”,每个 Rc 指针对它指向的内部数据只有读功能,和共享引用&一致,因此,它是安全的。区别在于,共享引用对数据完全没有所有权,不负责内存的释放,Rc 指针会在引用计数值减到 0 的时候释放内存。Rust 里面的Rc<T>类型类似于 C++ 里面的shared_ptr<const T>类型,且强制不可为空。

从示例中我们还可以看到,使用 Rc 访问被包含的内部成员时,可以直接使用小数点语法来进行,与T &T Box<T>类型的使用方法一样。原因我们在前面已经讲过了,这是因为编译器帮我们做了自动解引用。我们查一下 Rc 的源码就可以知道:

rust
impl<T: ?Sized> Deref for Rc<T> {
    type Target = T;

    #[inline(always)]
    fn deref(&self) -> &T {
        &self.inner().value
    }
}

可见,Rc 类型重载了“解引用”运算符,而且恰好 Target 类型指定的是 T。这就意味着编译器可以将Rc<T>类型在必要的时候自动转换为&T 类型,于是它就可以访问 T 的成员变量,调用 T 的成员方法了。因此,它可以被归类为“智能指针”。

下面我们继续分析 Rc 类型的实现原理。它的源代码在alloc/rc.rs中,Rc 类型的定义如下所示:

rust
pub struct Rc<T: ?Sized> {
    _ptr: Shared<RcBox<T>>,
}

其中 RcBox 是这样定义的:

rust
struct RcBox<T: ?Sized> {
    strong: Cell<usize>,
    weak: Cell<usize>,
    value: T,
}

其中 Shared 类型我们暂时可以不用管它,当它是一个普通指针就好。目前它还没有稳定,后续可能设计上还会有变化,因此本书就不对它深究了。

同时,它实现了 Clone 和 Drop 这两个 trait。在 clone 方法中,它没有对它内部的数据实行深复制,而是将强引用计数值加 1,如下所示:

rust
impl<T: ?Sized> Clone for Rc<T> {

    #[inline]
    fn clone(&self) -> Rc<T> {
        self.inc_strong();
        Rc { ptr: self.ptr }
    }
}

fn inc_strong(&self) {
        self.inner().strong.set(self.strong().checked_add(1)
.unwrap_or_else(|| unsafe { abort() }));
}

drop方法中,也没有直接把内部数据释放掉,而是将强引用计数值减 1,当强引用计数值减到 0 的时候,才会析构掉共享的那块数据。当弱引用计数值也减为 0 的时候,才说明没有任何 Rc/Weak 指针指向这块内存,它占用的内存才会被彻底释放。如下所示:

rust
unsafe impl<#[may_dangle] T: ?Sized> Drop for Rc<T> {
    fn drop(&mut self) {
        unsafe {
            let ptr = self.ptr.as_ptr();

            self.dec_strong();
            if self.strong() == 0 {
                // destroy the contained object
                ptr::drop_in_place(self.ptr.as_mut());

                // remove the implicit "strong weak" pointer now that we've
                // destroyed the contents.
                self.dec_weak();

                if self.weak() == 0 {
                    Heap.dealloc(ptr as *mut u8, Layout::for_value(&*ptr));
                }
            }
        }
    }
}

从上面代码中我们可以看到,Rc 智能指针所指向的数据,内部包含了强引用和弱引用的计数值。这两个计数值都是用 Cell 包起来的。为什么这两个数字一定要用 Cell 包起来呢?

我们假设,如果不用Cell<usize>,而是直接用 usize 的话,在执行 clone 方法时会出现什么情况。

rust
fn clone(&self) -> Rc<T> {}

大家需要注意的是,这个 self 的类型是&Self,不是&mut Self。但我们同时还需要使用这个共享引用 self 来修改引用计数的值。

所以这个成员必须是具有内部可变性的。反之,如果它们是普通的整数,那么我们就要求使用&mut Self类型来调用clone方法,然而一般情况下,我们都会需要多个 Rc 指针指向同一块内存区域,引用计数值是共享的。如果存在多个&mut 型指针指向引用计数值的话,则违反了 Rust 内存安全的规则。

因此,Rc 智能指针的实现,必须使用“内部可变性”功能。Cell 类型提供了一种类似 C++ 的 mutable 关键字的能力,使我们可以通过不可变指针修改复合数据类型内部的某一个成员变量。

所以,我们可以总结出最适合使用“内部可变性”的场景是:当逻辑上不可变的方法的实现细节又要求某部分成员变量具有可变性的时候,我们可以使用“内部可变性”。Rc 内部的引用计数变量就是绝佳的例子。

多个 Rc 指针指向的共享内存区域如果需要修改的话,也必须用内部可变性。 如在下面的例子中,如果我们需要多个 Rc 指针指向一个 Vec,而且具备修改权限的话,那我们必须用 RefCell 把 Vec 包起来:

rust
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let shared_vec: Rc<RefCell<Vec<isize>>> = Rc::new(RefCell::new(vec![1, 2, 3]));
    let shared1 = shared_vec.clone();
    let shared2 = shared1.clone();

    shared1.borrow_mut().push(4);
    println!("{:?}", shared_vec.borrow());

    shared2.borrow_mut().push(5);
    println!("{:?}", shared_vec.borrow());
}

16.5.2 Cow

在 C++ 语境中,Cow 代表的是 Copy-On-Write,即“写时复制技术”。它是一种高效的资源管理手段。 假设我们有一份比较昂贵的资源,当我们需要复制的时候,我们可以采用“浅复制”的方式,而不需要重新克隆一份新的资源。 而如果要修改复制之后的值,这时候再执行深复制,在此基础上修改。因此,它的优点是把克隆这个操作推迟到真正需要“复制并写操作”的时候发生。

在 Rust 语境中,因为 Copy 和 Clone 有比较明确的语义区分,一般把 Cow 解释为 Clone-On-Write。它对指向的数据可能“拥有所有权”,或者可能“不拥有所有权”。

当它只需要对所指向的数据进行只读访问的时候,它就只是一个借用指针;当它需要写数据功能时,它会先分配内存,执行复制操作,再对自己拥有所有权的内存进行写入操作。 Cow 在标准库中是一个 enum:

rust
pub enum Cow<'a, B: ?Sized + 'a> where B: ToOwned {
    /// Borrowed data.
    Borrowed(&'a B),

    /// Owned data.
    Owned(<B as ToOwned>::Owned)
}

它可以是 Borrowed 或者 Owned 两种状态。如果是 Borrowed 状态,可以通过调用to_mut函数获取所有权。在这个过程中,它实际上会分配一块新的内存,并将原来 Borrowed 状态的数据通过调用 to_owned()方法构造出一个新的拥有所有权的对象,然后对这块拥有所有权的内存执行操作。

Cow 类型最常见的是跟字符串配合使用:

rust
use std::borrow::Cow;

fn remove_spaces<'a>(input: &'a str) -> Cow<'a, str> {
    if input.contains(' ') {
        let mut buf = String::with_capacity(input.len());

        for c in input.chars() {
            if c != ' ' {
                buf.push(c);
            }
        }

        return Cow::Owned(buf);
    }

    return Cow::Borrowed(input);
}

fn main() {
    let s1 = "no_spaces_in_string";
    let result1 = remove_spaces(s1);

    let s2 = "spaces in string";
    let result2 = remove_spaces(s2);

    println!("{}\n{}", result1, result2);
}

在这个示例中,我们使用 Cow 类型最主要的目的是优化执行效率。remove_spaces函数的输入参数是&str类型。 如果输入的参数本来就不包含空格,那么我们最好是直接返回参数本身,无须分配新的内存; 如果输入参数包含空格,我们就只能在函数体内部创建一个新的 String 对象,用于存储去除掉空格的结果,然后再返回去。

这样一来,就产生了一个小矛盾,这个函数的返回值类型用&str类型和String类型都不大合适。

  • 如果返回类型指定为&str类型,那么需要新分配内存的时候,会出现生命周期编译错误。 因为函数内部新分配的字符串的引用不能在函数调用结束后继续存在。

  • 如果返回类型指定为 String 类型,那么对于那种不需要对输入参数做修改的情况,有一些性能损失。 因为输入参数&str类型转为 String 类型需要分配新的内存空间并执行复制,性能开销较大。

这种时候使用 Cow 类型就是不二之选。既能满足编译器的生命周期要求,也避免了无谓的数据复制。Cow 类型,就是优秀的“零性能损失抽象”的设计范例。

C++implementations obey the zero-overhead principle:What you don’t use,you don’t pay for.And further:What you do use,you couldn’t hand code any better.

—— Stroustrup

由于 Rust 中有这套所有权、生命周期的基础,在 Rust 中使用 Cow 这种类型是完全没有风险的,任何可能的内存安全问题,编译器都可以帮我们查出来。 所以,有些时候,自由和不自由是可以相互转化的,语法方面的不自由,反而可能造就抽象水平的更自由。

Cow 类型还实现了 Deref trait,所以当我们需要调用类型 T 的成员函数的时候,可以直接调用,完全无须考虑后面具体是“借用指针”还是“拥有所有权的指针”。 所以我们也可以把它当成是一种“智能指针”。

Released under the MIT License