研读Rust圣经解析——Rust learn-12(智能指针)

news2024/11/10 17:45:33

研读Rust圣经解析——Rust learn-12(智能指针)

  • 智能指针
    • 智能指针选择
    • `Box<T>`
      • 使用场景
      • 创建Box
      • 使用Box在堆上存储递归类型数据
        • 解决
    • 通过 Deref trait 将智能指针当作常规引用处理
      • 追踪指针的值
      • 创建自定义的智能指针(*)
    • Deref隐式转换
    • Deref 强制转换如何与可变性交互
    • Drop Trait 运行清理代码
      • 实现Drop
      • 提早drop
    • Rc<T> 引用计数智能指针
      • 注意点
      • RC保证数据共享
    • `RefCell<T> `和内部可变性模式
      • 通过 `RefCell<T> `在运行时检查借用规则
      • 注意点
      • 内部可变性
      • 使用RefCell
      • 原理
    • 结合 Rc<T> 和 RefCell<T> 来拥有多个可变数据所有者

智能指针

指针 (pointer)是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其他数据
前面说的引用就是指针

智能指针(smart pointers)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能。智能指针的概念并不为 Rust 所独有;

智能指针选择

  • Rc<T> 允许相同数据有多个所有者;Box<T>RefCell<T> 有单一所有者。
  • Box<T> 允许在编译时执行不可变或可变借用检查;Rc<T>仅允许在编译时执行不可变借用检查;RefCell<T> 允许在运行时执行不可变或可变借用检查。
  • 因为 RefCell<T> 允许在运行时执行可变借用检查,所以我们可以在即便 RefCell<T> 自身是不可变的情况下修改其内部的值。

Box<T>

是最简单最直接的智能指针,数据会被存储到堆上,且没有任何性能损失,但也没什么额为功能

使用场景

  1. 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
  2. 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
  3. 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候

创建Box

如其他的方式一样都是直接new出来的

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

使用Box在堆上存储递归类型数据

首先我们先创建一个enum List

#[derive(Debug)]
enum List {
    Next(i32, List),
    OFF,
}

这是个递归的List,下一个节点依然可以装List
接下来使用它创建一个变量

#[derive(Debug)]
enum List {
    Next(i32, List),
    OFF,
}

use List::{Next, OFF};

fn main() {
    let list = Next(1, Next(2, OFF));
    println!("{:?}",list);
}

编译之后我们会发现报错,原因是:编译器认为这个类型 “有无限的大小”,所以它不知道要用多少空间存储它,或它认为这个玩意就没办法存
同时他也提供了解决方法:
在这里插入图片描述从这里可以看出,编译器希望我们使用Box来解决

解决

#[derive(Debug)]
enum List {
    Next(i32, Box<List>),
    OFF,
}

use List::{Next, OFF};

fn main() {
    let list = Next(1, Box::new(Next(2, Box::new(OFF))));
    println!("{:?}", list);
}

我们来解释一下为什么用Box就可以了,首先Box是将值存到了堆上,会尝试使用一个尽可能大的空间进行存储,因为 Box<T> 是一个指针,我们总是知道它需要多少空间:指针的大小并不会根据其指向的数据量而改变。因为OFF这个字段不存任何东西,自然它的大小要比Next要小,所以进行了区分,当编译器发现了一个需要空间小于正常递归空间大小的节点的时候就认为这是终止节点(其实我更喜欢叫他叶子节点,因为我常常把递归看作一个树结构)

通过 Deref trait 将智能指针当作常规引用处理

实现 Deref trait 允许我们重载 解引用运算符(dereference operator)*。通过这种方式实现 Deref trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。

追踪指针的值

这里我加了一个打印y这个变量的内存地址,或许我应该说打印y这个指针的内存地址

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

    assert_eq!(5, x);
    assert_eq!(5, *y);
    println!("{:p}",y);
}

y在这里是引用了x的值,所以y相当于一个指针,指针实际就是一个地址,要找到地址中真实的内容就需要使用解引用*
这段程序相当于

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

    assert_eq!(5, x);
    assert_eq!(5, *y);
    println!("{:p}",y);
}

智能指针也是指针这很好理解

