Rust能力养成系列之(40):内存管理:智能指针

前言

管理原始指针是充满风险的,开发人员在使用它们时需要消息处理很多细节。无意的误用会导致内存泄漏、悬置引用和双重释放(memory leaks, dangling references, and double frees)等问题。为了缓解这些问题,可以使用智能指针(smart pointer),这种方法起源自C++。

Rust中有多种智能指针。之所以称为智能,是因为还有额外的元数据和与之相关的代码,会在创建或销毁时参照和执行这些数据和代码。当智能指针超出作用域时,能够自动释放底层资源,而这是智能指针的主要用途之一。

智能指针的智能之处主要来自两个特性,称为Drop特性和Deref特性。在探索Rust中可用的智能指针类型之前,先详细了解这些特性。

 

Drop

这是我们多次提到的特性,可以在值超出作用域时自动释放所使用的资源。Drop特性类似于其他语言中的对象析构函数方法,它包含一个方法drop,当对象超出作用域时调用该方法。该方法以&mut self作为参数,值所占空间的释放遵循后进先出的原则。看下下面的代码:

// drop.rs

struct Character {
    name: String,
}

impl Drop for Character {
    fn drop(&mut self) {
        println!("{} went away", self.name)
    }
}

fn main() {
    let steve = Character {
        name: "Steve".into(),
    };
    let john = Character {
        name: "John".into(),
    };
}

编译通过,结果如下:

如果需要,drop方法是清理代码的理想工具。对于清理那些不好确定的类型,比如使用计数引用或垃圾收集器,还是很方便的。当我们实例化任何Drop实现值(任何堆分配的类型)时,Rust编译器在编译后的每个作用域结束后插入Drop方法调用。因此,不需要在这些实例上手动调用drop。这种基于作用域的自动回收机制来自于C++的RAII原理。

 

Deref and DerefMut

为了提供与常规指针类似的功能,即能够对所指向的底层类型上的调用方法进行引用解除(dereference),智能指针类型通常对Deref特性进行实现,这就使得开发者可以对这些类型使用*(解除引用操作符)。Deref提供只读访问,而DerefMut,可以提供对底层类型的可变引用。Deref具有以下类型签名:

pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}

上面代码定义了一个名为Deref的方法,该方法接收self的引用,并返回对底层类型的不可变引用。这里面涉及与Rust的deref强制( coercion feature)结合,从而减少了大量必须编写的代码。Deref强制是指一个类型自动从一个类型引用转换到另一个类型引用。我们将在第七章涉及高级概念时,进行讨论。

 

智能指针类型

标准库中的一些智能指针类型如下:

  • Box<T>:提供最简单的堆分配形式,Box类型拥有内部的值,因此可以用于在结构中保存值或从函数中返回值。
  • Rc<T>:用于引用计数。当某处接收一个新的引用时,它会使计数器加1,当释放一个引用时,它会使计数器减1。当计数器达到0时,该值将被删除
  • Arc<T>:用于原子引用计数。这与前一种类型类似,但具有原子性,以保证多线程安全
  • Cell<T>:用于实现Copy特性的类型的内部可变性。换句话说,以此可获得某对象多个可变引用的可能性。
  • RefCell<T>:提供类型的内部可变性,而不需要Copy特性。安全起见,会在运行时进行锁定。

 

Box<T>

标准库中的泛型类型Box为开发者提供了进行堆分配的最简单方法。它只是在标准库中被声明为一个元组结构,把分配的值进行包装并将其放在堆上。Box类型的所有权语义取决于封装的类型( wrapped type)。如果基础类型是Copy,则Box实例变为Copy,否则进行默认移动。

要使用Box创建类型为T的堆分配值,只需调用关联的new方法,并传入值。创建封装类型T的Box值将返回Box实例,它是栈上指向在堆上分配T的指针。下面的示例演示如何使用Box:

// box_basics.rs

fn box_ref<T>(b: T) -> Box<T> {
    let a = b;
    Box::new(a)
}

struct Foo;

fn main() {
    let boxed_one = Box::new(Foo);
    let unboxed_one = *boxed_one;
    box_ref(unboxed_one);
}

在main函数中,我们通过调用Box::new(Foo)在boxed_one中创建了一个堆分配值。Box类型可被可用于创建递归类型定义,比如,下面的Node(节点)类型表示单链表中的节点:

// recursive_type.rs

struct Node {
    data: u32,
    next: Option<Node>
}

fn main() {
    let a = Node { data: 33, next: None };
}

编译时,会出现这样的错误:

意思说不能有这个节点类型的定义,因为next有一个指向自身的类型。如果这个定义是可以允许的,那么编译器就无法停止对节点定义的分析了,因为它会一直对节点定义求值,直到超出内存为止。下面的代码片段可以说明这点:

struct Node {
    data: u32,
    next: Some(Node {
              data: u32,
              next: Node {
                        data: u32,
                        next: ...
                    }
          })
}

如此的话,节点定义的计算将一直进行,直到编译器耗尽内存。此外,由于每个数据在编译时都需要有一个静态已知的大小,这在Rust中是一个不可表示的类型。我们需要使下一个字段具有固定的大小,可以通过将next放在指针后面来实现这一点,因为指针总是固定大小的。如果你看到编译器错误消息,我们将使用Box类型,新节点定义更改如下:

struct Node {
    data: u32,
    next: Option<Box<Node>>
}

可以编译通过,读者可以自行测试。

当定义需要隐藏在Sized indirection的递归类型时,也会使用Box类型。因此,由一个对自身有引用的变量组成的enum可以在以下情况下使用Box类型来隐藏该变量:

  • 当需要将类型存储为trait对象时
  • 当需要在集合中存储函数时

 

结语

本篇着重介绍了智能指针的大略,下一篇介绍一个相对具体的指针类型:引用计数智能指针(Reference counted smart pointers)。

 

主要参考和建议读者进一步阅读的文献

https://doc.rust-lang.org/book

深入浅出 Rust,2018,范长春

Rust编程之道,2019, 张汉东

The Complete Rust Programming Reference Guide,2019, Rahul Sharma,Vesa Kaihlavirta,Claus Matzinger

Hands-On Data Structures and Algorithms with Rust,2018,Claus Matzinger

Beginning Rust ,2018,Carlo Milanesi

Rust Cookbook,2017,Vigneshwer Dhinakaran

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值