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

news/2024/11/26 6:04:38/

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句柄赋值给fOk(file) => file,// 打开文件失败,将错误返回(向上传播)Err(e) => return Err(e),};// 创建动态字符串slet 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.ppmy.cn/news/43081.html

相关文章

电子招投标系统源码之了解电子招标投标全流程

随着各级政府部门的大力推进&#xff0c;以及国内互联网的建设&#xff0c;电子招投标已经逐渐成为国内主流的招标投标方式&#xff0c;但是依然有很多人对电子招投标的流程不够了解&#xff0c;在具体操作上存在困难。虽然各个交易平台的招标投标在线操作会略有不同&#xff0…

话说~~ HTTP协议请求的工作流程 (Web服务请求过程)最细详解

目录 文章导入 &#xff1a; 概述 &#xff1a; 详解过程 &#xff1a; DNS域名解析 &#xff1a; DNS 域名解析过程分析 &#xff1a; TCP连接搭建 &#xff1a; 等待TCP队列 建立TCP连接 发起 HTTP 请求 &#xff1a; # 哪是如何进行 HTTP 请求的呢 &#…

DDD解决了什么问题

前言 前面我们有文章介绍了架构设计的目标以及DDD相关的领域知识&#xff0c;本章重点会介绍DDD到底在架构设计中解决了什么问题。 1. 系统复杂性的6个来源以及通用解法 架构设计的目标是管理系统复杂性以及高效率匹配资源。那系统复杂性源于好处呢&#xff1f; 1.1 高性能…

【音视频第8天】mediasoup拥塞控制【未完待续】

WebRTC的拥塞控制方式主要有以下几个&#xff1a;Transport-cc、BBR-congestion、remb&#xff08;BBR已被google从webrtc移除了&#xff09;。mediasoup支持Transport-cc和remb。 一、前言 实时通信的延时指标 视频服务质量指标 音视频服务质量与带宽之间的矛盾、实时性与服…

接口的使用

目录 接口的基本介绍 接口的注意事项和使用细节 接口vs继承 接口体现多态 接口体现多态数组 接口体现多态传递现象 接口的习题 第一题 第二题 接口的基本介绍 接口就是给出一些没有实现的方法&#xff0c;封装到一起&#xff0c;到某个类要使用的时候&#xff0c;在根…

Office365初始化配置部署方案

目录 一、前言 二、Office 365 配置和部署的基本概念 三、Office 365 配置和部署的步骤 1、准备工作

企业云盘如何实现文件共享?

企业文件共享的方式有很多&#xff0c;最常见的就是使用第三方企业云盘工具进行文件实时共享&#xff0c;这种方法不仅方便安全&#xff0c;而且兼容性高。 企业云盘主要是通过建立企业内部共享文件夹进行文件分享&#xff0c;支持通过权限管控来保障文件的安全&#xff0c;管理…

一文吃透低代码平台源代码交付的重要性(避坑指南)

一、前言 作为这两年IT界的风口&#xff0c;低代码在众人眼里已经不是什么陌生的概念。 对标于传统的纯代码开发&#xff0c;低代码是一种快速开发软件&#xff08;应用程序&#xff09;的方法&#xff0c;平台通过对大量功能与场景做提前封装&#xff0c;使得用户可以在可视…