每日一R「10」数据结构(一)智能指针

330 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详情

在前面学习所有权时,我们学习了几个常用的智能指针,包括 Rc、Box、RefCell 等。智能指针是 Rust 中最容易令人感到困惑的一种数据结构。今天我们将跟着大佬一块再深入学习这块内容。

首先,我们先来回顾下 Rust 中指针类型。

01-指针类型

Rust 中的指针类型可以分为三类:

  1. 引用,包括共享(不可变)引用和可变引用。共享引用写作&type&'a type,实现了 Copy trait。可变引用写作&mut type&'a mut type,未实现 Copy trait。

  2. 裸指针,写作*const T*mut T。Rust 并不为裸指针提供安全性和生命周期保证。对裸指针进行解引用是 unsafe 操作。可以通过 reborrow(&*&mut *) 将裸指针转换为引用。在编写程序时,是不推荐使用裸指针的。Rust 在设计时允许裸指针存在的目的主要是为了支持与其他语言代码的互操作性、或者是对极致性能的追求以及编写一些底层的函数。

    裸指针进行比较时,比较得是它们的地址,而不是它们指向了什么。当裸指针指向动态大小的类型时,也会比较它们指向的内容。

  3. 智能指针,是一种特殊的数据结构,用法与其他指针类型类似,包含了额外的元数据信息。或者按照课程讲义中的说法,任何实现了特定 trait 的数据结构都可以称之为智能指针(smart pointer)。

    在 Rust 中,凡是需要做资源回收的数据结构,且实现了 Deref/DerefMut/Drop,都是智能指针。

02-动态大小类型

Rust 中大多数类型都具有编译时可知的固定大小,换句话说,大部分类型实现了 Sized trait。如果一个类型的大小只能在运行时才能知道,则称其为动态大小的类型(dynamically sized type, DST)。我们在学习泛型是有看到?Sized用法,意为动态大小的类型。

常见的 DST 有:切片类型、trait object,它们仅可在有限的场景下使用

  1. 指向动态大小类型的指针类型(引用、裸指针或智能指针)的大小是固定的,而且是指向固定长度类型的指针类型的两倍大。
    • 指向切片类型的指针,除了指针外,还包含切片中元素的数量;
    • 指向 trait object 的指针,除了指向类型的指针,还包含指向 vtable 的指针
  2. 泛型参数或 trait 关联类型的 bound 为?Sized时,可使用 DST。
  3. 可以为 DST 实现 trait,不同之处在于Self: ?Sized
  4. 如果结构体的最后一个 field 是 DST,那么该结构体本身也将是一个 DST。

注:变量、函数参数、const 项和 static 项必须是实现了 Sized trait 的。

03-三个重要的智能指针

03.1-Box

Box 是用于堆分配的指针类型(智能指针)。

pub struct Box<T: ?Sized,A: Allocator = Global,>(Unique<T>, A);

Box 是在 C++ unique_ptr 智能指针基础上发展而来的,其内部包含了一个 Unique 用来致敬 C++。Unique 是一个私有的数据结构,它内部封装了一个 NonNull 类型的指针,而 NonNull 内部封装了一个裸指针。

pub struct Unique<T: ?Sized> {
    pointer: NonNull<T>,
    // PhantomData consumes no space, 
		// but simulates a field of the given type for the purpose of static analysis.
    // ref: https://doc.rust-lang.org/nomicon/phantom-data.html
    _marker: PhantomData<T>,
}
pub struct NonNull<T: ?Sized> {
    pointer: *const T,
}

Allocator trait

  • allocate: 分配堆内存
  • deallocate: 回收堆内存
  • grow / shrink,扩大或缩小堆上已分配的内存

既然是 Box 是一个智能指针,按照前面的说法,它必然实现了 Deref / DerefMut / Drop:

impl<T: ?Sized, A: Allocator> const Deref for Box<T, A> {
    type Target = T;

    fn deref(&self) -> &T {
        &**self
    }
}
impl<T: ?Sized, A: Allocator> const DerefMut for Box<T, A> {
    fn deref_mut(&mut self) -> &mut T {
        &mut **self
    }
}

unsafe impl<T: ?Sized, A: Allocator> Drop for Box<T, A> {
    fn drop(&mut self) {
        // FIXME: Do nothing, drop is currently performed by compiler.
    }
}

注:关于上面的 deref 方法,有个地方其实很难理解。例如,Box::new(1)指在堆上分配一块内存,存储 i32 值1。从上面的定义中我们知道,Box结构其实是一个 tuple,它包含两个元素,这点我们也可以通过编辑器来印证:

Untitled.png

