用Rust写一个链表,非常详细,一遍看懂

news2024/9/20 0:55:06

前言

在Rust里写一个链表可不是一件容易的事,涉及到很多的知识点,需要熟练掌握之后才能写出一个不错的链表。这篇文章主要介绍了如何写一个Rust链表,并且补充了涉及到的很多的额外知识点,尤其是所有权问题。
首先,你需要明白,为什么Rust链表难写,同样的为什么C实现简单一点呢?
只能有一个引用!!!这是最关键的,然后就是Rust中是没有NULL指针的,这就需要用到Option枚举,在编译阶段必须知道类型的大小,这就需要使用Box智能指针。
在C语言实现一个简单的链表,可以这样写:

Node* new_node = create_new_node(v);
new_node->next = head;
head = new_node;

在这里插入图片描述
这段代码里面,head和new_node->next指向了同一个节点,这个在C语言里面没事,但是在Rust不允许,因为指针类型为Box,Box对象同一时刻只能有一个可变引用,而在上面的插入过程中,第2行,有两个指针指向同一个头结点。

预先知识点

Option枚举及其所有权问题

1.最简单的Option枚举就是,里面有Some和None,对于枚举大家一定要了解,他是等于其中一个的,并不是包含关系,这一点一定要理清楚,不然代码会很难理解。None可以替代C语言中的NULL。

pub enum Option<T> 
{
    Some(T),
    None,
}

2.unwrap()方法:
在确认Option不为None的情况下,可以用unwrap方法拆解出其中的值,并获取值的所有权,如果为None,就会引发panic。这里要强调的是,unwrap会消费Option本身的值,后面就不能再用了。

    let a = Some(Box::new(5));
    let d = a.unwrap();
    // println!("{:?}", a); // cannot use 'a' again.
    println!("{:?}", d);

但这里有一个所有权的限制,因为涉及到其内部值的所有权的转移,所以只有Option原始变量本身可以调用unwrap方法,其引用(包括可变引用)均不可调用。这和unwrap的实现方法有关系,因为其消费了原始变量。下方代码不可编译:

    let mut a = Some(Box::new(5));
    let b = &mut a;
    let c = b.unwrap(); // Error! cannot move out of `*b` which is behind a mutable reference

