Skip to content

rust study -04 (集合 & 错误处理) #60

@zhuzhh

Description

@zhuzhh

集合

不同于rust内建的数组和元组类型,集合指向的数据是存储在堆上的。
这意味着存储的数据量没必要在编译时就已知,且运行时容量大小可变。
不同的集合类型,适用不同的场景

  • vector 允许一个挨着一个的存储一系列数量可变的值
  • 字符串(string)是字符的集合
  • 哈希 map(hash map)允许我们将值与一个特定的键(key)相关联。这是一个叫做 map 的更通用的数据结构的特定实现

Vector 存储列表
特点:

  • 在内存中彼此相邻地排列所有的值;
  • 只能存储相同类型的值
    let v: Vec<i32> = Vec::new(); // 需要声明 i32 类型
    let v = vec![1, 2, 3]; // Rust 自动推导出v的类型是 Vec(i32)

    // 放入 v 中的所有值都是 i32 类型,Rust 能自动推断出类型,所有不需要 Vec<i32> 注解
    let mut v = Vec::new();
    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
    
    // 读取 vector
    let v = vec![1, 2, 3, 4, 5];
    let third: &i32 = &v[2];
    let third: Option<&i32> = v.get(2);    

在拥有 vector 中项的引用的同时向其增加一个元素

    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];

    v.push(6);

    println!("The first element is: {first}");

会报错,起因是vector工作方式引起的
在 vector 的结尾增加新元素时,在没有足够空间将所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。

遍历

    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }

    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50; // 为了修改可变引用所指向的值,在使用 += 运算符之前必须使用解引用运算符(*)获取 i 中的值。
    }

在遍历时,如果在循环体内加入插入或删除项,则会报错
for 循环中获取的 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),
    ];
}

Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存
第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。

当 vector 被丢弃时,所有其内容也会被丢弃,这意味着这里它包含的整数将被清理。

String 字符串
创建字符串

let mut s = String::new();

let data = "initial contents";

let s = data.to_string();

// 该方法也可直接用于字符串字面值:
let s = "initial contents".to_string();

let s = String::from("initial contents");

String::from.to_string 最终做了完全相同的工作。

字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据。

字符串存储不同语音

    let hello = String::from("السلام عليكم");
    let hello = String::from("Dobrý den");
    let hello = String::from("Hello");
    let hello = String::from("שָׁלוֹם");
    let hello = String::from("नमस्ते");
    let hello = String::from("こんにちは");
    let hello = String::from("안녕하세요");
    let hello = String::from("你好");

更新字符串

let mut s = String::from("foo");
s.push_str("bar");

let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2); // push_str 方法采用字符串slice,因为并不需要获取参数的所有权。
println!("s2 is {s2}"); // s2还可以继续使用
// 如果 push_str 方法获取了 s2 的所有权,就不能在最后一行打印出其值了


// push 只能传入单个字符
s.push('l');
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用
// s1 不能使用引用类型(&s1),否则会报错
let s4 = "sdfsfs".to_string() + &s2;
let s5 = s1 + &"asda".to_string();

+ 运算符使用了 add 函数,签名类似

fn add(self, s: &str) -> String {}

这个语句会获取 s1 的所有权,附加上从 s2 中拷贝的内容,并返回结果的所有权
add 函数被调用时,Rust 使用了一个被称为 Deref 强制转换(deref coercion)的技术,你可以将其理解为它把 &s2 变成了 &s2[..]

&str切片类型;切片——是对数据值的部分引用。

字符串两种表示形式:String::from("test")”test“
Rust 两种字符串类型,str 和 String

str 是 Rust 核心语音类型,是字符串切片(String Slice),常常以引用的形式出现(&str)
凡是用双引号包括的字符串常量整体的类型性质都是 &str:
let s = "hello"; 这里的 s 就是 &str 类型的变量

String 类型是 Rust 标准公共库提供的一种数据类型,它的功能更完善——它支持字符串的追加、清空等实用的操作。
String 和 str 除了同样拥有一个字符开始位置属性和一个字符串长度属性以外还有一个容量(capacity)属性。

Stringstr 都支持切片,切片的结果是 &str 类型的数据

切片的结果是引用类型
let slice = &s[0..3];
简化
let slice = &s[..];

..y 等价于 0..y
x.. 等价于位置 x 到数据结束
.. 等价于位置 0 到结束

多个字符串相加

    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = s1 + "-" + &s2 + "-" + &s3;
    
    // 简化
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = format!("{s1}-{s2}-{s3}");

format!println! 的工作原理相同,不同于将输出打印到屏幕上,它返回一个带有结果内容的 String
format! 使用引用所以不会获取任何参数的所有权

字符串索引

let s1 = String::from("hello");
let h = s1[0];

会报错,字符串不支持索引
这和 Rust 如何在内存中存储字符串有关

String 是一个 Vec的封装

let s = String::from("hola");

索引操作预期总是需要常数时间 (O(1)),但是对于 String 不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符

遍历字符串

for c in "Зд".chars() {
    println!("{c}");
}
// 输出
З
д
for b in "Зд".bytes() {
    println!("{b}");
}
// 输出
208
151
208
180

哈希map
通过一个哈希函数来实现映射,决定如何将键和值放入内存中,

创建哈希 map

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

所有的键必须是相同类型,值也必须都是相同类型

访问hashmap

let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);

get 方法返回 Option<&V>,如果某个键在哈希 map 中没有对应的值,get 会返回 None
程序中通过调用 copied 方法来获取一个 Option<i32> 而不是 Option<&i32>,接着调用 unwrap_or,如果 score 没有对应键的项将 score 设置为零

