rust中的集合容器(切片和哈希)与错误处理

news2024/10/7 1:28:18

String、数组[T:n]、列表Vec\哈希表HashMap<K,V>等。
切片slice;
循环缓冲区 VecDeque、双向列表 LinkedList等。(这是指双向链表吗?)
这些集合容器的共性:
可以遍历
可以进行 map-reduce操作。
可以从一种类型转换成另一种类型。

主要学一下切片和哈希

切片
定义:是一组类型相同,但是长度不确定,在内存中连续存放的数据结构

fn main() {
    let arr = [1, 2, 3, 4, 5];
    let vec = vec![1, 2, 3, 4, 5];
    let s1 = &arr[..2];
    let s2 = &vec[..2];
    println!("s1: {:?}, s2: {:?}", s1, s2);

    // &[T]&[T] 是否相等取决于长度和内容是否相等
    assert_eq!(s1, s2);
    // &[T] 可以和 Vec<T>/[T;n] 比较,也会看长度和内容
    assert_eq!(&arr[..], vec);
    assert_eq!(&vec[..], arr);
}

在这里插入图片描述
在这里插入图片描述
从上面可以看出,切片通常用引用的形式,实际是指针指向实际数据空间,而vec的引用是指向指针的指针。

String 是一个特殊的 Vec,所以在 String 上做切片,也是一个特殊的结构 &str。
在这里插入图片描述
直接对string引用,就是一个指针,如果是&str或者且切片,就是胖指针了。

迭代器可以说是切片的孪生兄弟。**切片是集合数据的视图,而迭代器定义了对集合数据的各种访问操作。**iterator trait 有很多方法,但一般我们只需要定义它的关联类型 Item 和 next() 方法。
Item 定义了每次从迭代器里取出的数据类型。
next()是取下一个值的方法。为None,代表没有了。

切片将它们抽象成相同的访问方式,实现了在不同数据结构之上的同一抽象,这种方法很值得我们学习
哈希表
跟创建动态数组 Vec 的方法类似,可以使用 new 方法来创建 HashMap,然后通过 insert 方法插入键值对。
使用迭代器和 collect 方法创建
在实际使用中,不是所有的场景都能 new 一个哈希表后,然后悠哉悠哉的依次插入对应的键值对,而是可能会从另外一个数据结构中,获取到对应的数据,最终生成 HashMap。
例如考虑一个场景,有一张表格中记录了足球联赛中各队伍名称和积分的信息,这张表如果被导入到 Rust 项目中,一个合理的数据结构是 Vec<(String, u32)> 类型,该数组中的元素是一个个元组,该数据结构跟表格数据非常契合:表格中的数据都是逐行存储,每一个行都存有一个 (队伍名称, 积分) 的信息。
但是在很多时候,又需要通过队伍名称来查询对应的积分,此时动态数组就不适用了,因此可以用 HashMap 来保存相关的队伍名称 -> 积分映射关系。 理想很丰满,现实很骨感,如何将 Vec<(String, u32)> 中的数据快速写入到 HashMap<String, u32> 中?
遍历列表,将每一个元组作为一对 KV 插入到 HashMap 中,很简单,但是……也不太聪明的样子,换词说就是 —— 不够 rusty。
好在,Rust 为我们提供了一个非常精妙的解决办法:先将 Vec 转为迭代器,接着通过 collect 方法,将迭代器中的元素收集后,转成 HashMap:

