了解Rust中的智能指针(附代码)

310 阅读9分钟

开发人员在管理Heap或Stack上的数据时可以使用传统的指针方法。然而,使用这些指针方法也有缺点,比如当动态分配的对象没有及时被垃圾收集时,会造成内存泄漏。好消息是,存在更好的内存管理方法,可以自动处理垃圾回收,而且没有运行时间成本,它们被称为智能指针。

Rust是一种开源的、低级的、面向对象的、静态类型的编程语言,具有高效的内存管理,可以确保高性能和安全性。它具有广泛的功能,许多组织使用它来构建高度安全和健壮的应用程序,包括网络、移动、游戏和网络应用程序。

本文将让你了解什么是智能指针,它们的使用情况,以及它们在Rust中的实现。其中包含的代码示例将教你了解Rust的各种类型的智能指针。

什么是智能指针?

智能指针是一种抽象的数据类型,在编程中的作用与普通指针(存储数值的内存地址的变量)类似,再加上额外的功能,如析构器和重载运算符。它们还包括自动内存管理,以解决内存泄漏等问题。

当开发者将包含动态分配的数据的内存与智能指针链接时,它们会被自动去掉分配或清理。

一些智能指针的使用案例包括:

  • 自动取消分配数据和销毁对象
  • 检查超过其界限的数据或变量
  • 减少与使用普通指针有关的错误
  • 在取消数据分配后保持程序的效率
  • 追踪程序的数据/对象/变量的所有内存地址
  • 管理程序应用中的网络连接

智能指针如何在Rust中工作

Rust通过一个名为所有权的系统(或一组规则)来实现内存管理,该系统包含在应用程序的程序中,并在程序成功编译前由编译器检查,不会造成任何停机。

通过使用结构体,Rust可以执行智能指针。在前面提到的智能指针的额外能力中,它们还具有拥有值本身的能力。

接下来,你会了解到一些有助于在Rust中自定义智能指针操作的特质。

Deref 特质

Deref特质用于有效的解除引用,使人们能够方便地访问存储在智能指针后面的数据。你可以使用Deref 特质来处理智能指针作为一个引用。

解除引用运算符意味着使用单数运算符* ,作为由指针派生的内存地址的前缀,单数引用运算符& ,标记为 "引用"。该表达式可以是可变的 (&mut) ,也可以是不可变的 (*mut)。在内存地址上使用解除引用操作符会返回来自指针点的值的位置。

因此,Deref 特质只是定制了解除引用操作符的行为。

下面是一个关于Deref 特质的说明:

fn main() {
        let first_data = 20;
        let second_data = &first_data;
        
        if first_data == *second_data {
                println!("The values are equal");
      } else {
             println!("The values are not equal");
     }
}

上面代码块中的函数实现了以下内容:

  • 20 的值存储在一个first_data 变量中
  • second_data 变量使用引用操作符& 来存储first_data 变量的内存地址
  • 一个检查first_data 的值是否等于second_data 的值的条件。在second_data 上使用解除引用操作符* ,以获得存储在指针内存地址中的值。

下面的截图显示了该代码的输出:

Code Output Of Deref Trait

Drop 特质

Droptrait与Deref trait相似,但用于解构,Rust通过清理程序不再使用的资源自动实现解构。因此,Drop trait被用于存储未使用值的指针,然后将该值在内存中占用的空间去掉。

要使用Drop 特质,你需要用一个可变的引用来实现drop() 方法,对不再需要或超出范围的值执行销毁,定义为:

fn drop(&mut self) {};

为了更好地理解Drop 特质是如何工作的,请看下面的例子:

struct Consensus  {
        small_town: i32
}

impl Drop for Consensus {
        fn drop(&mut self) {
                println!("This instance of Consensus has being dropped: {}", self.small_town);
}
}

fn main() {
        let _first_instance = Consensus{small_town: 10};
        let _second_instance = Consensus{small_town: 8};

        println!("Created instances of Consensus");
}
The code above implements the following:
  • 创建了一个包含32位有符号整数类型的值的结构,称为small_town
  • Drop 特质包含了带有可变引用的drop() 方法,是用 impl关键字在结构上实现。当main() 函数内的实例超出范围时(也就是main() 函数内的代码运行完毕时),println! 语句内的信息将被打印到控制台中
  • main() 函数只是创建了两个Consensus的实例,并在创建后将println! 内的信息打印到屏幕上

下面的截图显示了该代码的输出:Output Code Of Drop Trait

Rust中智能指针的类型和它们的使用情况

Rust中存在几种类型的智能指针。在本节中,你将通过代码实例了解其中的一些类型和它们的使用情况。它们包括:

  • Rc<T>
  • Box<T>
  • RefCell<T>

Rc<T> 智能指针

[Rc<T>](https://doc.rust-lang.org/book/ch15-04-rc.html)代表了引用计数的智能指针类型。在Rust中,每个值每次都有一个所有者,一个值有多个所有者是违反所有权规则的。然而,当你声明一个值并在代码中的多个地方使用它时,引用计数类型允许你为你的变量创建多个引用。

顾名思义,引用计数的智能指针类型会记录你在代码中对每个变量的引用数量。当引用的计数返回到零时,它们就不再被使用了,智能指针就会将它们清理掉。

在下面的例子中,你将创建三个列表,与一个列表共享所有权。第一个列表将有两个值,第二和第三个列表将把第一个列表作为它们的第二个值。这意味着后两个列表将与第一个列表共享所有权。你将首先在use 语句中加入Rc<T> 前奏,这将使你获得所有可在代码中使用的 RC 方法。

然后你将

  • enum 关键字定义一个列表,并List{}
  • Cons() 创建一对结构体,以保持一个引用计数的列表的值
  • 为定义的列表声明另一个use 语句
  • 创建一个主函数来实现以下内容。
    • 构建一个新的引用计数的列表作为第一个列表
    • 通过传递第一个列表的引用作为参数,创建第二个列表。使用 clone()函数,它创建了一个新的指针,指向第一个列表中的值的分配。
    • 在每个列表后通过调用 [Rc::strong_count()](https://docs.rs/rc/0.1.1/rc/fn.strong_count.html)函数

在你喜欢的代码编辑器中输入以下代码:

use std::rc::Rc;
 
enum List {
   Cons(i32, Rc<List>), Nil,
}
 
use List::{Cons, Nil};
 
fn main() {
   let _first_list = Rc::new(Cons(10, Rc::new(Cons(20, Rc::new(Nil)))));
   println!("The count after creating _first_list is {}", Rc::strong_count(&_first_list));
   let _second_list = Cons(8, Rc::clone(&_first_list));
   println!("The count after creating _second_list is {}", Rc::strong_count(&_first_list));
   { 
       let _third_list = Cons(9, Rc::clone(&_first_list));
       println!("The count after creating _third_list is {}", Rc::strong_count(&_first_list));
   }
 
   println!("The count after _third_list goes out of scope is {}", Rc::strong_count(&_first_list));
}

在你运行该代码后,结果如下:

Reference Counter Smart Pointer

Box<T> 的智能指针

在Rust中,数据分配通常是在堆栈中完成的。然而,Rust中的一些方法和智能指针的类型使你能够在堆中分配数据。这些类型之一是Box<T> 智能指针;""代表数据类型。要使用Box智能指针在堆中存储一个值,你可以将这段代码:Box::new() 围绕它。例如,假设你要在一个堆中存储一个值:

fn main() {
        let stack_data = 20;
        let hp_data = Box::new(stack_data); // points to the data in the heap
        println!("hp_data = {}", hp_data);  // output will be 20.
}

从上面的代码块中,注意到:

  • stack_data 被存储在一个堆中
  • 盒式智能指针hp_data 被存储在堆中

此外,你可以通过在hp_data 前面使用星号(*)来轻松地取消对存储在堆中的数据的引用。 代码的输出将是:

Output Code Of Box Smart Pointer

RefCell<T> 智能指针

RefCell<T> 是一个智能指针类型,在运行时而不是在编译时执行借用规则。在编译时,Rust的开发者可能会遇到 "借用检查器 "的问题,由于不符合Rust的所有权规则,他们的代码仍然没有被编译。

将一个带值的变量绑定到另一个变量上,并使用第二个变量,这在Rust中会产生一个错误。Rust中的所有权规则确保每个值有一个所有者。你不能在其所有权被转移后使用一个绑定,因为Rust为每个绑定创建一个引用,除非使用Copy 特质

Rust中的借用规则需要以引用的形式借用所有权,你可以对一个资源有一个/多个引用(&T),或者一个可变的引用(&mut T)。

然而,Rust中一种叫做 "内部可变性 "的设计模式允许你用不可变的引用来变异这些数据。RefCell<T> ,用数据中的不安全代码使用这种 "内部可变性 "设计模式,并在运行时执行借用规则。

通过RefCell<T> ,可变和不可变的借阅都可以在运行时检查。因此,如果你的代码中有几个不可变的引用的数据,通过RefCell<T> ,你仍然可以对数据进行变异。

之前,在Rc<T> 部分,你使用了一个实现多个共享所有权的例子。在下面的例子中,你将修改Rc<T> 的代码例子,在定义Cons 时,将Rc<T> 包围在RefCell<T>

#[derive(Debug)]
enum List {
  Cons(Rc<RefCell<i32>>, Rc<List>), Nil,
}
use List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
   let data = Rc::new(RefCell::new(10));
 
   let _first_list = Rc::new(Cons(Rc::clone(&data), Rc::new(Nil)));
 
   let _second_list = Cons(Rc::new(RefCell::new(9)), Rc::clone(&_first_list));
 
   let _third_list = Cons(Rc::new(RefCell::new(10)), Rc::clone(&_first_list));
 
   *data.borrow_mut() += 20;
 
   println!("first list after = {:?}", _first_list);
   println!("second list after = {:?}", _second_list);
   println!("third list after = {:?}", _third_list);
}

上面的代码实现了以下内容:

  • 创建data ,并在Rc<RefCell<i32>> 中定义。Cons
  • 创建_first_list ,其共享所有权为data
  • 创建另外两个列表,_second_list_third_list ,它们的共享所有权为_first_list
  • 调用 borrow_mut()函数(该函数返回 [RefMut<T>](https://doc.rust-lang.org/std/cell/struct.RefMut.html)智能指针),并使用解除引用操作符* 来解除引用Rc<T> ,从RefCell中获得内部值,并改变该值

请注意,如果你不把 #[derive(Debug)]作为你代码中的第一行,你会有以下错误:

Debug Error

一旦代码运行,第一个列表、第二个列表和第三个列表的值就会发生变化:

Mutated List

结论

你已经来到了本文的结尾,在这里你了解了智能指针,包括它们的用例。我们介绍了智能指针在Rust中的工作原理和它们的特性(Deref 特质和Drop 特质)。你还了解了Rust中智能指针的一些类型和使用情况,包括Rc<T>,Box<T>, 和RefCell<T>