下面是unwrap源码:传入self,会转移所有权。

    pub const fn unwrap(self) -> T {
        match self {
            Some(val) => val,
            None => panic!("called `Option::unwrap()` on a `None` value"),
        }

3.take()方法:
take方法可以从Option中取出值,为原来的Option变量留下None值,但原来的变量还有效(但实际上没什么用处了,因为只剩下None了)。take方法的原始值或者引用必须为mut类型。强调一下,借用情况下可以调用take方法。或者说指向Option的可变引用(指针)可以调用take方法。

fn main() {
    let mut a = Some(Box::new(5));
    let mut b = &mut a;
    let c = &mut b;
    let d = c.take();
    println!("{:?}", c);//None
    println!("{:?}", d);//Some(5)
    println!("{:?}", a);//None
}

4.as_ref()方法:
但上面这两个方法,都改变了Option变量本身。如果不改变或受到限制无法改变Option本身时,只想借用或可变借用其中unwrap的值应该怎么办呢?这也是在实现链表时遇到的让人掉坑的问题。幸好Option提供了 as_ref 和 as_mut方法。

fn as_ref(&self) -> Option<&T>
//接受一个不可变引用,不获取所有权,返回一个Option枚举,其中的T为不可变引用类型,不获取所有权。

该方法将对Option的引用变为对Option所包含对象的不可变引用,并且返回一个新的Option。对这个新的Option进行unwrap操作,可以获得原Option所包含的对象的不可变引用。原始Option变量及其引用,均可调用as_ref方法,有点克隆的味道。例如:

    let a = Some(Box::new(5));
    let b = a.as_ref();
    let c = b.unwrap();
    println!("{:?}", a);//Some(5)
    println!("{:?}", b);//Some(5)
    println!("{:?}", c);//5

5.as_mut()方法
与as_ref类似,as_mut只是返回了Option包含的数据的可变引用,

fn main() {
    let mut a = Some(Point { x: 5, y: 5 });
    // let b = a.as_mut();
    let b = &mut a;
    let c = b.as_mut(); // c is a new Option;
    let d = c.unwrap();
    d.x += 10;
    let e = &mut d.y;
    *e += 20;
 
    println!("{:?}", d.x);//15
    // println!("{:?}", c); // c is not available because of method call of "unwrap".
    println!("{:?}", b);//Some(Point { x: 15, y: 25 })
    println!("{:?}", a);//Some(Point { x: 15, y: 25 })
}

通过以上代码可以看出,在未改变Option变量a的情况下,通过as_mut方法,改变了其包含的数据的值。这个能力对于编写链表时,尤其是节点的插入、删除时,可以灵活的操作指向下一个节点的指针。
说明一点,调用as_mut方法时,Option变量本身或其引用,必须为可变的,即mut类型。
简单类型的一个例子:这里注意原来的5已经改变成15。

fn main() {
    let mut a = Some(5);
    let b = a.as_mut().unwrap();
    *b += 10;
    println!("b = {:?}", b);//b = 15
    println!("a = {:?}", a);//a=Some(15)
}
if let和while let还有?操作符
 if let Some(a_T) = a_option { 
  //a_T生命周期仅在花括号内生效。
  //当a_option不为None时满足条件。
}

 while let Some(a_T) = a_option { 
  //当a_option 为None时退出循环。
}

?操作符的作用就是简化Result枚举的,举个例子:

let f = File::open("username.txt");
let mut f = match f {
    Ok(file) => file,
    Err(e) => return Err(e),
};
//有了?运算符,就可以改成下面一行话
let mut f = File::open("username.txt")?;
Rust方法和关联函数

具体可以看https://blog.csdn.net/cacique111/article/details/126311569,说的很详细。

如何编写一个链表

链表的定义

链表比较绕圈子,回顾Rust内存管理的基础知识,Rust需要在编译时知道一个类型占用多少空间,Node结构体内部嵌套了它自己,这样在编译时就无法确认其占用空间大小了。 在Rust中当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候,可以使用智能指针Box。

/// 单链表节点
#[derive(Debug)]
struct Node<T> {
    val: T,
    next: Option<Box<Node<T>>>,
}
/// 单链表
#[derive(Debug)]
struct LinkedList<T> {
    head: Option<Box<Node<T>>>,
    size: usize,
}

上面的代码定义之后,在Rust里面经常使用New函数,所以下面写上面结构体的new方法,

impl<T> LinkedList<T> {
    pub fn new() -> Self {
        SimpleLinkedList {
            head: None,
            size: 0,
        }
    }

LinkedList的new函数用来创建一个空链表。

常用的链表操作
    pub fn is_empty(&self) -> bool {
        self.size == 0
    }
    pub fn len(&self) -> usize {
        self.size
    }
    //头插法
    pub fn push(&mut self, data: T)->&mut Self {
        let node = Box::new(Node::<T>{
            val: data,
            next: self.head.take(),//如果是空链表的话,直接为None,要不然就是Some(T)。为什么这里不用self.head?
            //看上去一样,但是如果不take,就会出现多个指针指向同一个节点,不符合所有权规则,可变引用可以进行take
            //take之后就会释放,变成None
        });
        self.head = Some(node);//将“头指针”指向新插入的节点
        self.size += 1;
        self
    }
//pop会从头节点开始删除节点,返回例如Some(5),Option<T>,T的所有权转移,删除转移也没啥。
    pub fn pop(&mut self) -> Option<T> {
        if self.is_empty() {
            return None;
        }
        let head = self.head.take().unwrap();//取出Some里面的值
        self.head = head.next;//head指向下一个节点
        self.size -= 1;
        Some(head.val)
    }
    //peek返回头节点,是T的引用,不转移所有权。
    pub fn peek(&self) -> Option<&T> {
        if self.is_empty() {
            return None
        }
        return Some(&self.head.as_ref().unwrap().val)
        //这里确实,只是返回一个引用,不能对原来的链表造成任何影响。
    }
    pub fn peek_mut(&mut self) -> Option<&mut T> {
        if self.is_empty() {
            return None
        }//这里就是as_mut,外部改变T的值,链表里面的值也会受到改变。
        return Some(&mut self.head.as_mut().unwrap().val)
    }
    pub fn rev(mut self) -> SimpleLinkedList<T> {
        let mut rev_list = SimpleLinkedList::<T>::new();
        while !self.is_empty() {
            rev_list.push(self.pop().unwrap());
        }
        rev_list
    }

main函数部分,其中迭代器部分下期会有详细的解答。

fn main(){
    let mut list=SimpleLinkedList::new();
    list.push(3);
    list.push(2);
    list.push(1);
    list.len();
    // println!("{list:?}");
    // for val in list.from_iter() {
    //     println!("{}",val);
    // }
    // for val in list.iter_mut() {
    //     *val += 1;
    //     println!("{}",val);
    // }
    for val in list.into_iter() {
        println!("{}",val);
    }

    //println!("{:?}",list.pop());
    //list.pop();
    //println!("{list:?}");
    //println!("{}",list.len());
}

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

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

相关文章

人工智能内容生成元年—AI绘画原理解析

AIGC体验生成一、背景 2022年AIGC&#xff08;AI生成内容&#xff09;焕发出了勃勃生机&#xff0c;大有元年之势&#xff0c;技术与应用迭代都扎堆呈现。在各种新闻媒体处可以看到诸多关于学术前沿研究&#xff0c;以及相应落地的商用案例。可谓出现了现象级的学术-商业共振。…

phpbugs代码审计第三题多重加密答案错误

phpbugs多重加密 这题官方给的答案是错的&#xff0c;网上给的也是错的&#xff0c;麻烦别直接拿给的答案来抄好吗&#xff1f;就想纠正一下别误导别人了 源码如下: <?phpinclude common.php;$requset array_merge($_GET, $_POST, $_SESSION, $_COOKIE);//把一个或多个…

安卓期末大作业——售票APP源码和设计报告

大作业文档 项目名称&#xff1a;售票APP专业&#xff1a;班级&#xff1a;学号&#xff1a;姓名&#xff1a; 目 录 目录 一、项目功能介绍3二、项目运行环境31、开发环境32、运行环境33、是否需要联网3三、项目配置文件及工程结构31、工程配置文件32、工程结构目录4四、项…

【C语言 数据结构】串 - 顺序

文章目录串 - 顺序一、串的表示及实现1.1 串的概念1.2 串的初始化1.3 复制串1.4 串的判空1.5 串的比较1.6 串的长度1.7 清空串1.8 串的拼接1.9 串的截取1.10 插入子串1.11 串的打印二、串的应用2.1 串的模式匹配 - 基本操作2.2 串的模式匹配 - BF2.3 串的模式匹配 - KMPnext数组…

【学习笔记77】ajax的函数封装

一、封装分析 &#xff08;一 &#xff09;封装方案 1、回调函数方式 将来使用的时候, 可能会遇到回调地狱 2、promise方式 效果不会有变化, 而且还能和 async/await 一起使用 &#xff08;二&#xff09;函数的参数 请求方式: method > 默认值 get请求地址: url > 必填…

Android开发——Jetpack Compose的使用

Android开发——Jetpack Compose的使用什么是Jetpack ComposeJetpack Compose带来的变化Jetpack Compose的两种运用方法将Jetpack Compose 添加到现有项目1.gradle 配置2.使用Kotlin-Gradle 插件3. 添加依赖项创建一个新的支持Jetpack Compose的项目1.创建一些简单应用定义可组…

12.3做题

一.车队问题 1.思路: 先把所在位置进行排序,升序排序, 计算出每辆车在不受其余车的影响时&#xff0c;行驶到终点需要的时间 从后往前看 对于相邻的两辆车 S 和 F&#xff0c;F 的起始位置大于 S&#xff0c;如果 S 行驶到终点需要的时间小于等于 F&#xff0c;那么 S 一定…

MySQL集群搭建-MMM高可用架构

MySQL集群搭建-MMM高可用架构 原文地址 https://segmentfault.com/a/1190000017286307&#xfeff; 1 MMM 介绍 1.1 简介 MMM 是一套支持双主故障切换以及双主日常管理的第三方软件。MMM 由 Perl 开发&#xff0c;用来管理和监控双主复制&#xff0c;虽然是双主架构&#xff…

volatile

是java虚拟机提供的轻量级的同步机制&#xff08;乞丐版的synchronized) 具备三个性质&#xff1a;保证可见性&#xff0c;不保证原子性&#xff0c;禁止指令重排 前置知识&#xff1a; …

JS读取本地CSV文件数据

JS读取本地CSV文件数据 文件中的部分数据如图 需求是需要提取出文件的数据 使用到的模块是 Papa Parse 1. 依赖安装 yarn add papaparse papaparse的基本使用可以参考官方demo 2. 解析本地文件 首先需要注意, papaparse解析本地文件, 需要的文件格式是从DOM中获得的File…

GO高级特性 之 并发模型

本文介绍一些并发的基础知识、常见的并发模型一级Go语言的MPG并发模型及其运行原理 并发与并行的区别 -并发并行概念并发指同一时间段&#xff0c;多条命令在CPU上同时执行。并行指同一时刻&#xff0c;多条命令在CPU上执行运行原理并发程序不要求计算机有多核计算能力&#…

毕设选题推荐基于python的django框架医疗急诊预约系统

&#x1f496;&#x1f525;作者主页&#xff1a;计算机毕设老哥&#x1f525; &#x1f496; 精彩专栏推荐订阅&#xff1a;在 下方专栏&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; Java实战项目专栏 Python实…

项目成本管理质量管理

项目成本管理-控制成本目录概述需求&#xff1a;设计思路实现思路分析1.EVM2.偏差指标3.Question:4.绩效指标5.典型偏差TCPIS曲线图绩效审查参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip …

[近两万字] MySQL大全

目录 单元1 创建数据库 1.1 创建数据库 1.查看数据库 2.选择数据库 3.删除数据库 1.2 创建数据表 1.查看表结构 2.查看所有数据表 3.复制表结构 4.删除表 5.修改表数据 5.1 修改表名 5.2 添加字段 5.3删除字段 5.4修改字段的数据类型 5.5修改字段的名称 5.6修改字段…

[网络工程师]-应用层协议-SNMP

简单网络管理协议&#xff08;Simple Network Management Protocol&#xff0c;SNMP&#xff09;是在应用层上进行网络设备间通信的管理协议&#xff0c;可以用于网络状态监视、网络参数设定、网络流量统计与分析、发现网络故障等。SNMP基于UDP协议&#xff0c;由SNMP协议、管理…

【交通建模】基于模型的自主交通仿真框架附matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

【深入浅出Java并发编程指南】「难点 - 核心 - 遗漏」让我们一起探索一下CyclicBarrier的技术原理和源码分析

CyclicBarrier和CountDownLatch CyclicBarrier和CountDownLatch 都位于java.util.concurrent这个包下&#xff0c;其工作原理的核心要点&#xff1a; CyclicBarrier工作原理分析 那么接下来给大家分享分析一下JDK1.8的CyclicBarrier的工作原理。 简单认识CyclicBarrier 何为…

Nginx动静分离、缓存配置、性能调优、集群配置

一. Nginx动静分离 1. 准备 1个web程序&#xff1a;部署在7061端口&#xff0c;启动 【dotnet NginxWeb.dll --urls"http://*:7061" --ip"127.0.0.1" --port7061】 Nginx程序&#xff1a;监听7000端口 2. 目的 比如单独启动部署在7061端口下的web程序&am…

c++ 静态库,动态库的制作和使用

文章目录1.什么是库&#xff1f;2.静态库的制作1.静态库的命名规则2.静态库的制作与使用1.静态库的制作2.静态库的使用3.动态库的制作1.动态库的命名规则2.动态库的制作与使用1.动态库的制作2.动态库的使用3.动态库加载失败的原因4.静态库和动态库的对比1.程序编译成可执行文件…

[附源码]Python计算机毕业设计Django基于Java的失物招领平台

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…