fn main() {
    use std::collections::HashMap;

    let teams_list = vec![
        ("中国队".to_string(), 100),
        ("美国队".to_string(), 10),
        ("日本队".to_string(), 50),
    ];

    let teams_map: HashMap<_,_> = teams_list.into_iter().collect();
获取哈希表元素时

get 方法返回一个 Option<&i32> 类型:当查询不到时,会返回一个 None,查询到时返回 Some(&i32)
&i32 是对 HashMap 中值的借用,如果不使用借用,可能会发生所有权的转移

错误处理
错误处理的三种方式:使用返回值、异常处理和类型系统。而 Rust 目前看到的方案:主要用类型系统来处理错误,辅以异常来应对不可恢复的错误。

C语言中基本用返回值(错误码)来处理错误,比如文件fopen(filename)打开失败返回NULL,创建socket失败返回-1等。这样有很多局限,返回值本来有自己的语义,非要把错误和返回值混淆在一起,**加重了开发者的心智负担。**开发者很有可能会忘记处理,或者记错返回值的语义。

C++,JAVA中用异常处理来处理错误。C++ 异常处理涉及到三个关键字:try、catch、throw。
如果有一个块throw抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。
您可以使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。
以下是尝试除以零时抛出异常的实例:
double division(int a, int b)
{
if( b == 0 )
{
throw “Division by zero condition!”;
}
return (a/b);
}

int main ()
{
int x = 50;
int y = 0;
double z = 0;
try {
z = division(x, y);
cout << z << endl;
}catch (const char
msg) {
cerr << msg << endl;
}
*
return 0;
}
异常处理的缺点:第一是可以看大异常处理的程序流很复杂,函数返回点可能在你意料之外,这就导致了代码管理和调试的困难,所以保证异常安全的第一个原则就是:避免抛出异常。
异常处理另外一个比较严重的问题是:开发者会滥用异常。异常处理的开销要比处理返回值大得多,滥用会有很多额外的开销

Rust总结前辈的经验,使用类型系统来构建主要的错误处理流程。构建了Option类型和Result类型。
pub enum Option {
None,
Some(T),
}
#[must_use = “this Result may be an Err variant, which should be handled”]
pub enum Result<T, E> {
Ok(T),
Err(E),
}
Option是一个简单的enum, 它可以处理有值/没值 这种最简单的错误类型。

Result是一个复杂些的enum。当函数出错时,可以返回Err(E),否则Ok(T)。
可以看到Result类型有must_use, 如果没有使用就会报warning,以保证错误被处理了。
在这里插入图片描述
上图中的例子,如果我们不处理read_file的返回值,就开始有提示了。 (那这不是回到了 Golang的 到处都是 if err != nil的情况了吗?)Go 语言为人诟病的其中一点就是 if err != nil {} 的大量使用,缺乏一些程序设计的美感

?操作符
如果执行传播错误,不想当时处理,就用?操作符。这样让错误传播和异常处理不相上下,同时又避免了异常处理带来的问题。(一个设计良好的程序,一个功能涉及十几层的函数调用都有可能。而错误处理也往往不是哪里调用出错,就在哪里处理,实际应用中,大概率会把错误层层上传然后交给调用链的上游函数进行处理,错误传播将极为常见。)


#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    // 打开文件,f是`Result<文件句柄,io::Error>`
    let f = File::open("hello.txt");

    let mut f = match f {
        // 打开文件成功,将file句柄赋值给f
        Ok(file) => file,
        // 打开文件失败,将错误返回(向上传播)
        Err(e) => return Err(e),
    };
    // 创建动态字符串s
    let mut s = String::new();
    // 从f文件句柄读取数据并写入s中
    match f.read_to_string(&mut s) {
        // 读取成功,返回Ok封装的字符串
        Ok(_) => Ok(s),
        // 将错误向上传播
        Err(e) => Err(e),
    }
}
}

这个例子中,错误传播到调用该函数的地方,自己不关心怎么处理,至于怎么处理就是调用者的事,如果是错误,它可以选择继续向上传播错误,也可以直接 panic,亦或将具体的错误原因包装后写入 socket 中呈现给终端用户。
但是上面的代码也有自己的问题,那就是太长了
明星出场,必须得有排面,来看看 ? 的排面:

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}
}