创建自定义的智能指针(*)

我们也可以自己去写一个Box<T>其实这并不难,首先我们需要为结构体实现new方法,这很简单,传入一个泛型返回带泛型的结构体即可

use std::ops::Deref;

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

    assert_eq!(5, x);
    assert_eq!(5, *y);
}


struct Smarter<T>(T);

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

impl<T> Deref for Smarter<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

然而,若只是到实现new你是无法实现解引用的,因为它需要实现Deref这个trait,看名字就知道了ref是引用,de反义词前缀,在这个实现中需要我们实现两个东西:

  1. type:你看作一个类型代指(想想TypeScript中的type)后面会讲
  2. deref方法:需要返回引用自身的target,也就是我们上面的类型代指,其实就是返回本身的self中的实际内容(想想unwrap系列的解构)

此时我们自己定义的智能指针就可以使用*进行解引用了

Deref隐式转换

隐式转换是指如:&String会转化为&str,这很常见

let a = String::from("newWord");
let b = &a

正常应该b的类型是&String,但实际上b则是&str,因为 String 实现了 Deref trait 因此可以返回 &str

Deref 强制转换(deref coercions)将实现了 Deref trait 的类型的引用转换为另一种类型的引用
Deref 强制转换是 Rust 在函数或方法传参上的一种便利操作,并且只能作用于实现了 Deref trait 的类型。当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时将自动进行。这时会有一系列的 deref 方法被调用,把我们提供的类型转换成了参数所需的类型。
Deref 强制转换的加入使得 Rust 程序员编写函数和方法调用时无需增加过多显式使用 & 和 * 的引用和解引用。这个功能也使得我们可以编写更多同时作用于引用或智能指针的代码

Deref 强制转换如何与可变性交互

Rust 提供了 DerefMut trait 用于重载可变引用的 * 运算符。

Rust 在发现类型和 trait 实现满足三种情况时会进行 Deref 强制转换:

  1. T: Deref<Target=U> 时从&T&U
  2. T: DerefMut<Target=U> 时从&mut T&mut U
  3. T: Deref<Target=U>时从 &mut T &U

Drop Trait 运行清理代码

对于智能指针模式来说第二个重要的 trait 是 Drop,其允许我们在值要离开作用域时执行一些代码。可以为任何类型提供 Drop trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。

Rust其实没有全自动GC的概念,但是通过实现Drop,编译器会在该回收的时候自动插入回收代码以进行回收,我个人认为这是种半自动GC
相当于,我们手动调用drop函数

实现Drop

如下,我们定义一个结构体,然后让他实现Drop这个trait,在离开作用域之后自动调用drop(当然这是编译器帮我们干的所以我们称为自动,实际上就是编译器帮我们写了drop函数)

struct Smarter {
    data: String,
}

impl Drop for Smarter {
    fn drop(&mut self) {
        println!("drop->{}", self.data);
    }
}

fn main() {
    
    let a = Smarter {
        data: String::from("1"),
    };

    let b = Smarter {
        data: String::from("2"),
    };
}

我们执行一下

drop->2
drop->1

你看到的应该是先2后1,为什么?原因很简单啊,因为栈是后进先出的

提早drop

通过 std::mem::drop我们可以实现提早清理,就是手动drop

fn main() {
    let a = Smarter {
        data: String::from("1"),
    };
    drop(a);
}

这里给出一个其他博主的例子:

fn main() {
    let mut list = A { children: None };
    for _ in 0..1_000_000 { list = A { children: Some(Box::new(list)) }; }
    println!("for complete, list is going to drop");
    drop(list);
    println!("program complete");
}

struct A {
    children: Option<Box<A>>,
}

impl Drop for A {
    fn drop(&mut self) {
        if let Some(mut child) = self.children.take() {
            while let Some(next) = child.children.take() {
                child = next;
            }
        }
    }
}

Rc 引用计数智能指针

大部分情况下所有权是非常明确的:可以准确地知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的节点,而这个节点从概念上讲为所有指向它的边所拥有。节点直到没有任何边指向它之前都不应该被清理因此也没有所有者。

为了启用多所有权需要显式地使用 Rust 类型 Rc<T>,其为 引用计数(reference counting)的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。

在这里插入图片描述
那么Rc实际是一个计数器,当计数器清0的时候那么这个就能被drop了

Rc<T> 用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的哪一部分会最后结束使用它的时候。如果确实知道哪部分是最后一个结束使用的话,就可以令其成为数据的所有者,正常的所有权规则就可以在编译时生效。

注意点

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

RC保证数据共享

现在我们有一个场景,我们有三个链表,a链表的尾部节点是b链表,c链表的尾部节点是b链表

我们首先来写出这个程序(不用Rc):

#[derive(Debug,Clone)]
enum List{
    Next( i32,Box<List>),
    OFF
}


use crate::List::{Next,OFF};

fn main() {
    let b = Next(1,Box::new(Next(2,Box::new(OFF))));
    let a = Next(4,Box::new(b.clone()));
    let c = Next(8,Box::new(b.clone()));

    println!("{:#?}",a);
    println!("{:#?}",b);
    println!("{:#?}",c);
}

测试之后没有任何问题
接下来我们使用Rc改写

#[derive(Debug)]
enum List{
    Next( i32,Rc<List>),
    OFF
}


use std::rc::Rc;
use crate::List::{Next, OFF};

fn main() {
    let b = Rc::new(Next(1,Rc::new(Next(2,Rc::new(OFF)))));
    let a = Next(4,Rc::clone(&b));
    let c = Next(8,Rc::clone(&b));

    println!("{:#?}",a);
    println!("{:#?}",b);
    println!("{:#?}",c);
}

我们看到使用Rc改写之后:

  1. 去除了原来的Box,改用Rc
  2. 去除Clone标注,通过Rc::clone代替

主要产生以上两种改变,其实想法都是clone出b,不获取b的所有权这会将引用计数从 1 增加到 2 并允许 a 和 c 共享 Rc<List>中数据的所有权,每次调用 Rc::cloneRc<List> 中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理

RefCell<T> 和内部可变性模式

内部可变性(Interior mutability)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 unsafe 代码来模糊 Rust 通常的可变性和借用规则。不安全代码表明我们在手动检查这些规则而不是让编译器替我们检查
当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的 unsafe 代码将被封装进安全的 API 中,而外部类型仍然是不可变的。

通过 RefCell<T> 在运行时检查借用规则

RefCell<T> 代表其数据的唯一的所有权

对于引用和 Box<T>,借用规则的不可变性作用于编译时。对于 RefCell<T>,这些不可变性作用于 运行时。对于引用,如果违反这些规则,会得到一个编译错误。而对于 RefCell<T>,如果违反这些规则程序会 panic 并退出。
在运行时检查借用规则的好处则是允许出现特定内存安全的场景,而它们在编译时检查中是不允许的。
要记住Rust是保守的,安全的,这句话就是整个Rust的核心,所有特性都是围绕这句话来的

注意点

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

内部可变性

指的是可变的借用不可变的值
如下的程序是有问题的,因为本身x是不可变的,但是y取了x的可变引用,这本身是不允许的

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

使用RefCell

我们看这个例子,虽然arr是不可变的,但是我们通过包装RefCell之后获取了其所有权,使用borrow_mut方法获取了内部可变性,使其变成可变的,进行了操作,官方的例子很长,或许你没有耐心看完,我想我这样写应该能让你快速的了解

use std::borrow::BorrowMut;
use std::cell::RefCell;


fn do_change(arr: RefCell<Vec<i32>>)->Vec<i32>{
   return arr.borrow_mut().to_vec().iter().map(|x|x+30).collect();

}

fn main() {
    let arr = RefCell::new(vec![1, 2, 3]);
    let new_arr = do_change(arr);
    println!("{:?}", new_arr);
}

原理

使用RefCell<T>在运行时记录借用信息
RefCell<T>会记录当前存在多少个活跃的Ref<T>和 RefMut<T>智能指针:

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

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

结合 Rc 和 RefCell 来拥有多个可变数据所有者

这个例子我认为圣经上写的很好了,解释也很到位,所以大家直接看吧

