【跟小嘉学 Rust 编程】十五、智能指针

系列文章目录

【跟小嘉学 Rust 编程】一、Rust 编程基础
【跟小嘉学 Rust 编程】二、Rust 包管理工具使用
【跟小嘉学 Rust 编程】三、Rust 的基本程序概念
【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念
【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据
【跟小嘉学 Rust 编程】六、枚举和模式匹配
【跟小嘉学 Rust 编程】七、使用包(Packages)、单元包(Crates)和模块(Module)来管理项目
【跟小嘉学 Rust 编程】八、常见的集合
【跟小嘉学 Rust 编程】九、错误处理(Error Handling)
【跟小嘉学 Rust 编程】十一、编写自动化测试
【跟小嘉学 Rust 编程】十二、构建一个命令行程序
【跟小嘉学 Rust 编程】十三、函数式语言特性:迭代器和闭包
【跟小嘉学 Rust 编程】十四、关于 Cargo 和 Crates.io
【跟小嘉学 Rust 编程】十五、智能指针

前言

指针是一个包含了内存地址的变量,该内存地址引用或执行了另外的数据。在Rust中最常见的指针类型就是引用。不同的是在Rust中引用被赋予更深的含义就是借用其他变量的值。

主要教材参考 《The Rust Programming Language》


一、智能指针

1.1、智能指针(smart point)

智能指针是一个复杂的数据结构,包含了比引用更多的信息,例如元数据,当前长度,最大可用长度等。

在之前章节实际上我们已经见识过多种智能指针了,例如动态字符串 String 和动态数据 Vec。

智能指针往往是基于结构体实现,它与我们自定义的结构体最大的区别在于它实现了 Deref 和 Drop 特征:

  • Deref:可以让智能指针像引用那样工作,这样你就可以写出同时支持智能指针和引用的代码,例如 *T
  • Drop:允许你就指定智能指针超出作用域后自动执行的代码,例如数据清理等收尾工作

1.2、Box 堆内存分配

在Rust 中,所有值默认都是在栈内存上分配,通过创建 Box<T> 可用把值装箱,使它在堆上分配。Box<T> 是一个智能指针,因为它实现了 Deref trait,它允许Box<T> 值被当作引用对待,当 Box<T> 值离开作用域时,由于它实现了 Drop trait ,首先删除其指向堆堆数据,然后删除自身。

使用场景

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

1.2.1、场景1:堆内存上分配数据


fn main() {
    
    
	let a = Box::new(1);  // Immutable
	println!("{}", a);    // Output: 1
	
	let mut b = Box::new(1);  // Mutable
	*b += 1;
	println!("{}", b);    // Output: 2
}

Box 的主要特性是单一所有权,即同时智能有一个人拥有对其指向数据的所有权,并且同时智能存在一个可变引用或多个不可变引用,这一点与Rust中其他属于堆上的数据行为一致。

1.2.2、场景2: cons list

cons list 是来自 Lisp 语言的一种数据结构。cons list 里面每个成员都包含两个元素:当前项都值和下一个元素。cons list 里的最后一个成员只包含一个 nil 值,没有下一个元素。

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,
}

1.3、Deref 解引用

1.3.1、Deref trait

Deref Trait 允许我们重载解引用运算符 *。实现 Deref 的智能指针可以被当作引用来对待,也就是说可以对智能指针使用 *运算符来解引用。

#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Deref for Box<T> {
    
    
    type Target = T;

    fn deref(&self) -> &T {
    
    
        &**self
    }
}

1.3.2、三种 Deref 转换

在之前,我们讲的都是不可变的 Deref 转换,实际上 Rust 还支持将一个可变的引用转换成另一个可变的引用以及将一个可变引用转换成不可变的引用,规则如下:

当 T: Deref<Target=U>,可以将 &T 转换成 &U,也就是我们之前看到的例子
当 T: DerefMut<Target=U>,可以将 &mut T 转换成 &mut U
当 T: Deref<Target=U>,可以将 &mut T 转换成 &U

