(十三)Rust_智能指针


智能指针

前言

本章节主要介绍 Rust 中“指针”的相关概念,包含智能指针以及其与引用的区别。

介绍标准库中常见的智能指针:

  • Box<T>:在 heap 内存上分配值。
  • Rc<T>:启用多重所有权的引用计数类型。
  • Ref<T>RefMut<T>,通过 RefCell<T> 访问:在运行时而不是编译时强制借用规则的类型。

此外还包括:

  • 内部可变模式(interior mutability pattern):不可变类型暴露出可修改其内部值的 API。
  • 引用循环(reference cycles):它们如何泄露内存,以及如何防止其发生。

智能指针基础

  • 指针:一个变量在内存中包含的是一个地址(指向其它数据)。

  • Rust 中最常见的就是“引用”。

    • 使用 &。
    • 借用它指向的值。
    • 没有其余开销。
    • 最常见的指针类型。
  • 智能指针:一种行为与指针相似的数据结构,有额外的元数据和功能。

  • 引用计数智能指针类型:类似 C++ 中的 share ptr,通过记录所有使用者的数量,使一份数据被多个所有者同时持有。

    • 并在没有任何使用者时自动清理数据。
  • 引用与智能指针的不同:

    • 引用:只借用数据。
    • 智能指针:很多时候都是拥有它所指向的数据。
  • 当前使用过的智能指针例子:String 和 Vec<T>

    • 都拥有一片内存区域,且允许用户对其操作。
    • 还拥有元数据(例如容量等)。
    • 提供额外的功能或保证。

智能指针的实现

  • 智能指针通常使用 struct 实现,并且实现了:
    • Deref 和 Drop 这两个 trait。
    • Deref trait:允许智能指针 struct 的实例像引用一样使用。
    • Drop trait:允许你自定义当智能指针实例走出作用域时的代码。

使用 Box<T> 来指向 Heap 上的数据

  • Box<T> 是最简单的智能指针:
    • 允许你在 heap 上存储数据。
    • stack 上是指向 heap 数据的指针。
    • 没有性能开销。
    • 没有其它额外功能。
    • 作为智能指针其实现来 Deref 和 Drop trait。

Box 常见的使用场景

  1. 在编译时无法确定数据大小,但使用该类型,上下文却需要知道它确切但大小。
  2. 当有大量的数据想要移交所有权,但需要确保在操作时数据不会被复制。
  3. 使用某个值时,你只关心它是否实现来特定但 trait,而不管线它但具体类型。
fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}

使用 Box 赋能递归类型

  1. 在编译时,Rust 需要知道一个类型所占但空间大小。
  2. 而递归类型但大小无法再编译时确定(递归层级无法确保)。
  3. Box 的类型就相当于一个指针,它的大小是确定的使用这个特性可以实现递归。

Cons List

  • Cons List 是来自 Lisp 语言的一种数据结构。
  • Cons List 里每个成员由两个元素组成:
    • 当前元素值。
    • 下一个元素。
  • Cons List 里最后一个成员只包含一个 nil 值,没有下一个元素。

Cons List

  • Rust 中 Cons List 并不是常用的集合,通常使用 Vec<T>。接下来尝试使用 Vec<T> 模拟 Cons List。

    • 通常 Rust 计算非递归的数据结构大小通常为选取其最大成员大小。

      /*
      会提示递归存在无限的大小,导致编译失败。
      Rust 无法计算递归类型所占用的空间。
      error[E0072]: recursive type `List` has infinite size
      --> src/main.rs:7:1
      |
      7 | enum List {
      | ^^^^^^^^^ recursive type has infinite size
      8 |     Cons(i32, List),
      |               ---- recursive without indirection
      |
      help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
      |
      8 |     Cons(i32, Box<List>),
      |               ++++    +
      */
      use crate::List::{Cons, Nil};
      
      fn main() {
          let list = Cons(1, Cons(2, Cons(3, Nil)));
      }
      
      enum List {
          Cons(i32, List),
          Nil,
      }
      
  • 使用 Box 来获得确定大小的递归类型:

    • Box<T> 是一个指针,其大小是固定的,这样 Rust 在编译期就可以知道这个结构所占用的栈空间大小。
