[易学易懂系列|rustlang语言|零基础|快速入门|(21)|智能指针](代码片段)

gyc567 gyc567     2023-05-04     601

关键词:

[易学易懂系列|rustlang语言|零基础|快速入门|(21)|智能指针]

实用知识

智能指针

我们今天来讲讲Rust中的智能指针。

什么是指针?

在Rust,指针(普通指针),就是保存内存地址的值。这个值,指向堆heap的地址。

什么是智能指针?

在Rust,简单来说,相对普通指针,智能指针,除了保存内存地址外,还有额外的其他属性或元数据。

在Rust中,因为有所有权和借用的概念,所以引用和智能指针,又有一点不一样。

简单来说,智能指针,拥有数据所有权,而引用没有。

智能指针分以下几种:

1.Box,用于在堆里分配内存。

2.Rc,引用计数类型,用于多线程中的多个所有权。

3.Ref and RefMut, 用于强制让借用规则在运行时生效,一般通过RefCell访问。

我们先来看看Box,来看看简单例子:

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

这段代码很简单,定义一个Box智能指针,把它绑定到变量b,b就是智能指针(在栈stack里),指向堆内存地址的数据(数据在堆heap里)。

结果打印:

b = 5

我们来看看一个复杂点的例子,我们想定义一个lisp语言中的cons list,这种类型,是个递归类型,

简单来说,它是一个封装数据的容器,如图所示:

技术图片

我们来用Rust简单定义下这个数据结构,如下代码:

enum List 
    Cons(i32, List),
    Nil,

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

用cargo run ,运行下,结果报错:

error[E0072]: recursive type `List` has infinite size
 --> srcmain.rs:5:1
  |
5 | enum List 
  | ^^^^^^^^^ recursive type has infinite size
6 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
  = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `List` represxyentable

编译器报告说,这是一个递归类型,不确定长度,没办法初始化

怎么办?

用Box,代码修改如下:

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


use crate::List::Cons, Nil;

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

现在一切正确。

现在的数据结构,对Rust来说是这样的,如下图所示:

技术图片

Box类型因为实现了解引用特征Deref,所以它跟引用类型一样,同时,因为它实现了Drop特征,所以当它超出了作用域,它所占有的stack和heap空间会自动释放。

我们现在再来看看普通引用和智能指针的不同。

1.智能指针实现了Deref特征,所以它跟普通引用类似。

我们来看看例子:

fn main() 
    let x = 5;
    let y = &x;//y借用x,即y绑定到x的引用,y现在是个引用类型

    assert_eq!(5, x);
    assert_eq!(5, y);//error,错误,不能比较数据类型和引用类型
    

运行上面的代码,编译器报错:

error[E0277]: can't compare `integer` with `&integer`
  --> srcmain.rs:28:5
   |
28 |     assert_eq!(5, y);
   |     ^^^^^^^^^^^^^^^^^ no implementation for `integer == &integer`
   |
   = help: the trait `std::cmp::PartialEq<&integer>` is not implemented for `integer`

怎么办?

用解引用操作符*。

我们来修改一下代码 :

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

    assert_eq!(5, x);
    assert_eq!(5, *y);//用解引用操作符*,来取y指针指向的值

运行代码,一切正常。

这个解引用操作符*,就是用来取引用(指针)指向的值。

我们现在用Box类型来重写一下上面的代码:

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

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

运行代码,一切正常。

说明 Box类型跟上面的普通引用(指针),是一样的效果。

它们唯一 的区别就是,一个是智能指针,一个是普通指针。

好理解。

现在我们再来看定义一个自己的智能指针,开始设计:

struct MyBox<T>(T);//tuple元组类型

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

然后,我们同样的方式来用这个自定义的智能指针:

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

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

struct MyBox<T>(T);

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

运行代码,报错了:

error[E0614]: type `MyBox<integer>` cannot be dereferenced
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^

为什么?

因为,我们的MyBox没有实现特征Deref。

好,我们来实现Deref特征,代码更新如下:

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

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

struct MyBox<T>(T);////tuple类型

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