其实 ? 就是一个宏,它的作用跟上面的 match 几乎一模一样:
如果结果是 Ok(T),则把 T 赋值给 f,如果结果是 Err(E),则返回该错误,所以 ? 特别适合用来传播错误。

Rust 中的错误主要分为两类:
可恢复错误,通常用于从系统全局角度来看可以接受的错误,例如处理用户的访问、操作等错误,这些错误只会影响某个用户自身的操作进程,而不会对系统的全局稳定性产生影响
不可恢复错误,刚好相反,该错误通常是全局性或者系统性的错误,例如数组越界访问,系统启动时发生了影响启动流程的错误等等,这些错误的影响往往对于系统来说是致命的
很多编程语言,并不会区分这些错误,而是直接采用异常的方式去处理。Rust 没有异常,但是 Rust 也有自己的卧龙凤雏:Result<T, E> 用于可恢复错误,panic! 用于不可恢复错误。

在 Rust 中触发 panic 有两种方式:被动触发和主动调用
fn main() {
let v = vec![1, 2, 3];

v[99];

} 数组访问越界会出发被动panic

在某些特殊场景中,开发者想要主动抛出一个异常,例如开头提到的在系统启动阶段读取文件失败。
对此,Rust 为我们提供了 panic! 宏,当调用执行该宏时,程序会打印出一个错误信息,展开报错点往前的函数调用堆栈,最后退出程序。
首先,来调用一下 panic!,这里使用了最简单的代码实现,实际上你在程序的任何地方都可以这样调用:

fn main() {
panic!(“crash and burn”);
}

当出现 panic! 时,程序提供了两种方式来处理终止流程:栈展开和直接终止。

其中,默认的方式就是 栈展开,这意味着 Rust 会回溯栈上数据和函数调用,因此也意味着更多的善后工作,好处是可以给出充分的报错信息和栈调用信息,便于事后的问题复盘。直接终止,顾名思义,不清理数据就直接退出程序,善后工作交与操作系统来负责。
对于绝大多数用户,使用默认选择是最好的,但是当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,例如下面的配置修改 Cargo.toml 文件,实现在 release 模式下遇到 panic 直接终止:
[profile.release]
panic = ‘abort’

线程 panic 后,程序是否会终止?
长话短说,如果是 main 线程,则程序会终止,如果是其它子线程,该线程会终止,但是不会影响 main 线程。因此,尽量不要在 main 线程中做太多任务,将这些任务交由子线程去做,就算子线程 panic 也不会导致整个程序的结束。

最后panic不要乱用。不可恢复时采用。
同时,这里也体验到了宏编程的思想和简洁性。实际上panic做的工作很多,但是被包含在宏里了,写代码就很方便了。

总结:
相比 C/Golang 直接用返回值的错误处理方式,Rust 在类型上更完备,构建了逻辑更为严谨的 Option 类型和 Result 类型,既避免了错误被不慎忽略,也避免了用啰嗦的表达方式传递错误;

相对于 C++ / Java 使用异常的方式,Rust 区分了可恢复错误和不可恢复错误,分别使用 Option / Result,以及 panic! / catch_unwind 来应对,更安全高效,避免了异常安全带来的诸多问题;

对比它的老师 Haskell,Rust 的错误处理更加实用简洁,这得益于它强大的元编程功能,使用 ?操作符来简化错误的传递。

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

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

相关文章

VUE使用el-ui的form表单输入框批量搜索<VUE专栏三>

针对form表单的输入框单号批量查询&#xff0c;这里用换行符进行分割&#xff0c;注意v-model不要使用.trim 前端代码&#xff1a; <el-form-item label"SKU编码:" prop"prodNumbers"><el-input type"textarea" :rows"4" pla…

阿里数学竞赛决赛名单公布:北大人数是清华4倍 | 最小仅14岁

