您现在的位置是:首页 >技术交流 >研读Rust圣经解析——Rust learn-9(集合,错误处理)网站首页技术交流

研读Rust圣经解析——Rust learn-9(集合,错误处理)

简明编程 2023-05-27 20:00:02
简介研读Rust圣经解析——Rust learn-9(集合,错误处理)

集合

Vector

Vec,也被称为 vector。vector 允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。它们在拥有一系列项的场景下非常实用

创建一个Vec

通过Vec创建

通过使用new进行创建
其实更好的是我们需要指定其中的类型

let a = Vec::new();
let a: Vec<i32> = Vec::new();

通过宏创建

Rust提供了一个宏vec!用于快速创建Vec并指定其中的元素

let a  = vec![];

更新Vector

通过使用push向Vec中添加元素

fn main() {
    let mut a = vec![];
    a.push(65);
    a.push(100);
    println!("{:?}", a);
}

遍历Vec

fn main() {
    let mut a = vec![];
    a.push(65);
    a.push(100);
    for item in a {
        println!("{:?}", item);
    }
}

访问Vec元素

我们直接通过如数组一样的方式直接使用[]就可以获取元素,或者可以通过使用get方法进行获取,但是get方法不会出现数组越界的问题,若越界则根据轮子公式:真实索引位置 = 原始索引位置 % 容量长度

let ele = &a[1];
let ele = a.get(1)

通过枚举存储多种类型

vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举

fn main() {
    enum SpreadsheetCell {
        Int(i32),
        Float(f64),
        Text(String),
    }

    let row = vec![
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("blue")),
        SpreadsheetCell::Float(10.12),
    ];
}

HashMap

哈希 map(hash map)。HashMap<K, V> 类型储存了一个键类型 K 对应一个值类型 V 的映射。它通过一个 哈希函数(hashing function)来实现映射,决定如何将键和值放入内存中。很多编程语言支持这种数据结构

通过使用键就可以找到值

新建一个HashMap

首先引入use std::collections::HashMap;
通过new构建即可,当然更好是要指明存储类型

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
}

为HashMap添加元素

通过使用insert方法向map中增加元素,增加的元素必须要使用一样的类型(当然应用枚举也可以存储不同类型)

map.insert("1", 1);
map.insert("2", 2);
println!("{:#?}", map);

获取HashMap中的元素

我们通过使用get获取到的是Option<&T>若没有值则返回None,然后使用copied方法获取去除值引用的Option<T>接着调用 unwrap_or ,若没有该键所对应的项时将其设置为零

let a = map.get("1").copied().unwrap_or(0);

遍历

在map中遍历得到的应该是键值对的形式

for (k, v) in &map {
        println!("{}", k);
        println!("{}", v);
}

覆盖

通过insert插入一个原始HashMap中有的键名,即可实现覆盖

map.insert("1", 1);
map.insert("1", 2);

只在键没有对应值时插入键值对

entry 函数的返回值是一个枚举,Entry,它代表了可能存在也可能不存在的值。通过or_insert表示没有的时候就插入,有就不做操作

map.entry(String::from("1")).or_insert(1);

统计单词出现次数

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

    let text = "hello world wonderful world";

    let mut map = HashMap::new();

    for word in text.split_whitespace() {
        let count = map.entry(word).or_insert(0);
        *count += 1;
    }

    println!("{:?}", map);
}

错误处理

错误是软件中不可否认的事实,所以 Rust 有一些处理出错情况的特性。在许多情况下,Rust 要求你承认错误的可能性,并在你的代码编译前采取一些行动。这一要求使你的程序更加健壮,因为它可以确保你在将代码部署到生产环境之前就能发现错误并进行适当的处理。

Rust 将错误分为两大类:可恢复的(recoverable)和 不可恢复的(unrecoverable)错误。对于一个可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作。不可恢复的错误总是 bug 出现的征兆,比如试图访问一个超过数组末端的位置,因此我们要立即停止程序。

注意点

Rust没有异常,对应有Result<T,E>用于处理可恢复的错误
对于不可处理的使用panic!宏

处理不可恢复的错误

出现不可恢复的错误,我们要求立即停止程序并告知用户错误信息,这时候我们可以应用panic!宏进行处理,例如在无法打开文件读取的时候,很适合!

panic!("错误提示信息")

在release时直接终止程序