RefCell<T> 的一个常见用法是与 Rc<T> 结合。回忆一下 Rc<T> 允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了 RefCell<T>Rc<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() {
    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(3)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}

我们可以看到List的Cons是Rc<RefCell<i32>>, Rc<List>这样我们传入一个通过使用Rc::new出来的RefCell,而真实的值类型是i32所以再通过RefCell::new 一下,这样我们可以修改这个传入的i32的值

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/448885.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

手把手实现一个lombok

手把手实现一个lombok 一、lombok原理 JSR269二、实现步骤1.工程与环境依赖注意细节 2.注解处理器3.注解4.jcTree 修改语法4.新建模块依赖我们这个jar包进行编译5.源码调试 一、lombok原理 JSR269 什么是JSR &#xff1f; JSR是Java Specification Requests的缩写&#xff0c…

python 神经网络回归

神经网络回归数据&#xff0c;21条&#xff0c;每条12个月&#xff0c;根据输入预测下一年 数据集&#xff1a; 下载链接 620906209062090620906209062090620906209062090620906209062090660936365969622628916949069597656066534455211622826735957783727886869762952670916…

【远程访问及控制】

目录 一、OpenSSH服务器1.1、SSH远程管理1.2、ssh服务器的端口1.3、修改端口号1.4、设置用户登录 二、实验2.1、设置白名单2.2、设置黑名单 三、sshd 服务支持两种验证方式:3.1、密码验证3.2、密钥对验证公钥和私钥的关系&#xff1a; 四、使用SSH客户端程序4.1、ssh 远程登录4…

java项目之疫情网课管理系统(springboot+vue源码)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的疫情网课管理系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 &#x1f495;&#x1f495;作者&#xff1a;风…

Java集合框架(Collection)

集合框架 一个Java对象可以在内部持有若干其他Java对象&#xff0c;并对外提供访问接口&#xff0c;把这种Java对象称为集合 集合框架都包含三大块内容&#xff1a;&#xff08;接口、实现、算法&#xff09; &#xff08;1&#xff09;对外的接口 &#xff08;2&#xff09;…

【C++】string类的简单模拟实现

目录 string类初识 string模拟实现 string类成员变量 构造函数 拷贝构造 赋值运算符重载 析构函数 深浅拷贝问题 string类初识 由于C语言中的字符串不太符合OOP(面向对象编程)的思想&#xff0c;而且其底层空间需要用户自己管理&#xff0c;经常有访问越界的情况出现。…

模仿风宇博客登录蒙层弹窗(vuex+computed实现)

效果图 使用了动态组件做组件切换使用vue内置的transition组件实现过渡效果&#xff0c;蒙层 和 弹框 都使用了transition&#xff0c;并嵌套vuex computed计算属性&#xff0c;实现数据 和 方法共享&#xff0c;让其它组件也能够控制到登录弹框蒙层使用了固定定位未实现&…

读取注册表中的REG_QWORD和REG_BINARY(二进制)类型数据

读取注册表中的REG_QWORD和REG_BINARY二进制类型数据 发现的问题注册表中的一些概念&#xff08;统一认识&#xff09;读取代码&#xff08;字节数据大于8的会显示f开头的前缀&#xff09;说明&#xff08;备注&#xff09;改进代码参考链接 发现的问题 首先我们要明确&#x…

【Jetpack】DataBinding 架构组件 ⑥ ( RecyclerView 数据绑定 )

文章目录 一、RecyclerView 数据绑定核心要点1、启用数据绑定 / 导入依赖2、RecyclerView 条目 DataBinding 布局3、自定义 RecyclerView.Adapter 适配器要点 ( 本博客重点 ★ ) 二、RecyclerView 数据绑定源码示例1、build.gradle 构建脚本 ( 启用数据绑定 / 导入依赖 )2、主界…

ggplot中的注释图层annotate

文章目录 介绍利用注释层添加图形利用注释层添加文本利用注释层添加公式 介绍 ggplot作图包中除了常见的geom图层外&#xff0c;还有一个annotate的注释图层&#xff0c;实现对作图对象的额外添加&#xff0c;其添加要素不在ggplot()所接受的数据框中。 利用注释层添加图形 …

家政服务APP小程序开发功能详解

随着人们生活水平的提高&#xff0c;对家政服务的要求也越来越高。而传统的到家政公司寻找服务人员的方法显然已经无法满足人们需求&#xff0c;取而代之的是线上预约家政服务。家政服务App小程序软件可以满足用户在线预约&#xff0c;还可以根据自己的需求定制家政服务、选择家…

【Hadoop-CosDistcp】通过CosDistcp的方式迁移Cos中的数据至HDFS

【Hadoop-CosDistcp】通过CosDistcp的方式迁移Cos中的数据至HDFS 1&#xff09;功能说明2&#xff09;使用环境3&#xff09;下载与安装4&#xff09;原理说明5&#xff09;参数说明6&#xff09;使用示例7&#xff09;迁移 Cos 中的数据至 HDFS 及数据校验7.1.数据迁移7.2.数据…

【Unity入门】16.脚本引用组件

【Unity入门】脚本引用组件 大家好&#xff0c;我是Lampard~~ 欢迎来到Unity入门系列博客&#xff0c;所学知识来自B站阿发老师~感谢 &#xff08;一&#xff09;脚本引用普通组件 &#xff08;1&#xff09;点击控制音频播放 还记得我们的车载音乐AudioSource吗&#xff1f;…

zabbix搭建

1.环境 本实验使用一台centos7主机&#xff0c;关闭了firewalld和selinux服务&#xff0c;zabbix版本为5.0版本&#xff0c;mysql使用版本为5.7版本 若要搭建6.0以上版本的zabbix&#xff0c;则需要使用mysql 8.0以上的版本 其它版本的zabbix可参考zabbix官网:Download and…

YOLOv5+单目实现三维跟踪(python)

YOLOv5单目跟踪&#xff08;python&#xff09; 1. 目标跟踪2. 测距模块2.1 设置测距模块2.2 添加测距 3. 主代码4. 实验效果 相关链接 1. YOLOv5单目测距&#xff08;python&#xff09; 2. YOLOv7单目测距&#xff08;python&#xff09; 3. YOLOv7单目跟踪&#xff08;pytho…

C++练级之初级:第四篇

C练级之初级&#xff1a;第四篇 引用 C练级之初级&#xff1a;第四篇1.引用1.1引用的介绍1.2引用的使用场景1.3常引用 2.引用的底层3.引用的与指针的比较 总结 1.引用 1.1引用的介绍 &#x1f914;首先还是一个问题&#xff0c;引用是解决C语言什么不足&#xff1f; 指针在&am…

Python自动化sql注入:布尔盲注

在sql注入时&#xff0c;使用python脚本可以大大提高注入效率&#xff0c;这里演示一下编写python脚本实现布尔盲注的基本流程&#xff1a; 演示靶场&#xff1a;sqli-labs 布尔盲注 特点&#xff1a;没有回显没有报错&#xff0c;但根据sql语句正常与否返回不同结果&#x…

新手做电商直播带货怎么和快递合作谈价格

新手做电商直播带货怎么和快递合作谈价格达人带货一般怎样的合作模式&#xff1f;#达人带货 #直播带货 #红人 #百收网 跟快递谈价其实是有方法的&#xff0c;快递的价格不是说不能打下来&#xff0c;就是需要你们多一点点的心机。这个视频我就再给你们补充三个方法&#xff0c;…

HCIP之STP

企业网三层架构 线路冗余—二层网络桥接环路 因为路由器的路由表是由相对完善的计算所得&#xff0c;且存在防环规则&#xff1b;故路由器物理链路上实施备份时&#xff0c;一般不会出现环路&#xff1b;但交换转发数据依赖MAC表&#xff08;CAM表&#xff09;&#xff0c;该表…

【模式识别4】YOLO目标检测数据集xml格式转txt格式

YOLO目标检测数据集xml格式转txt格式 1. 转换前的xml格式2. xml格式转txt格式代码2.1 源代码2.2 需要修改的地方 3. 转换后的txt格式 代码资源&#xff1a;voc2txt.py 1. 转换前的xml格式 如果我们使用LabelImg工具标注数据集&#xff0c;生成的xml文件如下&#xff1a; xml…