Rust学习第十三天——智能指针

本文介绍了Rust编程语言中的指针概念,包括引用和智能指针如Box。Box用于在堆上存储数据,实现了Deref和Droptrait,常用于递归类型。另外,文章还讨论了Rc引用计数智能指针以及内部可变性机制的RefCell,展示了它们在多所有权和可变性需求下的应用。
摘要由CSDN通过智能技术生成

介绍

相关概念

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

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

  • 引用:

  • 使用&

  • 借用它指向的值

  • 没有其余的开销

智能指针

  • 智能指针是这样的一些数据结构:

  • 行为和指针相似

  • 有额外的元数据和功能

引用计数(reference counting)智能指针类型

  • 通过记录所有者的数量,使一份数据被多个所有者同时持有

  • 并在没有任何所有者时自动清理数据

引用和智能指针的其他不同

  • 引用:只借用数据

  • 智能指针:很多时候都拥有它所指向的数据

智能指针的例子

  • String和Vec<T>

  • 都拥有一片内存区域,且允许用户对其操作

  • 还拥有元数据(例如容量等)

  • 提供额外的功能或保障(String保障其数据是合法的UTF-8编码)

智能指针的实现

  • 智能指针通常使用struct实现,并且实现了:

  • Deref和Drop这两个trait

  • Deref trait:允许智能指针struct的实例像引用一样使用

  • Drop trait:允许你自定义当智能指针实例走出作用域时的代码

使用Box指向Heap上的数据

使用Box<T>来指向Heap(堆内存)上的数据

Box<T>

  • Box<T>是最简单的智能指针:

  • 允许你在heap上存储数据(而不是stack)

  • stack上是指向heap数据的指针

  • 没有性能开销

  • 没有其他额外的功能

  • 实现了Deref trait和Drop trait

Box<T>的常用场景

  • 在编译时,某类型的大小无法确定。但使用该类型时,上下文却需要知道它的确切大小。

  • 当你有大量数据,想移交所有权,但需要确保在操作时数据不会被复制。

  • 使用某个值时,你只关心它是否实现了特定的trait,而不是它的具体类型。

使用Box<T>在Heap上存储数据

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

使用Box 赋能递归类型

  • 在编译时,Rust需要知道一个类型所占的空间大小。

  • 而递归类型的大小无法在编译时确定

  • 但Box类型大小确定

  • 在递归类型中使用Box就可以解决上述问题

  • 函数语言中的Cons list

关于Cons List

  • Cons List是来自Lisp语言的一种数据结构。

  • Cons List里每一个成员由两个元素组成。

  • 当前项的值

  • 下一个元素

  • Cons List里最后一个成员只包含一个Nil值(相当于终止的标记),没有下一个元素

Cons List并不是Rust的常用集合

  • 通常情况下,Vec<T>是更好的选择

  • 创建一个Cons List

  • Try:

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

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

enum List {
    Cons(i32, List),
    Nil,
}
  • Rust如何确定为枚举分配的空间大小

enum Message {
    Quit,
    Move {x:i32, y:i32},
    Write(String),
    ChangeColor(i32, i32, i32),
}

使用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,
}
  • Box<T>:

  • 只提供了“间接”存储和heap内存分配的功能

  • 没有其他额外功能

  • 没有性能开销

  • 适用于需要“间接”存储的场景,例如Cons List

  • 实现了Deref trait和Drop trait

如果rust结构体包含自身的时候最好用引用是吗?如果必须拥有成员的所有权就得使用box这种?

Deref Trait

  • 实现Deref Trait使我们可以自定义解引用运算符*的行为。

  • 通过实现Deref,智能指针可像常规引用一样来处理

解引用运算符

  • 常规引用是一种指针

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);  // *是解引用符号
}

把Box<T>当做引用

  • Box<T>可以替代上例中的引用

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

    assert_eq!(5, x);
    assert_eq!(5, *y);  // *是解引用符号
}

