第N次入门Rust - 12.智能指针


前言

这一篇介绍Rust中常用的智能指针以及如何自定义智能指针~


12.1 智能指针基础概念

  • 指针是一种存储其它数据在内存中地址的数据类型,通过指针类型的值可以像通过索引一样找到其指向数据。
  • “引用” 是Rust中最常见的指针,其具有以下特点:
    • 对值使用&获取指针(当然根据前面的文章中可知这种说法其实不太准确,但使用引用的场景基本都离不开&运算符)
    • 借用它指向的值,但不会拥有指向值的所有权
    • 没有其余开销,与其他智能指针相比的没有额外的元数据;
  • 智能指针指是指满足下列特点的数据结构:
    • 行为和指针一样
    • 有额外的元数据和功能
  • Rust中引用和智能指针的区别:引用只借用数据并没有该数据的所有权,但智能指针很多时候都拥有它所指向的数据的所有权。
  • 引用计数(reference counting)智能指针是一类比较常使用的智能指针类型,Rust中的引用计数智能指针类型其特点包括:
    • 通过记录所有者的数量,使一份数据被多个所有者同时持有;
    • 当再没有任何所有者时自动清理数据;
  • Rust中使用智能指针的例子,比如说StringVec<T>,它们都拥有一下智能指针的特点:
    • 都拥有一片内存区域,且允许用户对其操作;
    • 拥有元数据(例如容量等);
    • 提供额外的功能或保障(例如String保障其数据是合法的UTF-8编码);
  • Rust的标准库中就有一些常用的智能指针类型,比如:
    • Box<T>:用于在堆上分配值;
    • Rc<T>:一个引用计数类型,其数据可以有多个所有者;
    • Ref<T>RefMut<T>:通过 RefCell<T> 访问。( RefCell<T> 是一个在运行时而不是在编译时执行借用规则的类型);

12.2 常用智能指针

这里将介绍几种最基础的智能指针:

智能指针类型简介允许所有者数目何时检查借用规则谁保证借用规则通过被引值是否可变使用场景
Box<T>最普通的智能指针一个(独占所有权)编译期编译器不可变、可变没要求
Rc<T>引用计数智能指针多个(共享所有权)编译期编译器不可变单线程
RefCell<T>内部可变性(外部不可变,内部可变)一个(独占所有权)运行时开发者不可变、可变单线程

12.2.1 Box<T>:最普通的智能指针

  • Box<T>是指向类型为T的堆内存分配值的智能指针,可以通过解引用操作符来获取Box<T>中的T。当Box<T>超过作用域范围时,Rust会自动调用其析构函数,销毁内部对象,并释放所占堆内存。
  • Box<T>会在堆上存储数据,创建语法为Box::new(需要管理的值)
    fn main() {
        let x: Box<i32> = Box::new(5);
        let y: i32 = *x;
        
        println!("x: {}", x);   // 5
        println!("y: {}", y);   // 5
    }
    
  • Box的功能类似C中最普通的指针,只是带上了自动释放内存的功能。
  • 为什么可以实现将栈上的数据变为堆上的数据:实际上Box::new的实现是使用了box关键字,这个关键字曾是一个不稳定的语法,因此通过new()函数创建Box智能指针能更好地兼容不同版本的编译器
  • Box<T>类型是智能指针,因为它实现了:
    • Deref trait ,使Box<T>值允许被当做引用对待;
    • Drop trait ,使Box<T>值离开作用域时,box(Box里指向实际数据的字段)所指项的数据也会被清除;

例子:使用Box实现一个Lisp中的ConsList结构

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

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

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

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

  • Rust提供Rc<T>智能指针来引用计数。
  • Rc<T>允许一个值有多个所有者,引用计数确保了只要还存在所有者,该值既保持有效。每当值共享所有权时,计数会增加一。当计数为零,即所有共享变量离开作用域时,该变量才会被析构。
  • Rc<T>用于希望堆上分配的数据可以供程序的多个部分读取,并且无法在编译时确定哪个部分是最后使用者的场景。
  • Rc<T>是单线程引用计数指针,不是线程安全的类型,不允许将引用计数传递或共享给其它线程。
  • Rc<T>定义于std::rc模块,所以使用前需要先use std::rc;
  • 创建Rc<T>的时候使用Rc::new(需要管理的值)
  • 使用rc值.clone()Rc::clone(&rc值)克隆rc值,克隆以后引用计数增加,克隆出来的rc值地址相同。
  • 用法:
    use std::rc::Rc;
    
    fn main() {
    	let x = Rc::new(5);
    	println!("{:p}: strong_count:{}", x, Rc::strong_count(&x));      // 0x7fb15ec05ae0 strong_count: 1
     	let y = x.clone();
    	println!("{:p}: strong_count:{}", y, Rc::strong_count(&x));      // 0x7fb15ec05ae0 strong_count: 2
        {
            let z = Rc::clone(&x);
            println!("{:p}: strong_count:{}", z, Rc::strong_count(&x));      // 0x7fb15ec05ae0 strong_count: 3
        }
        
        println!("strong_count:{}", Rc::strong_count(&x));      // strong_count: 2
    }
    
  • 当克隆值离开作用域,Rc<T>实例会自动计数减一,当计数为0,说明没有所有者,则清除被引用值。
  • 通过不可变引用,Rc<T>允许在程序的多个部分之间只读地共享数据。如果Rc<T>也允许多个可变引用,则会违反借用规则之一:相同位置的多个可变借用可能造成数据竞争和不一致。