use std::ops::Deref;
////必须实现Deref trait,否则不能使用*操作符
impl<T> Deref for MyBox<T> 
    type Target = T;

    fn deref(&self) -> &T 
        &self.0////tuple索引
    

运行代码,一切正常。

我们来分析一下代码:

type Target = T;语法是指定一个Deref特征的关联类型。

而这段代码:

 fn deref(&self) -> &T 
        &self.0////tuple索引
    

则实现解引用方法,这里直接返回元组tuple第一个索引。

当前我们也可以用官方标准写法:

use std::ops::Deref;

struct DerefExample<T> 
    value: T,


impl<T> Deref for DerefExample<T> 
    type Target = T;

    fn deref(&self) -> &Self::Target 
        &self.value
    


fn main() 
    let x = DerefExample  value: 'a' ;
    assert_eq!('a', *x);
    println!("", *x);
    let y = DerefExample 
        value: String::from("Good!"),
    ;
    println!("", *y);

现在我们再看看把这个自定义智能指针作为传递参数:

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

我们先定义一个hello的方法,这个方法直接打印一条简单的问候信息。

我们看看怎么调用:

fn main() 
    let m = MyBox::new(String::from("Rust"));
    hello(&m);

完整代码如下:

use std::ops::Deref;
fn main() 

    let m = MyBox::new(String::from("Rust"));
    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;

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

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

运行代码,打印结果信息:

Hello, Rust!

一切正常。

因为Rust实现了强制解引用机制(deref coercion),所以我们不用再用解引用操作符访问:

fn main() 
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);//解引用写法

我们再来看看特征Drop Trait

简单来说,特征Drop是用来标记相关变量超出作用域后释放资源。

在Rust所有智能指针都已经由编译器自动加入实现这个方法。

当然,我们也可以定制一下这个方法,我们来看看简单例子:

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.");
//c,d在这里结束生命周期,这里Rust自动调用Drop实现方法

我们在main函数创建了两个实例c,d,在最后一个大括号时,结束这两个实例的“生命”,自动调用相关Drop实现方法,打印结果为:

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

在这里,Drop特征实现,有点类似于java的finalize方法,是一个对象或资源的临终遗言。

我们再来看看更复杂的智能指针,RC智能指针,也就是引用计数智能指针。

为什么要有引用计数智能指针呢?

因为有这样的情景,一个数据,可能有多个拥有者(当然这里的拥有者也就是线程)。这就是多所有权(multiple ownership)

我们可以想象,这个多所有权,就像一台电视机,一个房间只有一台电视机,第一个人来了,打开电视机,第二个人,第三个人来了,就各加一个座位(这里就像引用加个计数器,每来一个人加1),有人离开了,就把座位拿开(计数器减1),直到最后一个人看完了电视,把电视关了。

如果,中间有人a离开,但还有其他人在看电视,这个a直接把电视机关了,结果肯定会引起喧嚣!!!!

我们回过头来看看之前提到过的cons list数据结构:

技术图片

我现在用Box类型定义:

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


use crate::List::Cons, 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));

运行代码,编译错误:

error[E0382]: use of moved value: `a`
  --> src/main.rs:13:30
   |
12 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
13 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move
   |
   = note: move occurs because `a` has type `List`, which does not implement
   the `Copy` trait

怎么办?

用RC类型,修改代码如下 :

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


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

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

我们来看看引用计数智能指针的计数器,发生了什么,修改代码:

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


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

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));
    // println!("", c);
    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))

运行代码,打印结果为:

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

我们看到RC类型的引用计数器,是从1开始累加的。

中间c的生命周期结束了,就减一。

现在我们再来看看ReCell类型的智能指针。

我们从上面的例子可在看到 ,因为Rust默认的变量绑定是不可变的。

所以当我们要有一个变量,在运行时可变的。这时,就要用到ReCell类型。

看下面简单代码:

use std::cell::RefCell;
fn main() 
    let c = RefCell::new(5);

    *c.borrow_mut() = 7;
    assert_eq!(*c.borrow(), 7);

我们可以让编译器通过,并且成功运行。