use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

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

Deref Trait

  • 实现 Deref Trait 使我们可以自定义解引用运算符 * 的行为,使得智能指针可以像常规引用一样来处理

解引用运算符

  • 常规引用也是一种指针。

    fn main() {
        let x = 5;
        let y = &x;
    
        assert_eq!(5, x);
        assert_eq!(5, *y);
    }
    
  • Box<T> 当作引用:

    fn main() {
        let x = 5;
        let y = Box::new(x);
    
        assert_eq!(5, x);
        assert_eq!(5, *y);
    }
    

定义自己的智能指针

  • Box<T> 被定义成拥有一个元素的 tuple struct。

    • 如下,当没有实现解引用方法时:

      /*
      error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
      --> src/main.rs:16:19
      |
      16 |     assert_eq!(5, *y);
      |                   ^^
      */
      struct MyBox<T>(T);
      
      impl<T> MyBox<T> {
          /// Creates a new [`MyBox<T>`].
          fn new(x: T) -> MyBox<T> {
              MyBox(x)
          }
      }
      
      fn main() {
          let x = 5;
          // let y = Box::new(x);
          let y = MyBox::new(x);
      
          assert_eq!(5, x);
          assert_eq!(5, *y);
      }
      
  • 标准库中的 Deref trait 要求我们实现一个 deref 方法:

    • 该方法借用 self。

    • 返回一个指向内部数据的引用。

      use std::ops::Deref;
      
      struct MyBox<T>(T);
      
      impl<T> MyBox<T> {
          /// Creates a new [`MyBox<T>`].
          fn new(x: T) -> MyBox<T> {
              MyBox(x)
          }
      }
      // 实现 Deref
      impl<T> Deref for MyBox<T> {
          type Target = T;
          fn deref(&self) -> &Self::Target {
              &self.0
          }
      }
      
      fn main() {
          let x = 5;
          // let y = Box::new(x);
          let y = MyBox::new(x);
      
          assert_eq!(5, x);
          assert_eq!(5, *y); // == *(y.deref())
      }
      

函数和方法的隐式解引用转化(Deref Coercion)

  • 隐式解引用是为函数和方法提供的一种便捷特性。

  • 假设 T 实现来 Deref trait:

    • Deref Coercion 可以把 T 的引用转化为 T 经过 Deref 操作后生成的引用。
  • 当把某类型的引用传递给函数或方法时,但它但类型与定义但参数类型不匹配:

    • Deref Coercion 就会自动发生。

    • 编译器会对 deref 进行一系列调用,来把它转为所需的参数类型。

      • 在编译时完成,没有额外的性能开销。
      use std::ops::Deref;
      
      fn hello(name: &str) {
          println!("Hello, {}", name);
      }
      
      fn main() {
          let m = MyBox::new(String::from("Rust"));
      
          // &MyBox<String> deref  &String
          // &String deref &str
          hello(&m);
          // 如果没有 deref
          hello(&(*m)[..]);
      
          hello("Rust");
      }
      
      struct MyBox<T>(T);
      
      impl<T> MyBox<T> {
          /// Creates a new [`MyBox<T>`].
          fn new(x: T) -> MyBox<T> {
              MyBox(x)
          }
      }
      
      impl<T> Deref for MyBox<T> {
          type Target = T;
          fn deref(&self) -> &Self::Target {
              &self.0
          }
      }
      

解引用与可变性

  • 可使用 DerefMut Trait 重载可变引用的 * 运算符。
  • 在类型和 trait 在下列三种情况发生时,Rust 会执行 deref coercion:
    • T:Deref<Target=U>,允许 &T 转换为 &U。
    • T:DerefMut<Target=U>,允许 &mut T 转化为 &mut U。
    • T:Deref<Target=U>,允许 &mut T 转化为 &U。反过来将不可变转化为 mut 可变是不允许的。

Drop Trait

  • 实现 Drop Trait,可以让我们自定义当值将要离开作用域时发生的动作

    • 例如:文件、网络资源释放等。
    • 任何类型都可以实现 Drop trait。
  • Drop trait 只要求你实现 drop 方法。

    • 参数:对 self 的可变引用。
  • Drop trait 在预导入模块里(prelude)。

// output:
// CustomSmartPointer created
// Dropping CustomSmartPointer with data `other stuff`!
// Dropping CustomSmartPointer with data `my stuff`!

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("my stuff"),
    };
    let d = CustomSmartPointer {
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointer created");
}

使用 std::mem::drop 来提前 drop 值

  • 很难直接禁用自动的 drop 功能,也没有这个必要,Drop trait 的目的就是进行自动的释放处理逻辑。
  • Rust 不允许手动调用 Drop trait 的 drop 方法。但是可以调用标准库的 std::mem::drop 函数,来提前 Drop 值。
/*
可以看出 my stuff 在最前边输出了
output:
    Dropping CustomSmartPointer with data `my stuff`!
    CustomSmartPointer created
    Dropping CustomSmartPointer with data `other stuff`!
*/
struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("my stuff"),
    };
    // c.drop(); error
    drop(c);
    let d = CustomSmartPointer {
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointer created");
}

Rc<T>:引用计数智能指针

  • 有时,一个值会有多个所有者。
  • 为了支持多重所有权:Rc<T>
    • reference couting(引用计数)。
    • 追踪所有到值的引用。
    • 0 个引用:该值可以被清理掉。

Rc<T> 使用场景

  • 需要在 heap 上分配数据,这些数据被程序的多个部分读取(只读),但在编译时无法确定哪个部分最后使用完这些数据的情况。

  • Rc<T> 只能用于单线程场景。

  • Rc<T> 不在预导入模块中(prelude)。

  • Rc::clone(&a) 函数:增加引用计数。

  • Rc::strong_count(&a):获得引用计数。

    • Rc::weak_count 弱引用计数。
  • 例子:

    • 两个 List 共享,另一个 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(3, Box::new(a)); // a 的所有权已经移交到了 b
      }
      
    • 使用 Rc 来实现:

      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)); // 这样 b 不会获得所有权,而是增加引用计数
          let c = Cons(3, Rc::clone(&a));
      }
      
  • Rc::clone() 与类型的 clone() 方法的区别:

    • Rc::clone():增加引用,不会执行数据的深度拷贝操作。
    • 类型的 clone():很多回执行数据的深度拷贝。
  • Rc<T> 通过不可变引用,使得程序中不同部分之间共享只读数据。

RefCell<T> 和内部可变性

内部可变性

  • 内部可变性是 Rust 的设计模式之一,它允许你在只持有不可变引用的前提下对数据进行修改。

    • 数据结构中使用了 unsafe 代码来绕过 Rust 正常的可变性与借用规则。
  • 可变的借用一个不可变的值,如下例中 y 就是借用不可变的 x,这将导致编译错误:

    fn test() {
        let x = 5;
        let y = &mut x;
    }
    
    • 但是有时需要对外部提供是不可变的,但是对于实现内部需要是可变的情况:

RefCell<T>

  • Rc<T> 不同,RefCell<T> 类型代表了其持有数据的唯一所有权。

  • 其与 Box<T> 的区别:

  • 借用规则在不同阶段进行检查比较:

  • RefCell<T>Rc<T> 类似,只能用于单线程的场景。

选择 Box、Rc、RefCell 的依据

Box Rc RefCell
同一数据的所有者 一个 多个 一个
可变性、借用检查 可变、不可变借用(编译时检查) 不可变借用(编译时检查) 可变、不可变借用(运行时检查)

使用 RefCell<T> 在运行时记录借用信息

  • RefCell<T> 会记录当前存在多少个活跃的 Ref<T>RefMut<T> 智能指针:

    • 每次调用 borrow:不可变引用计数加 1。
    • 任何一个 Ref<T> 的值离开作用域被释放时:不可变借用计数减 1。
    • 每次调用 borrow_mut:可变引用计数加 1。
    • 任何一个 RefMut<T> 的值离开作用域被释放时:可变引用计数减 1。
  • 以此技术来维护借用检查规则:

    • 任何一个给定时间里,只允许拥有多个不可变引用或一个可变引用。

Rc<T>RefCell<T> 结合使用

  • 实现一个拥有多重所有权的可变数据:

    #[derive(Debug)]
    enum List {
        Cons(Rc<RefCell<i32>>, Rc<List>),
        Nil,
    }
    
    use crate::List::{Cons, Nil};
    use std::cell::RefCell;
    use std::rc::Rc;
    
    fn main() {
        // Rc 里包含了一个 RefCell 来进行修改
        let value = Rc::new(RefCell::new(5));
        let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
        let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
        let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));
    
        *value.borrow_mut() += 10;
    
        println!("a after = {:?}", a);
        println!("b after = {:?}", b);
        println!("c after = {:?}", c);
    }
    

其他可实现内部可变性的类型

  • Cell<T>:通过复制来访问数据。
  • Mutex<T>:用于实现跨线程情况下的内部可变性模式。

Rust 可能发生内存泄露

  • Rust 的内存安全机制可以保证很难发生内存泄露,但是也不是不可能的。

  • 例如使用Rc<T>RefCell<T> 就可能创造出循环引用,从而发生内存泄露:

    • 每个项的引用计数不会为 0,值也不会被处理掉。
  • 如下例创建了一个循环:

// output:
// a initial rc count = 1
// a next item = Some(RefCell { value: Nil })
// a rc count after b creation = 2
// b initial rc count = 1
// b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
// b rc count after changing a = 2
// a rc count after changing a = 2

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

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

    println!("a initial rc count = {}", Rc::strong_count(&a));
    println!("a next item = {:?}", a.tail());

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    println!("b initial rc count = {}", Rc::strong_count(&b));
    println!("b next item = {:?}", b.tail());

    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("b rc count after changing a = {}", Rc::strong_count(&b));
    println!("a rc count after changing a = {}", Rc::strong_count(&a));

    // Uncomment the next line to see that we have a cycle;
    // it will overflow the stack
    // println!("a next item = {:?}", a.tail());
}

防止内存泄露的解决方法

  • 依靠开发之自觉,不依靠 Rust。
  • 重新组织数据结构:一些引用来表达所有权,一些引用不表达所有权。
    • 循环引用中的一部分具有所有权关系,另一部分不涉及所有权关系。
    • 而只有所有权关系才影响值的清理。

防止循环引用把 Rc<T> 换成 Weak<T>

  • Rc::cloneRc<T> 实例的 strong_count 加 1,Rc<T> 的实例只有在 strong_count 为 0 的时候才会被清理。

  • Rc<T> 实例通过调用 Rc::downgrade 方法可以已创建值的 Weak Reference (弱引用):

    • 返回类型是 Weak<T>
    • 调用 Rc::downgrade 会为 weak_count 加 1。
  • Rc<T> 使用 weak_count 来追踪存在多少 Weak<T>

  • weak_count 不为 0 不影响 Rc<T> 的清理。

  • Weak<T> 使用前需要判断所指向的对象是否还存在:

use std::borrow::Borrow;
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    // 叶子节点需要保存分支信息,才可以进行向上查找,但是由于叶子是所属分支的
    // 需要使用 weakptr 进行保存,避免循环
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    // upgrade 将 weak 提升
    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);
    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

文章作者: Layton
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Layton !
  目录