4月10日消息&#xff0c;第二届阿里巴巴全球数学竞赛决赛入围名单公布&#xff0c;全球12个国家516位选手晋级&#xff0c;晋级率仅有1&#xff05;。 根据参赛者填报信息&#xff0c;晋级选手80&#xff05;以上是90后&#xff0c;年纪最小的只有14岁。 入围人数最高的前20所高…

【Linux】git命令(基础,新手)

文章目录1.查看当前git版本信息2.安装git3.将远端仓库克隆到本地4.三板斧第一招&#xff1a;git add5.三板斧第二招&#xff1a;git commit6.三板斧第三招&#xff1a;git push7.对仓库文件进行更改8.查看使用提交日志9.查看本地与远端的同步状态10.从远端仓库拉取最新版本文件…

ChatGPT Plus价格太贵,可以约上三五知己一起上车体验一下,这个项目就能帮到你

❝ 对于想体验ChatGPT PLus的小伙伴&#xff0c;可能觉得自己一个人一个月花费20美元&#xff0c;相对于人民币每月137多&#xff0c;确实是一个不少的开支&#xff0c;如果&#xff0c;几个人合作一个账号&#xff0c;这样负担就减少了。刚好&#xff0c;最近逛github发现刚好…

深度学习-第R2周——LSTM火灾温度预测

深度学习-第R2周——LSTM火灾温度预测深度学习-第R2周——LSTM火灾温度预测一、前言二、我的环境三、前期工作1、导入数据集2、数据可视化四、构建数据集1、设置x,y2、归一化3、划分数据集五、构建模型六、模型训练1、编译2、训练七、评估1、loss图2、预测深度学习-第R2周——L…

MySQL数据库实现主主同步

前言 MySQL主主同步实际上是在主从同步的基础上将从数据库也提升成主数据库&#xff0c;让它们可以互相读写数据库&#xff0c;从数据库变成主数据库&#xff1b;主从相互授权连接&#xff0c;读取对方binlog日志并更新到本地数据库的过程,只要对方数据改变&#xff0c;自己就…

K均值聚类分析流程

K均值聚类分析流程 一、案例背景 在某体育赛事中&#xff0c;意大利、韩国、罗马尼亚、法国、中国、美国、俄罗斯七个国家的裁判对300名运动员进行评分&#xff0c;现在想要通过评分上的差异将300名选手进行分类&#xff0c;计划将选手分为高水平、中水平、低水平三个类别。因…

Unity2D 商业游戏案例 - 梦幻西游(第二季 框架设计篇)

00 网址 来源 siki学院的&#xff08;1年有限期到期前下载的项目&#xff0c;现在已经过期&#xff0c;所以自己理清项目&#xff09; 所以更多的不是学习这个项目&#xff0c;而是学习理清该类型的项目的思路 Unity2D 商业游戏案例 - 梦幻西游&#xff08;第二季 框架设计篇&…

python+vue 在线考试系统的设计与实现

1.用户登录 用户要通过本系统查询对课程信息进行下载&#xff0c;必须先输入用户名和密码进行登陆。为了避免非其他人员都可以获得登陆权限&#xff0c;登陆系统不设注册过程&#xff0c;所有用户和教师的登陆信息将事先由管理人员直接对数据库进行录入。 2.教师 教师登录系统后…

【排序】排序这样写才对Ⅱ -冒泡排序与快速排序Ⅰ

Halo&#xff0c;这里是Ppeua。平时主要更新C语言&#xff0c;C&#xff0c;数据结构算法......感兴趣就关注我吧&#xff01;你定不会失望。 &#x1f308;个人主页&#xff1a;主页链接 &#x1f308;算法专栏&#xff1a;专栏链接 我会一直往里填充内容哒&#xff01; &…

【Spring6】| Spring6整合JUnit

目录 一&#xff1a;Spring6整合JUnit 1. Spring对JUnit4的支持 2. Spring对JUnit5的支持 一&#xff1a;Spring6整合JUnit 1. Spring对JUnit4的支持 准备工作&#xff1a;pom.xml 注&#xff1a;以前是直接使用单元测试Junit&#xff0c;现在使用Spring对Junit的整合&…

快递电子运单上,电话应隐藏6位以上,禁止显示这些信息

我国快递年业务量达千亿件&#xff0c;快递电子运单是应用于快递外包装的重要单据&#xff0c;每年耗用量很大。在强化个人信息保护方面&#xff0c;《快递电子运单》国家标准要求快递企业、电商经营主体等采取措施&#xff0c;避免在电子运单上显示完整的收寄件人个人信息。 …

【机器学习】P14 Tensorflow 使用指南 Dense Sequential Tensorflow 实现

Tensorflow 第一节&#xff1a;使用指南Tensorflow 安装神经网络一些基本概念隐藏层和输出层&#xff1a;神经元的输出公式Tensorflow 全连接层 Dense 与 顺序模型 SequentialDense LayerSequential Model代码实现一个神经网络实现方式一&#xff1a;手写神经网络* 实现方式二&…

JavaScript基础入门全解析(上)

JavaScript基础语法 什么是JavaScript&#xff08;简称js&#xff09; 1.首先了解前端页面的组成&#xff08;前端页面的三层结构&#xff09; ●HTML 表示了你的页面内有什么&#xff0c;组成页面的骨架 &#xff08;结构层&#xff09; ●CSS 表示了你的页面中每一个内容是…

Linux系统中安装新版本nacos(centos7)

1. 背景需求 由于一些限制,在客户现场的Linux操作系统中,没有安装docker k8s等容器,无法直接使用镜像安装,而且客户要求只能在原始的操作系统中安装最新版的nacos,(为什么需要安装最新版的nacos,因为检测国网检测到之前版本的nacos有漏洞,需要安装新版的nacos). 2. 下载nacos…

Windows10+Cmake+VS2019编译opencv

主要参考&#xff1a;Windows10CmakeVS2019编译opencv&#xff08;超级详细&#xff09;_vs编译opencv_乐安世家的博客-CSDN博客 OpenCV&#xff1a;Releases - OpenCV 想直接简单使用的话&#xff0c;不需要自己编译&#xff0c;下载编译好的就可以 假如需要用到opencv-contr…

【Python入门第四十九天】Python丨NumPy 数组拆分

拆分 NumPy 数组 拆分是连接的反向操作。 连接&#xff08;Joining&#xff09;是将多个数组合并为一个&#xff0c;拆分&#xff08;Spliting&#xff09;将一个数组拆分为多个。 我们使用 array_split() 分割数组&#xff0c;将要分割的数组和分割数传递给它。 实例 将数…

Docker教程:如何将Helix QAC创建为一个容器并运行?

在这个Docker教程中&#xff0c;你将了解到如何将Helix QAC创建为一个容器化的镜像并运行。 Docker的基本定义是一个开源且流行的操作系统级虚拟化&#xff08;通常称为“容器化”&#xff09;技术&#xff0c;它是轻量级且可移植的&#xff0c;主要在Linux和Windows上运行。D…

Linux主机 SSH 通过密钥登录

我们一般使用 PuTTY 等 SSH 客户端来远程管理 Linux 服务器。但是&#xff0c;一般的密码方式登录&#xff0c;容易有密码被暴力破解的问题。所以&#xff0c;一般我们会将 SSH 的端口设置为默认的 22 以外的端口&#xff0c;或者禁用 root 账户登录。其实&#xff0c;有一个更…

换电脑 NoteExpress 数据备份迁移

前言 主要操作是跟着这篇博客做的&#xff1a;NoteExpress数据库备份和转移。但也有一些不一样的地方 旧电脑NoteExpress(NE)版本3.7&#xff0c;新电脑版本3.8 旧电脑 导出配置文件 桌面找到图标&#xff0c;打开位置&#xff0c;点击配置备份&#xff08;绿色的图标&#…