为什么?

我们再来看看另一个例子:

use std::cell::RefCell;
fn main() 
    // let c = RefCell::new(5);

    // *c.borrow_mut() = 7;
    // assert_eq!(*c.borrow(), 7);

    let x = RefCell::new(42);

    let y = x.borrow_mut();
    let z = x.borrow_mut();//每二次可变借用,已经违反了编译器的借用规则。但可以编译通过。
    

运行代码,编译通过。

我们看到两次可变借用已经违反了借用规则。

Rust的借用规则很简单:

同一时间,同一数据

1.允许一个或多个共享借用(不可变借用)

2.只允许一个可变借用。

上面的代码已经两个可变借用,但也可以通过。

ReCell主要 作用就是用于运行时来检查借用规则。这就是内部可变性的设计模式。

主要用途在哪里?

我们再来看看例子:

struct Point 
    x: i32,
    y: i32,


let mut a = Point  x: 5, y: 6 ;

a.x = 10;

let b = Point  x: 5, y: 6 ;

b.x = 10; // Error: cannot assign to immutable field `b.x`.错误

解决错误用Cell:

use std::cell::Cell;

struct Point 
    x: i32,
    y: Cell<i32>,


let point = Point  x: 5, y: Cell::new(6) ;

point.y.set(7);

println!("y: :?", point.y);

https://doc.rust-lang.org/stable/book/ch15-00-smart-pointers.html

https://stackoverflow.com/questions/30831037/situations-where-cell-or-refcell-is-the-best-choice

[易学易懂系列|golang语言|零基础|快速入门|](代码片段)

...用和人工智能等领域占有越来越重要的地位。本文章是【易学易懂系列|编程语言入门】第一篇幅,希望可以帮助对编程感兴趣的同学更好地入门。本系列主要的核心思想是:实践实践再实践!每天编程至少一小时!好吧,我们... 查看详情

docker零基础快速入门(通俗易懂)(代码片段)

「作者主页」:士别三日wyx「作者简介」:CSDNtop100、阿里云博客专家、华为云享专家、网络安全领域优质创作者Docker一、安装Docker二、配置镜像加速器三、Docker服务命令四、Docker镜像命令五、Docker容器命令六、Docker容器... 查看详情

全网通bc26透传ttlnb-iot模块nb核心板stm32二次开发物联网(快速入门,通俗易懂,简单易学)

BC26模块是一款高性能、低功耗、多频段LTECatNB1无线通信模块。BC26兼容移GSM/GPRS系列的MC26模块,提供丰富的外部接口和协议线,同时支持中国移动的OneNET、中国电信loT、华为OceanConet以及阿里云等物联网平台,为客户的... 查看详情

零基础入门python爬虫[1]前言

...言的一个重要领域就是爬虫,通过Python编写爬虫简单易学&# 查看详情

有零基础开始自学python的小伙伴吗?怎么样可以快速入门?

...程语言被人成为是人工智慧的首选语言,而且被冠以简单易学、应用广泛的头衔。实际上如果没有相应的编程基础,学习任意一门编程语言都是有一定的难度的。不过相对于Java、C语言等编程语言,Python编程语言确实要更容易学... 查看详情

linux零基础快速入门到精通导学

...速入门到精通导学whyLinux课程设计如何学习导学whyLinux一系列我们的日常行为,背后都有Linux系统默默无闻的奉献。如果把操作系统分为:个人桌面操作 查看详情

python教程入门学习零基础怎样快速入门python语言?

...大。但其实,Python编程语言最大的特点之一就是简单易学,如果你是行外人员,想要转行编程,那么选择Python便是正确的选择,如果你已经是行业人士,想要增加自己的职场竞争力,选择学习Python也是... 查看详情

《零基础安装oracle数据库》单机系列⑤一键快速安装oracle21c数据库(代码片段)