12.2.3 RefCell<T> :处理内部可变性场景

基础用法和说明

  • 内部可变性:允许在不可变引用时也能改变数据,是Rust中的一种设计模式。使用场景:一个值不允许外部对其进行可变操作,但值内部允许进行可变操作。
    • 该模式在数据结构中使用 unsafe 代码来模糊 Rust 通常的可变性和借用规则,所涉及的 unsafe 代码将被封装进安全的 API 中,而外部类型仍然是不可变的;
  • Rust的可变或不可变是针对变量绑定而言的,比如结构体的可变或不可变是指其实例的可变性,而不是某个字段的可变性。
    • 实际开发中,常常需要实例不可变,但某个字段需要可变;
    • 比如二叉搜索树中插入一个节点,那么就需要节点结构体实例的左子节点和右子节点是可变的;
  • RefCell<T>并没有完全绕开借用规则,编译器的借用检查器允许内部可变并在运行时执行借用检查。如果在运行时出现了违反借用的规则,比如有多个可变借用会导致程序错误。RefCell<T>通过将借用规则的检查时期延后至运行时,使修改一个不可变值的内存数据(字段)称为可能,只要修改时仍满足借用规则即可
  • RefCell<T>通过引入少量的性能损耗,但是会使得代码更灵活。
  • RefCell<T>只适用于单线程场景。
  • RefCell<T>提供的borrow方法返回Ref类型的智能指针,borrow_mut方法返回RefMut类型的智能指针。
    use std::cell::RefCell;
    
    fn main() {
        let v: RefCell<Vec<i32>> = RefCell::new(vec![1,2,3,4]);
        println!("{:?}", v.borrow());
        
        v.borrow_mut().push(5);
        println!("{:?}", v.borrow());
    }
    
    // [1,2,3,4]
    // [1,2,3,4,5]
    
  • RefCell<T>会记录当前有效的Ref<T>RefMut<T>智能指针的数量。在任何时候,同一作用域中只允许有多个Ref<T>或一个RefMut<T>
    use std::cell::Ref;
    use std::cell::RefCell;
    
    fn main() {
        let v: RefCell<Vec<i32>> = RefCell::new(vec![1,2,3,4]);
        
        let v_borrow_1: Ref<Vec<i32>> = v.borrow();
        println!("{:?}", v_borrow_1);
        
        let v_borrow_2: Ref<Vec<i32>> = v.borrow();
        println!("{:?}", v_borrow_2);
    }
    
    // [1,2,3,4]
    // [1,2,3,4]
    
  • RefCell<T>常配合Rc<T>来使用。Rc<T>允许数据有多个所有者,但只能提供数据的不可变访问。如果两者结合使用,Rc<RefCell<T>>表面上是不可变的,但利用RefCell<T>的内部可变性可以在需要时修改数据。

Rc<T> 结合 RefCell<T> 实现值拥有多个可变引用

  • 根据借用规则,一个值同一时刻要么可以有多个不可变引用,要么只能有一个可变引用;
  • 为了实现一个值可以有多个可变引用,可以将Rc<T>RefCell<T>结合使用,具体做法为:Rc<RefCell<T>>,则可以使同一份数据可以有多个RefCell<T>引用,每一个RefCell<T>引用又可以对改数据进行可变操作。

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

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

12.3 智能指针的实现

  • 智能指针是一个在 Rust 经常被使用的通用设计模式,很多库都有自己的智能指针,开发者也可以编写属于自己的智能指针。
  • 智能指针通常以struct的方式实现,从广义的角度看只要满足前面提到的智能指针的要求都是智能指针类型。在Rust中,为了能更好地自定义智能指针的实现,提供了DerefDrop两个trait,只要实现了这两个trait,就会被认为是智能指针,这两个trait的作用如下:
    • Deref trait:允许自定义智能指针的值表现得像引用一样,统一对智能指针和引用的操作。比如说指针的值为p,则当使用*p时将会执行Deref中定义的方法(如果需要返回可变引用,需要实现DerefMut trait);
    • Drop trait:允许开发者自定义当智能指针离开作用域时运行的代码;

12.3.1 Deref trait:解引用

  • 实现Deref trait的类型的值val支持使用解引用符*运算,即*val(提供解引用功能)。
  • 智能指针实现了Deref trait,且方法会返回智能指针包装的引用(只读引用)。

Box<T>是如何实现Deref trait的

  • Box<T>是一个实现了Deref trait的tuple struct,并重载了deref(&self) -> &Self::Target方法。
  • 由于Box<T>实现了Deref,当对Box实例使用解引用运算符时,实际上Rust底层会变为对boxIns.derfer()的结果使用解引用。即实现Deref trait,能够自定义结构体解引用运算的目标。
  • deref 方法返回值的引用。 如果deref方法直接返回值而不是值的引用,其值的所有权将被移出self,大部分情况下这并不是开发者想要的。

Deref trait实现示例

use std::ops::Deref;

struct MyBox<T>(T);

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

impl<T> Deref for MyBox<T> {
    type Target = T;
    
    fn deref(&self) -> &T {
        &self.0
    }
}

fn main() {
    let x= 5;
    let y = MyBox::new(x);
    
    assert_eq!(5, x);
    assert_eq!(5, *y);  // *y 等价于 *(y.deref())
}

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

  • 隐式解引用转换(Deref Coercion)是为函数和方法提供的一种边界特性。
  • 假设T实现了Deref trait:隐式解引用转换可以把T的引用转化为T经过deref操作后生成的引用。
  • 当把某类型的引用传递给函数或方法时,但它的类型与定义的参数类型不匹配:
    • 隐式解引用转化就会自动发生;
    • 编译器会对deref进行一系列调用,来把它转换为所需要的参数类型;
    • 在编译时完成,不会有额外的性能开销;
  • 示例:
    use std::ops::Deref;
    
    fn hello(name: &str) {
        println("Hello, {}", name);
    }
    
    fn main() {
        let m = MyBox::new(String::from("Rust"));	// m的类型 MyBox<String>
        hello(&m);		// 1. 由于MyBox实现了Deref,所以 &m 转化为 &String
        hello("Rust");	// 2. 由于String也实现了String,所以 &String 转化为 &str
    }
    
    struct MyBox<T>(T);
    
    impl<T> MyBox<T> {
        fn new(x: T) -> MyBox<T> {
            MyBox(x)
        }
    }
    
    impl<T> Deref for MyBox<T> {
        type Target = T;
        
        fn deref(&self) -> &T {
            &self.0
        }
    }
    

DerefMut trait

  • 可使用DerefMut trait重载可变引用的*运算符。
  • Deref trait重载不可变引用的*运算符。
  • DerefMut trait重载可变引用的*运算符。
  • Rust 在发现类型和 trait 实现满足三种情况时会进行隐式解引用转换:
    • T: Deref<Target=U> 时从 &T&U
    • T: DerefMut<Target=U> 时从 &mut T&mut U
    • T: Deref<Target=U> 时从 &mut T&U

12.3.2 Drop trait:清理资源

  • 实现Drop trait的类型的值在离开作用域时,会自动执行drop方法。
  • 智能指针实现了Drop trait,会在drop方法中执行如释放内存,文件资源或网络资源关闭等操作。
  • 所有权系统会保证引用总是有效的,也保证了drop方法只会在值不再使用时被调用一次。
  • 函数中的值执行drop方法时,会按照变量声明顺序的反顺序执行(因为创建函数参数时需要入栈)。
  • Drop trait 要求实现一个叫做 drop 的方法,它获取一个 self 的可变引用。这个drop函数也叫析构参数(destructor)
    fn drop(&mut self) { }
    
    • Drop位于preclude模块中。
  • Rust不允许开发者显式调用Drop trait的drop方法(因为会发生double free的错误),如果开发者真的要在作用域结束之前强制释放变量的话,需要使用标准库提供的std::mem::drop
  • 示例:
    struct Custom {
        data: String,
    }
    
    impl Drop for Custom {
        fn drop(&mut self) {
            println!("Dropping Custom with data: {}", self.data);
        }
    }
    
    fn main() {
        let str1 = Custom{data: String::from("hello world!")};
        let str2 = Custom{data: String::from("hello result!")};
        
        println!("Custom created");
        println!("str1: {}", str1.data);
        println!("str2: {}", str2.data);
        
        // Custom created
        // str1: hello world!
        // str2: hello rust!
        // Dropping Custom with data: hello rust!
        // Dropping Custom with data: hello world!
    }
    

12.4 引用循环和内存泄漏

  • 使用Rc<T>可能会发生引用循环,造成逻辑上的内存泄漏。
  • 防止内存泄漏的解决办法。
  • 为了避免引用循环,可以将Rc<T>改为Weak<T>
  • 通过调用 Rc::downgrade 并传递 Rc<T> 值的引用来创建其值的 弱引用(weak reference)
    • 调用 Rc::downgrade 时会得到 Weak<T> 类型的智能指针;
    • 不同于将 Rc<T> 值的 strong_count1,调用 Rc::downgrade 会将 weak_count1
    • Rc<T> 类型使用 weak_count 来记录其存在多少个 Weak<T> 引用,类似于 strong_count。其区别在于 weak_count 无需计数为 0 就能使 Rc<T> 实例被清理;
  • 强引用代表如何共享 Rc<T> 值的所有权,但弱引用并不属于所有权关系。它们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为 0 时被打断。
  • 调用 Weak<T> 实例的 upgrade 方法,这会返回 Option<Rc<T>>。如果 Rc<T> 值还未被丢弃,则结果是 Some;如果 Rc<T> 已被丢弃,则结果是 None。因为 upgrade 返回一个 Option<T>,我们确信 Rust 会处理 SomeNone 的情况,所以它不会返回非法指针。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值