当出现 panic 时,程序默认会开始 展开(unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接终止(abort),这会不清理数据就退出程序

我们通过配置panic为abort采取直接终止不展开

[profile.release]
panic = 'abort'

何时panic!

程序的开发者认为一个错误是不可能恢复处理的就直接panic!决定权在开发者手中

在圣经中给出了如下场景适合使用panic

  1. 示例、代码原型和测试都非常适合 panic
  2. 当我们比编译器知道更多的情况
  3. 错误处理指导原则:在当有可能会导致有害状态的情况下建议使用 panic!
  4. 创建自定义类型进行有效性验证

https://kaisery.github.io/trpl-zh-cn/ch09-03-to-panic-or-not-to-panic.html

处理可恢复的错误

大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反应的原因失败
首先我们认识一下Result枚举

enum Result<T, E> {
    Ok(T),
    Err(E),
}

枚举中有两个字段,Ok和Err,很容易理解

  • Ok:表示程序没问题正常返回
  • Err:表示程序出现错误,反馈调用者进行处理

处理Err

那么设置了Err就要相应进行处理,我们可以通过使用match匹配到Err进行特殊处理

fn run_err(a: i32) -> Result<(),&'static str> {
    if a > 5 {
        return Err("err");
    }
    return Ok(());
}

fn main() {
    let test_err = run_err(6);

    match test_err {
        Ok(()) => println!("success"),
        Err(err) => println!("{}", err)
    }
}

当然你也可以单独处理Err情况(if-let)

if let Err(err) = test_err {
        println!("{}", err);
}

panic的简写形式

使用Result虽然交给调用者进行处理但是不代表调用者不抛出panic,为了更简便,所以应声几种简单的形式

unwrap

  1. unwrap:返回包含的Ok值,消耗self值。 由于此功能可能会引起恐慌,因此通常不鼓励使用它。相反,更喜欢使用模式匹配并显式处理Err情况,或者调用unwrap_or、unwrap_or_else或unwrap_default。
  2. unwrap_err:返回包含的Err值,消耗self值若值是Ok则panic
  3. unwrap_or:返回包含的Ok值或提供的默认值。 传递给unwrap_or的参数被急切地评估;如果要传递函数调用的结果,建议使用unwrap_or_else,它是延迟求值的。
  4. unwrap_or_else:返回包含的Ok值或从闭包中计算它
  5. unwrap_or_default:返回包含的Ok值或默认值 使用self参数,如果Ok,则返回包含的值,否则如果Err,则返回该类型的默认值。

expect

  1. expect:返回包含的Ok值,消耗self值。 由于此功能可能会引起恐慌,因此通常不鼓励使用它。相反,更喜欢使用模式匹配并显式处理Err情况,或者调用unwrap_or、unwrap_or_else或unwrap_default。 恐慌 如果值是Err,则会发生恐慌,其中包含传递的消息和Err的内容的恐慌消息。
  2. expect_err:返回包含的Err值,消耗self值。 恐慌 如果值为Ok,则会出现恐慌,其中包含传递的消息和Ok的内容的恐慌消息

传播错误

当编写一个其实先会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 传播(propagating)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。

其实前面的这个示例其实就是传播了错误return Err("err");

fn run_err(a: i32) -> Result<(), &'static str> {
    if a > 5 {
        return Err("err");
    }
    return Ok(());
}

fn main() {
    let test_err = run_err(6);
}

?运算符

?运算符是传播错误的简写方式
下面的例子中我们将run_err中的错误传递到deal_err中,最后传到main中进行处理

fn run_err(a: i32) -> Result<(), &'static str> {
    if a > 5 {
        return Err("err");
    }
    return Ok(());
}

fn deal_err() -> Result<(),  &'static str> {
    let a = run_err(6)?;
    Ok(a)
}

fn main() {
    let err_msg = deal_err().unwrap_err();
    println!("{}",err_msg);
}

如果 Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行。如果值是 Err,Err 中的值将作为整个函数的返回值,就好像使用了 return 关键字一样,这样错误值就被传播给了调用者。
? 运算符只能被用于返回值与 ? 作用的值相兼容的函数。因为 ? 运算符被定义为从函数中提早返回一个值

更好的函数返回值书写方式

Result<(返回类型),Box<dyn Error>>

Box<dyn Error> 类型是一个 trait 对象,可以将 Box<dyn Error> 理解为 “任何类型的错误”。在返回 Box<dyn Error> 错误类型 main 函数中对 Result 使用 ? 是允许的,因为它允许任何 Err 值提前返回。即便 main 函数体从来只会返回 std::io::Error 错误类型,通过指定 Box<dyn Error>,这个签名也仍是正确的,甚至当 main 函数体中增加更多返回其他错误类型的代码时也是如此。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。