遍历

for (key, value) in &scores {
    println!("{key}: {value}");
}

哈希 map 和所有权

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

    let field_name = String::from("Favorite color");
    let field_value = String::from("Blue");

    let mut map = HashMap::new();
    map.insert(field_name, field_value);
    // 这里 field_name 和 field_value 不再有效,
    // 尝试使用它们看看会出现什么编译错误!
}

覆盖

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Blue"), 25);

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

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

    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);

    scores.entry(String::from("Yellow")).or_insert(50);
    scores.entry(String::from("Blue")).or_insert(50);

    println!("{:?}", scores); // {"Yellow": 50, "Blue": 10}
}

根据旧值更新一个值

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); // {"world": 2, "hello": 1, "wonderful": 1}

哈希函数
HashMap 默认使用 SipHash的哈希函数,它可以抵御涉及哈希表(hash table)的拒绝服务(Denial of Service, DoS)攻击
然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价
可以指定一个不同的 hasher 来切换为其它函数。hasher 是一个实现了 BuildHasher trait 的类型

错误处理
Rust将错误分为两种:可恢复的和不可恢复的
可恢复错误,使用 Result<T, E>
不可恢复错误,使用 panic! 宏。在程序遇到不可恢复的错误时停止执行

panic!
造成panic的两种方式
1、数组越界等代码逻辑问题
2、显式调用 panic!

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

Cargo.toml修改配置

[profile.release]
panic = 'abort'

使用backtrace

fn main() {
    let v = vec![1, 2, 3];

    v[99];
}

运行RUST_BACKTRACE=1 cargo run
报错信息会展示详细的调用栈
RUST_BACKTRACE 环境变量设置为任何不是 0 的值来获取 backtrace 看看

Result 处理可恢复的错误

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

匹配不同的错误

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file: {:?}", other_error);
            }
        },
    };
}

简化

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

失败时 panic 的简写:unwrap 和 expect

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

如果 Result 值是成员 Ok,unwrap 会返回 Ok 中的值。如果 Result 是成员 Err,unwrap 会为我们调用 panic!

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

expect 与 unwrap 的使用方式一样:返回文件句柄或调用 panic! 宏。expect 在调用 panic! 时使用的错误信息将是我们传递给 expect 的参数,而不像 unwrap 那样使用默认的 panic! 信息。

传播错误

#![allow(unused)]
fn main() {
  use std::fs::File;
  use std::io::{self, Read};
  
  fn read_username_from_file() -> Result<String, io::Error> {
      let username_file_result = File::open("hello.txt");
  
      let mut username_file = match username_file_result {
          Ok(file) => file,
          Err(e) => return Err(e), // 提前结束函数,并把来自 File::open 的错误值作为函数的错误值返回给调用者
      };
  
      let mut username = String::new();
  
      match username_file.read_to_string(&mut username) {
          Ok(_) => Ok(username),
          Err(e) => Err(e),  // 因为这是函数的最后一个表达式,所以不需要显式的调用 return
      }
  }
}

简写—— 使用 ? 运算符

use std::fs::File;
use std::io::{self, Read};

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

使用 ? 运算符向调用者返回错误的函数

Result 值之后的 ?,和上面 match 表达式相同的工作方式
如果 Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行。如果值是 ErrErr 中的值将作为整个函数的返回值,就好像使用了 return 关键字一样,这样错误值就被传播给了调用者。

match 表达式和 ? 运算符的不同点:?运算符会把错误值传递给 from 函数,其用来将错误从一种类型,转为另一种类型。当 ? 运算符调用 from 函数时,收到的错误类型被转换为由当前函数返回类型所指定的错误类型

链式调用

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();

    File::open("hello.txt")?.read_to_string(&mut username)?;

    Ok(username)
}

?运算符使用场景
?运算符只能被用于返回值 与 ? 作用的值相兼容的函数
? 运算符被定义为从函数中提早返回一个值

? 要返回的错误类型,必须要和函数声明需要的返回类型一致
可用于 Result<OK, Err>、Option

注意你可以在返回 Result 的函数中对 Result 使用 ? 运算符,可以在返回 Option 的函数中对 Option 使用 ? 运算符,但是不可以混合搭配。? 运算符不会自动将 Result 转化为 Option,反之亦然;

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")?;
}

以上代码编译会报错
函数返回值类型不是 Result
错误指出只能在返回 Result 或者其它实现了 FromResidual 的类型的函数中使用 ? 运算符
修复问题有两种方法,一是将函数的返回值改为 Result<T, E>;二是使用 matchResult<T, E> 的方法中合适的一个来处理 Result<T, E>

main 函数比较特殊,他是可执行程序的入口点和退出点
main函数可以返回 Result<(), E>

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;

    Ok(())
}

Box<dyn Error> 是一个trait 对象,目前可以理解为是 任何类型的错误

如果 main 返回 Ok(()) 可执行程序会以 0 值退出
如果存在 error,则会返回非 0 的整数

main 函数也可以返回任何实现了 std::process::Termination trait 的类型,它包含了一个返回 ExitCode 的 report 函数

要不要panic!

当你有一些其他的逻辑来确保 Result 会是 Ok 值时,调用 unwrap 或者 expect 也是合适的

    use std::net::IpAddr;

    let home: IpAddr = "127.0.0.1"
        .parse();
    // 虽然使用硬编码,存在一个有效的字符串也不能改变 parse 方法的返回值类型是 Result 类型
    // 所以这里还是会报错

    // 修复问题
    let home: IpAddr = "127.0.0.1"
        .parse()
        .expect("Hardcoded IP address should be valid");

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions