Rust 学习笔记 - 智能指针

345 阅读11分钟

智能指针

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

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

引用使用 & 表示,借用它指向的值,没有其余开销,是最常见的指针类型。

智能指针是这样一些数据结构:行为和指针相似,但有额外的元数据和功能。

引用只借用数据,智能指针很多时候都拥有它所指向的数据。

Box<T>

Box<T> 是最简单的智能指针,允许你在 heap 上存储数据(而不是 stack)。

一个 Box<T> 包含 stackheap 两部分:stack 上存着指针,指针指向 heap 上的数据。Box<T> 除了把数据存入 heap 没有其它的性能开销,也没有其它额外的功能。

为什么 Box<T> 是智能指针,因为它实现了 Deref traitDrop trait

创建

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}

使用场景

  1. 在编译时,某类型的大小无法确定。但使用该类型时,上下文却需要知道它的确切大小。
  2. 当你有大量数据,想移交所有权,但需要确保在操作时数据不会被复制。
  3. 使用某个值时,你只关心它是否实现了特定的 trait,而不关心它的具体类型。

第一种场景举例:递归类型

在编译时,Rust 需要知道一个类型所占的空间大小,而递归类型的大小无法在编译时确定。

比如:有一种叫 Cons List 的数据类型:其数据结构就是这样,每个Cons 都是 <i32, Cons> 这个结构,它的最后一个元素为 <i32, Nil>Nil 是一个终止标记),这种结构类似链表。显然这个数据结构中带着递归,其大小是无法确定的,而 Box<T> 的大小是确定的,它可以解决这个问题。

代码实现:

enum List {
    Cons(i32, List), // 报错 recursive type `List` has infinite size
    Nil,
}

解决错误:使用一些“间接”的指针来存储,Box<T> 是一个指针,Rust 知道它需要多少空间,因为指针的大小不会基于它指向的数据大小变化而变化。

use crate::List::{Cons, Nil};

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

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

Deref Trait

智能指针需要实现 Deref trait,下面来看一下什么是 Deref trait

实现 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);
}

实现 Deref Trait 的一个实例

定义一个自己的智能指针 MyBox<T>,类似 Box<T>。使用 tuple struct 来实现。

use std::ops::Deref;
struct MyBox<T>(T); // tuple struct

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

// 标准库中的 Deref trait 要求我们实现一个 deref 方法

impl<T> Deref for MyBox<T> {
    type Target = T; // 定义一个关联类型
    
    // 该方法借用 self,并返回一个指向内部数据的引用
    fn deref(&self) -> &T {
        &self.0 // 元祖的第一个元素
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y); // rust 隐式将*y 展开为 *(y.deref())
}

函数和方法的隐式解引用转化

隐式解引用转化(Deref Coercion)是为函数和方法提供一种便捷特性。

假设 T 实现了 Deref TraitDeref Coercion 可以把 T 的引用转化为 T 经过 Deref 操作后生成的引用。

当把某个类型的引用传递给函数或方法时,但它的类型与定义的参数类型不匹配,Deref Coercion 就会自动发生。编译器会对 deref 进行一系列调用,来把它转为所需的参数类型。

fn hello(name: &str) {
    println!("Hello, {}", name);
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    // &m 是 &MyBox<String>
    // 调用 deref 方法转化其为 &String
    // &String 也实现了 deref 方法,继续调用将其转化为 &str
    hello(&m);

    hello("Rust");
}

解引用与可变性

可使用 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

Drop Trait

实现 Drop Trait 之后,可以让我们自定义当值将要离开作用域时发生的动作。例如:文件、网络资源释放等。任何类型都可以实现 Drop Trait

Drop Trait 只要求实现 drop 方法,参数为对 self 的可变引用。

Drop Trait 在预导入模块里(prelude)。

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.")
}

输出结果:

CustomSmartPointer created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

很难直接禁用自动的 drop 功能,也没必要,Drop Trait 的目的就是进行自动的释放处理逻辑,Rust 不允许手动调用 Drop Traitdrop 方法(比如:c.drop())。如果想要提前 drop 值,可以调用标准库的 std::mem::drop 函数。

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

输出结果:

Dropping CustomSmartPointer with data `my stuff`!
CustomSmartPointer created.
Dropping CustomSmartPointer with data `other stuff`!

Rc<T>

Rc<T> 引用计数智能指针(reference counting),有时候,一个值会有多个所有者,比如:图这种数据结构。为了支持多重所有权,Rc<T> 追踪所有到值的引用,如果引用数变为 0,该值就可以被清理调了。

Rc<T> 不在预导入模块中,需要手动导入 std::rc::Rc

Rc::clone(&a) 函数会增加引用计数,这个克隆和类型上面的 clone() 方法有区别,很多类型上的克隆会执行数据的深度拷贝操作,而 Rc::clone(&a) 则不会。

Rc::strong_count(&a) 可以获得强引用计数

Rc::weak_count(&a) 可以获得弱引用计数

使用场景

需要在 heap 上分配数据,这些数据被程序的多个部分读取(只读,Rc<T> 通过不可变引用在程序不同部分之间共享数据,如果可变则会造成数据竞争),但是在编译的时候无法确定哪个部分最后使用完这些数据。另外,Rc<T> 只能用于单线程场景。

实例

两个 List 共享另一个 List 的所有权,如图所示:

list.jpg

首先来看一个用 Box<T> 实现的错误示例:

use crate::List::{Cons, Nil};

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

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a)); // 此处会报错:value used here after move
}

在定义 b 的时候 a 已经被移动了,再使用 a 就获取不到所有权了,就会报错。

使用 Rc<T> 来实现:

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

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

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c gose out of scope = {}", Rc::strong_count(&a));
}

输出:

count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c gose out of scope = 2

RefCell<T>

Rc<T> 不同,RefCell<T> 类型代表了其持有数据的唯一所有权。只能用于单线程场景。

RefCell<T>Box<T> 的区别:

Box<T>RefCell<T>
编译阶段强制代码遵守借用规则只会在运行时检查借用规则
否则出现错误否则触发 panic

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

编译阶段运行时
尽早暴露问题问题暴露延后,甚至到生成环境
没有任何运行时开销因借用计数产生些许性能损失
对于大多数场景是最佳选择实现某些特定内存安全场景(不可变环境中修改自身数据)
是 Rust 的默认行为

选择 Box<T>Rc<T>RefCell<T> 的依据:

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

内部可变性(interior mutability)

内部可变性是 Rust 的设计模式之一,它允许你在只持有不可变引用的前提下对数据进行修改。数据结构中使用了 unsafe 代码来绕过 Rust 正常的可变性和借用规则。内部可变性是可变的借用一个不可变的值。

下面是一个通常的情况,不可变的值不能被可变的借用:

fn main() {
    let x = 5;
    let y = &mut x; // 报错:cannot borrow `x` as mutable, as it is not declared as mutable
}

下面再来看一个复杂一点的例子:

// lib.rs
// 定义一个 trait
pub trait Messenger {
    // send 方法,self 的不可变引用和文本消息作为参数
    fn send(&self, mgs: &str);
}

// 定义一个泛型结构体,T 要实现 Messenger 这个 trait
pub struct LimitTracker<'a, T: 'a + Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;
        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quotal!");
        } else if percentage_of_max >= 0.9 {
            self.messenger.send("Urgent warning: You've used up over 90% your quotal!");
        } else if percentage_of_max >= 0.75 {
            self.messenger.send("Warning: You've used up over 75% your quotal!");
        }
    }
}

#[cfg(test)]
mod tests {
    use std::cell::RefCell;

    use super::*;

    struct MockMessenger {
        // sent_messages: Vec<String>,
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                // sent_messages: vec![],
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        // fn send(&mut self, message: &str) {
        fn send(&self, message: &str) {
            // self.sent_messages.push(String::from(message));
            // borrow_mut 方法返回智能指针 RefMut<T>,它实现了 Deref
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);
        // assert_eq!(mock_messenger.sent_messages.len(), 1);
        // borrow 方法返回智能指针 Ref<T>,它实现了 Deref
        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}
// cargo test 运行

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

  • 每次调用 borrow 不可变借用计数加 1,任何一个 Ref<T> 的值离开作用域被释放时不可变借用计数减 1

  • 每次调用 borrow_mut 可变借用计数加 1,任何一个 RefMut<T> 的值离开作用域被释放时可变借用计数减 1

以此技术来维护借用检查规则:任何一个给定时间里,只允许拥有多个不可变借用或一个可变借用,否则就会在运行时 panic。

实现拥有多重所有权的可变数据

Rc<T>RefCell<T> 结合使用来实现一个用用多重所有权的可变数据。

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

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

fn main() {
    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);
}

输出结果:

a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))

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

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

循环引用导致内存泄漏

Rust 虽然提供了很强大的内存安全机制,很难发生内存泄露,但也不是不可能。

制造一个内存泄漏

例如使用 Rc<T>RefCell<T> 就可能创造出循环引用,从而发生内存泄漏:每个项的引用数量不会变成 0,值也就不会被释放掉。

内存泄露的例子:

leak.PNG

use std::{rc::Rc, cell::RefCell};
use crate::List::{Cons, Nil};
#[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());
}

防止内存泄漏的方法

  1. 依靠开发来保证,不能依靠 Rust
  2. 重新组织数据结构:一些引用来表达所有权,一些引用不表达所有权,循环引用中的一部分具有所有权关系,另一部分不涉及所有权关系,而只有所有权关系才影响值的清理
  3. Rc<T> 换成 Weak<T>

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> 实例的清理。

强引用与弱引用对比

强引用(Strong Reference)是关于如何分享 Rc<T> 实例的所有权。

弱引用(Weak Reference)并不表达上述意思。

使用 Weak Reference 并不会创建循环引用:当 Strong Reference 数量为 0 的时候,Weak Reference 会自动断开。

使用 Weak<T> 前需保证它指向的值仍然存在:在 Weak<T> 实例上调用 upgrade 方法,返回 Option<Rc<T>>

实例

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

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

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    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());
}