定义自己的智能指针

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

  • (例子)MyBox<T>

实现Deref Trait

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

  • 该方法借用self

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

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; // 定义了Deref这个trait的关联类型

    fn deref(&self) -> &T {
        &self.0
    }
}

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

    assert_eq!(5, x);
    assert_eq!(5, *y);  // *是解引用符号
    // *(y.deref())
}

(Deref本质是引用的转换,把自身的引用转换成内部数据的引用。)

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

  • 隐式解引用转化(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"));

    // &m &MyBox<String>
    // deref &String
    // deref &str


    hello(&m);
    // hello(&(*m)[ .. ]);
}

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; // 定义了Deref这个trait的关联类型

    fn deref(&self) -> &T {
        &self.0
    }
}

解引用与可变性

  • 可使用DerefMut trait重载可变引用的*运算符

  • 在类型和trait在下列三种情况发生时,Rust会执行deref coercion:

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!("CustomSmartPointers created.");
}

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

  • 很难直接禁用自动的drop功能,也没必要

  • Drop trait的目的就是进行自动的释放处理逻辑

  • Rust不允许手动调用Drop trait的drop方法

  • 但可以使用标准库的std::mem::drop函数,来提前drop值

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

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

  • 有时,一个值会有多个所有者

  • 为了支持多重所有权:Re<T>

  • reference counting(引用计数)

  • 追踪所有值得引用

  • 0个引用:该值可以被清理掉

Re<T>使用场景

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

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

例子

  • Re<T>不在预导入模块(prelude)

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

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

  • 还有Re::weak_count函数

  • 例子

  • 两个List共享另一个List所有权

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

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

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10,
    Rc::new(Nil)))));
    
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}
use crate::List::{Cons, Nil};
use std::rc::Rc;

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 goes out of scope = {}",
    Rc::strong_count(&a));
}

Rc<T>

  • Rc<T>通过不可变引用,使你可以在程序不同部分之间共享只读数据

RefCell<T>和内部可变性

内部可变性

  • 内部可变性时Rust的设计模式之一

  • 它允许你在支持有不可变引用的前提下对数据进行修改

  • 数据结构中使用了unsafe代码来绕过rust正常的可变性和借用规则

RefCell<T>

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

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

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

RefCell<T>也只能用于单线程场景

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

内部可变性:可变的借用一个不可变的值

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

  • 两个方法(安全接口)

  • borrow方法

  • 返回智能指针Ref<T>,它实现了Deref

  • borrow_mut方法

  • 返回智能指针RefMut<T>,它实现了Deref

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

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

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

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

  • Cell<T>:通过复制来访问数据

  • Mutex<T>:用于实现跨线程情形下的内部可变性模式

循环引用可导致内存泄漏

Rust可能发生内存泄漏

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

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

  • 每个项的引用数量不会变成0,值也不会被处理掉。

  • 例子:

防止内存泄漏的解决办法

  • 依靠开发者来保证,不能依靠Rust

  • 重组数据结构:一些引用来表达所有权,一些引用不表达所有权

  • 循环引用中的一部分具有所有权关系,另一部分不涉及所有权关系

  • 而只有所有权关系才影响值得清理

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

  • Rc::clone为Rc<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 Vs Weak

总结

接着奏乐接着舞。

所谓成长,不过是用时间慢慢擦亮你的眼睛,少时看重的,年长后却视若鸿毛,少时看轻的,年长后却视若泰山,成长之路,亦是渐渐放下执念,内心归于平静的旅程。也许,我们永远都不会知道自己走向何方,遇见何人,最后会变成什么样的人,但请一定要记住,能让自己登高的,永远不是别人的肩膀。人生的道路刚刚启程,接下来的旅程全由你自己选择,而当你累了倦了也不要迷茫,回头看一看,家里的大门永远为你敞开。
--萧炎之父萧战
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值