1.4、Drop 释放资源

1.4.1、Drop trait

Drop trait 主要作用是释放实现者实例拥有的资源,它只有一个方法 drop。当实例离开作用域时会自动调用该方法,从而调用实现者指定的代码。

#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<#[may_dangle] T: ?Sized> Drop for Box<T> {
    
    
    fn drop(&mut self) {
    
    
        // FIXME: Do nothing, drop is currently performed by compiler.
    }
}

1.4.2、使用 std::mem::drop 来提前 drop

Rust 不允许手动调用 Drop trait 的 drop 方法,但是可以 使用标准库的 std::mem::drop 来提前 drop。

1.5、引用计数智能指针(RC<T>Arc<T>)

1.5.1、RC<T>

RC<T> 主要用于同一个堆上所有分配的数据区域需要多个只读访问的情况,比起使用比起使用 Box<T> 然后创建多个不可变引用的方法更优雅也更直观一些,以及比起单一所有权,Rc<T> 支持多所有权。

Rc 为 Reference Counter 的缩写,即为引用计数,Rust 的 Runtime 会实时记录一个 Rc<T> 当前被引用的次数,并在引用计数归零时对数据进行释放(类似 Python 的 GC 机制)。因为需要维护一个记录 Rc<T> 类型被引用的次数,所以这个实现需要 Runtime Cost。

use std::rc::Rc;

fn main() {
    
    
    let a = Rc::new(1);
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Rc::clone(&a);
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
    
    
        let c = Rc::clone(&a);
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

需要注意

  • RC<T> 是完全不可变,可以理解为同一个内存上的数据同时存在多个只读指针
  • RC<T> 只适用单线程,尽管从概念上讲不同线程间只读指针是完全安全的,但是由于 RC<T> 没有实现多个线程间保证计数一致性,如果你尝试多线程内使用,会报错;

1.5.1、原子引用计数(Atomic reference counter)

此时引用计数就可以在不同线程中安全的被使用了。

use std::thread;
use std::sync::Arc;

fn main() {
    
    
    let a = Arc::new(1);
    thread::spawn(move || {
    
    
        let b = Arc::clone(&a);
        println!("{}", b);  // Output: 1
    }).join();
}

1.6、Cell 与 RefCell 内部可变性

1.6.1、内部可变性(interior mutability)

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

1.6.2、Cell

Cell 和 Refcell 在功能上没有区别,区别在于 Cell 适用于 T 实现 Copy 的情况

1.6.3、RefCell

由于 Cell 类型针对的是实现了 Copy 特征的值类型,因此在实际开发中,Cell 使用的并不多,因为我们要解决的往往是可变、不可变引用共存导致的问题,此时就需要借助于 RefCell 来达成目的。

Rust 规则 智能指针带来的额外规则
一个数据只有一个所有者 Rc/Arc 让一个数据可以拥有多个所有者
要么多个不可变借用,要么一个可变借用 RefCell 实现编译器可变、不可变引用共存
违背规则导致编译错误 违背规则导致运行时 panic

可以看出,Rc/Arc 和 RefCell 合在一起,解决了 Rust 中严苛的所有权和借用规则带来的某些场景下难使用的问题。但是它们并不是银弹,例如 RefCell 实际上并没有解决可变引用和引用可以共存的问题,只是将报错从编译期推迟到运行时,从编译器错误变成了 panic 异常:

1.6.4、Cell 和 RefCell

  • Cell 只适用于 Copy 类型,用于提供值,而RefCell 用于提供引用
  • Cell 不会panic ,而 RefCell 会
  • Cell 没有额外的性能损耗

从 CPU 来看,损耗如下:

  • 对 Rc 解引用是免费的(编译期),但是 * 带来的间接取值并不免费
  • 克隆 Rc 需要将当前的引用计数跟 0 和 usize::Max 进行一次比较,然后将计数值加 1
  • 释放(drop) Rc 需要将计数值减 1, 然后跟 0 进行一次比较
  • 对 RefCell 进行不可变借用,需要将 isize 类型的借用计数加 1,然后跟 0 进行比较
  • 对 RefCell 的不可变借用进行释放,需要将 isize 减 1
  • 对 RefCell 的可变借用大致流程跟上面差不多,但是需要先跟 0 比较,然后再减 1
  • 对 RefCell 的可变借用进行释放,需要将 isize 加 1

1.6.5、解决借用冲突

在 Rust 1.37 版本中新增了两个非常实用的方法:

  • Cell::from_mut,该方法将 &mut T 转为 &Cell
  • Cell::as_slice_of_cells,该方法将 &Cell<[T]> 转为 &[Cell]

1.7、Weak 和引用循环

1.7.1、引用循环和内存泄漏

Rust 的内存安全机制可以保证很难发生内存泄漏。但是不代表不会内存泄漏。一个典型的例子就是同时使用 Rc 和 RefCell 创建循环引用,最终这些引用的计数都无法被归零,因此 Rc 拥有的值也不会被释放清理。

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的初始化rc计数 = {}", Rc::strong_count(&a));
    println!("a指向的节点 = {:?}", a.tail());

    // 创建`b`到`a`的引用
    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    println!("在b创建后,a的rc计数 = {}", Rc::strong_count(&a));
    println!("b的初始化rc计数 = {}", Rc::strong_count(&b));
    println!("b指向的节点 = {:?}", b.tail());

    // 利用RefCell的可变性,创建了`a`到`b`的引用
    if let Some(link) = a.tail() {
    
    
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("在更改a后,b的rc计数 = {}", Rc::strong_count(&b));
    println!("在更改a后,a的rc计数 = {}", Rc::strong_count(&a));

    // 下面一行println!将导致循环引用
    // 我们可怜的8MB大小的main线程栈空间将被它冲垮,最终造成栈溢出
    // println!("a next item = {:?}", a.tail());
}

如何防止循环引用

  • 开发者去注意细节
  • 使用 Weak

1.7.2、Weak

Weak 类似 RC 但是和 RC持有所有权不同,Weak 不必持有所有权,仅仅保存一份指向数据的弱引用,如果你要想访问数据,需要通过 Weak 指针的 upgrade 方法实现,该方法返回个类型为 Option<Rc<T>> 的值。

所谓弱引用就是不保证引用关系存在,如果不存在,就返回None。

因为 Weak 引用不计入所有权,因此它无法阻止所引用的内存值被释放掉,而且 Weak 本身不对值的存在性做任何担保,引用的值还存在就返回 Some,不存在就返回 None。

Weak RC
不计数 计数
不拥有所有权 拥有值的所有权
不阻止值被释放(drop) 所有权计数归零,才能drop
引用存在返回some,不存在返回None 引用值必定存在
通过 upgrade 取到Option<Rc<T>> 再取值 通过 Deref 自动解引用,取值无需任何操作

弱引用非常适合如下场景

  • 持有一个 Rc 对象的临时引用,并且不在乎引用的值是否依然存在
  • 阻止 Rc 导致的循环引用,因为 Rc 的所有权机制,会导致多个 Rc 都无法计数归零

1.7.3、unsafe

除了使用 Rust 标准库提供的这些类型,你还可以使用 unsafe 里的裸指针来解决这些棘手的问题,但是由于我们还没有讲解 unsafe。

虽然 unsafe 不安全,但是在各种库的代码中依然很常见用它来实现自引用结构,主要优点如下:

  • 性能高,毕竟直接用裸指针操作
  • 代码更简单更符合直觉: 对比下 Option<Rc<RefCell<Node>>>

总结

以上就是今天要讲的内容

猜你喜欢

转载自blog.csdn.net/fj_Author/article/details/132507528