目录前言安装下载项目安装软件下载Oracle安装包安装编辑vagrant.yml文件开始安装使用方式连接主机关闭主机开启主机销毁主机写在最后前言很多朋友吐槽我的脚本不会用,看不懂,哎,一言难尽!于是,我将[vagrant+virtualbox+shell脚... 查看详情

《零基础安装oracle数据库》单机asm系列5️⃣一键快速安装oracle21c数据库(代码片段)

前言很多朋友吐槽我的脚本不会用,看不懂,哎,一言难尽!于是,我将[vagrant+virtualbox+shell脚本]组合起来,实现了零基础也可安装Oracle数据库的方式,我称之为新手纯享版本,真正一行短命令! 查看详情

《零基础安装oracle数据库》单机系列④一键快速安装oracle19c数据库(代码片段)

目录前言安装下载项目安装软件下载Oracle安装包安装编辑vagrant.yml文件开始安装使用方式连接主机关闭主机开启主机销毁主机写在最后前言很多朋友吐槽我的脚本不会用,看不懂,哎,一言难尽!于是,我将[vagrant+virtualbox+shell脚... 查看详情

《零基础安装oracle数据库》单机系列③一键快速安装oracle18c数据库(代码片段)

目录前言安装下载项目安装软件下载Oracle安装包安装编辑vagrant.yml文件开始安装使用方式连接主机关闭主机开启主机销毁主机写在最后前言很多朋友吐槽我的脚本不会用,看不懂,哎,一言难尽!于是,我将[vagrant+virtualbox+shell脚... 查看详情

《零基础安装oracle数据库》单机asm系列4️⃣一键快速安装oracle19c数据库(代码片段)

前言很多朋友吐槽我的脚本不会用,看不懂,哎,一言难尽!于是,我将[vagrant+virtualbox+shell脚本]组合起来,实现了零基础也可安装Oracle数据库的方式,我称之为新手纯享版本,真正一行短命令! 查看详情

《零基础安装oracle数据库》单机asm系列3️⃣一键快速安装oracle18c数据库(代码片段)

前言很多朋友吐槽我的脚本不会用,看不懂,哎,一言难尽!于是,我将[vagrant+virtualbox+shell脚本]组合起来,实现了零基础也可安装Oracle数据库的方式,我称之为新手纯享版本,真正一行短命令! 查看详情

《零基础安装oracle数据库》单机系列⑤一键快速安装oracle21c数据库(代码片段)

目录前言安装下载项目安装软件下载Oracle安装包安装编辑vagrant.yml文件开始安装使用方式连接主机关闭主机开启主机销毁主机写在最后前言很多朋友吐槽我的脚本不会用,看不懂,哎,一言难尽!于是,我将[va... 查看详情

《零基础安装oracle数据库》单机系列④一键快速安装oracle19c数据库(代码片段)

目录前言安装下载项目安装软件下载Oracle安装包安装编辑vagrant.yml文件开始安装使用方式连接主机关闭主机开启主机销毁主机写在最后前言很多朋友吐槽我的脚本不会用,看不懂,哎,一言难尽!于是,我将[va... 查看详情

《零基础安装oracle数据库》单机系列③一键快速安装oracle18c数据库(代码片段)

目录前言安装下载项目安装软件下载Oracle安装包安装编辑vagrant.yml文件开始安装使用方式连接主机关闭主机开启主机销毁主机写在最后前言很多朋友吐槽我的脚本不会用,看不懂,哎,一言难尽!于是,我将[va... 查看详情

《零基础安装oracle数据库》rac集群系列❺简单两步快速安装oracle21crac数据库(代码片段)

前言很多朋友吐槽我的脚本不会用,看不懂,哎,一言难尽!于是,我将[vagrant+virtualbox+shell脚本]组合起来,实现了零基础也可安装Oracle数据库的方式,我称之为新手纯享版本,真正一行短命令! 查看详情

《零基础安装oracle数据库》rac集群系列❺简单两步快速安装oracle21crac数据库(代码片段)

前言很多朋友吐槽我的脚本不会用,看不懂,哎,一言难尽!于是,我将[vagrant+virtualbox+shell脚本]组合起来,实现了零基础也可安装Oracle数据库的方式,我称之为新手纯享版本,真正一行短命令! 查看详情