我们可以看到,确实有两个 field,只不过是私有的,无法访问;第3个元素直接报不存在了。根据定义,我们可以知道 x.0 是一个 Unique 类型的值,x.1 是一个 Allocator 类型的值,默认为 Global。对Box::new(1)来说,它的结构应该是({pointer: {pointer: *const i32}}, Global)。这里我比较难理解的是 deref 中的第一个 * 是如何做到对 *const i32 这个裸指针进行解引用的。(有了解更多细节的小伙伴,欢迎在评论区留下你的高见)

使用 Box::new 是需要特别注意,传入该方法的数据会先出现在栈上,之后再移动到堆上。所以,如果传递过大的数据,例如Box::new([0u8; 1<<24]),在堆上分配16M内存,使用 cargo run --debug 运行时并不会做 inline 优化,所以可能会有问题。使用 cargo run --release 运行时会开启 inline 优化,运行不会有问题。

03.2-Cow<’a, B>

Cow 是 Rust 中提供的具备写时拷贝(Copy-on-Write)的智能指针,它是一个枚举值,包含了两类值:

pub enum Cow<'a, B> where B: 'a + ToOwned + ?Sized {
  Borrowed(&'a B),    // 包含了类型 B 的一个只读借用
  Owned(<B as ToOwned>::Owned),    // 类型 B 必须实现了 ToOWned trait
}

这里引入了一个之前未接触过的 ToOwned trait。它主要的内容包括一个关联类型 Owned 和 一个方法 to_owned:

pub trait ToOwned {
    type Owned: Borrow<Self>;  // 关联类型必须实现了 Borrowed trait,
                               // 这里的 Self 其实是上面定义 enum 是的类型 B
    #[must_use = "cloning is often expensive and is not expected to have side effects"]
    fn to_owned(&self) -> Self::Owned;  // 将当前类型转换为 Owned 类型
		...
}

ToOwned trait 又引入了其他的 trait:

pub trait Borrow<Borrowed> where Borrowed: ?Sized {
    fn borrow(&self) -> &Borrowed;   // 生成一个引用
}

接下来,我们通过一个示例,来学习下 Cow 的使用:

let cow: Cow<str> = Cow::Borrowed("hello");

我们先看下 str 有没有实现 ToOwned trait:

impl ToOwned for str {
    type Owned = String;
    #[inline]
    fn to_owned(&self) -> String {
        unsafe { String::from_utf8_unchecked(self.as_bytes().to_owned()) }
    }
...
}

根据 Cow 定义,关联类型 Owned 必须实现 Borrow trait,这里的关联类型是 String,我们在看下 String 是如何实现 Borrow trait 的。

impl Borrow<str> for String {
    #[inline]
    fn borrow(&self) -> &str {
        &self[..]
    }
}

既然 Cow 是一个智能指针,根据前面学习的内容,它肯定实现了 Deref trait:

impl<B: ?Sized + ToOwned> const Deref for Cow<'_, B>
where
    B::Owned: ~const Borrow<B>,
{
    type Target = B;

    fn deref(&self) -> &B {
        match *self {
            Borrowed(borrowed) => borrowed,
            Owned(ref owned) => owned.borrow(),
        }
    }
}

对 Cow 类型的数据进行解引用,会获得一个只读借用。

03.3-MutexGuard

MutexGuard 在调用 Mutex::lock 方法时产生。它首先会取得锁资源,若无法取得,则等待;若取得成功,则把 Mutex 的引用传递给 MutexGuard。我们来看下它的代码实现:

pub fn lock(&self) -> LockResult<MutexGuard<'_, T>> {
    unsafe {
        self.inner.raw_lock();
        MutexGuard::new(self)
    }
}

MutexGuard 是一个智能指针,它内部包含了一个到 Mutex 的引用以及一个 Guard。

// 这里用 must_use,当你得到了却不使用 MutexGuard 时会报警
#[must_use = "if unused the Mutex will immediately unlock"]
pub struct MutexGuard<'a, T: ?Sized + 'a> {
    lock: &'a Mutex<T>,
    poison: poison::Guard,
}

作为一个智能指针,它实现了 Deref 和 Drop trait:

impl<T: ?Sized> Deref for MutexGuard<'_, T> {
    type Target = T;

    fn deref(&self) -> &T {
        unsafe { &*self.lock.data.get() }
    }
}

impl<T: ?Sized> DerefMut for MutexGuard<'_, T> {
    fn deref_mut(&mut self) -> &mut T {
        unsafe { &mut *self.lock.data.get() }
    }
}

impl<T: ?Sized> Drop for MutexGuard<'_, T> {
    #[inline]
    fn drop(&mut self) {
        unsafe {
            self.lock.poison.done(&self.poison);
            self.lock.inner.raw_unlock();
        }
    }
}

当 MutexGuard 被释放时,它会在 drop 中对锁进行释放,使用者不必关心合适释放这个互斥锁。所有权机制保证这个锁一定会被释放。

本节课程链接:《15|数据结构:这些浓眉大眼的结构竟然都是智能指针?


历史文章推荐