农妇山泉一亩田 发表于 2023-6-23 15:42:06

rust 集合、错误处理、泛型、Trait、生命周期、包

集合组织特性相同的数据;泛型可以定义任何抽象数据类型;生命周期限制所有权的作用域范围;错误处理使程序更健壮。
集合

一组特性相同的数据集合,除了基本数据类型的元组、数组。rust 标准库提供了一些非常有用的数据结构。
Vector 存储列表

通过类型Vec定义。只能存储相同类型的值,在内存中彼此相邻排列存储。
let v:Vec<i32> = Vec::new();通过Vec::new()创建一个类型实例。因为没有初始化任何类型数据,就必须指定数据类型。定义集合实例就只允许存储指定的类型数据。
另一种方便创建集合实例的方式通过 rust 提供的vec!宏
let v = vec!;定义了实例v,可以初始化数据,rust 会推导出数据的类型。示例中默认推导出类型是 i32
可以通过内部方法,操作实例来添加、修改里面的数据

[*]v.push(val) 添加值。
[*]v.get(index)获取值。会得到一个可用于match匹配的Option
也可以使用索引取值&v。使用索引取值,如果超出最大索引,会报错;使用get()方法会返回None
[*]v.insert(index,val) 向指定 index 位置插入数据。
[*]v.remove(index) 移除指定 index 位置的数据,并返回该数据。
[*]v.pop() 移除最后一个元素,并返回。
[*]v.clear() 清空实例。移除所有元素。
[*]v.len() 返回当前数据个数。
要可编辑实例,声明必须使用mut可变。
let mut v:Vec<i32> = vec![];

// 更新值
v.push(23);
v.push(4);
v.push(15);
v.push(56);
//取值
v.get(2); // 4
v; // 4在操作vec时,注意引用所有权的转义。最好的方式就是只是值借用&v。
通过for循环来遍历 vector 中的值。
for i in &v{
    println!("{i}");
}在遍历时实例v不能插入、删除项。如果需要想遍历修改每一项值,可以传递可变引用
for i in &mut v {
    *i += 5;
    println!("{i}");
}因为是对值做操作。通过*解引用取到指针指向的值。再次从实例v取值时,都是最新计算过的值。
通过枚举存储多种类型

因为 vector 只能存储相同类型的值。实际开发中如果需要存储不同类型的值,可以使用枚举定义。
这样对于 vector 而言,它都是同一种枚举类型。
enum Color{
    Red(String),
    Green(u32,u32,u32),
    Green(u32,u32,u32,u8)
}

fn main(){
    let colors = vec!;
    for i in &colors {
      println!("{:?}", i);
    }
}字符串

之前已经通过String::from()来创建一个字符串变量值。字符串是字节的集合。
在 rust 中只有一种字符串类型:字符串 slice str;通常是以借用的方式&str。
作为一个集合,也可以通过 new 操作符创建一个实例。
let mut str = String::new();但是通过 new 创建是的实例不能初始化数据值。所以之前一直使用String::from()
也可以用一个字符串字面值创建 String
let s = "hboot";
let str = s.to_string();字符串是utf-8编码的。可以包含任何可以正确编码的数据。
操作字符串,作为一个集合,也有许多更新的方法:

[*]push_str 尾部附加字符串。不会获得变量的所有权,内部采用字符串 slice。
[*]push 尾部附加字符。
let mut s = String::from("hboot");

s.push_str(" hello");
s.push('A');也可以通过+运算符拼接字符串,运算位加值将会转义所有权,而被加值则必须引用
let s1 = String::from("hboot");
let s2 = String::from("hello");

let s = s1+&s2; // s1的所有权没有了,s2的所有权仍然存在也就是只能是&str和String相加。不能两个String相加,它们类型不同,确定相加是因为 rust 内部把 String 强制转换为&str
当拼接值过多时,我们可以通过format!宏来处理。它不会获取任何字符串的所有权
let s1 = String::from("hboot");
let s2 = String::from(" hello");
let s3 = String::from(" world");

let s = format!("{s1}{s2}{s3}");rust 的字符串不支持索引。

[*]对于字符串值在内存中是以字符编码 code 存储的,而不是字符。通过下标获取到并不是想看到的值。
[*]访问效率不高。通常索引预期复杂度(O(1)),但是在 rust 中,需要需要从头开始到索引位置遍历字符是否有效。
所以遍历字符串最好的方式明确需要的是字符还是字节。字符通过chars方法将其分开并返回多个char类型的值;字节则使用bytes方法返回字符的编码值。
let s1 = String::from("hboot");

// 遍历获取字符
for c in s1.chars() {
    println!("{c}");
}

// 遍历获取字节
for c in s1.bytes() {
    println!("{c}");
}对于字节,有的语言编码后可能不止一个字节组成,这个需要注意。
HashMap 存储键值对

创建HashMap实例,因为 HashMap 没有被 prelude。所以需要手动引入。
use std::collections::HashMap;

fn main(){
    let mut map = HashMap::new();
}当未被使用时,键值对的数据类型是unknown。在第一次插入数据后,则决定了后面的数据类型
let mut map = HashMap::new();

map.insert(1, 10);
map.insert(2, 30);此时默认类型为HashMap。当时用 String 作为键值是,变量的所有权将被转移给 map。字符串变量不可用
let mut map = HashMap::new();

let s = String::from("red");
map.insert(s, "red");通过map.get()获取 HashMap 中的值,返回Option,如果没有键时,则返回None.
可以通过copied()方法来获取Option;如果没有键时,可以通过uwrap_or()在没有键值时,设置一个替代值。
map.get(&String::from("yellow")).copied().unwrap_or('yellow');注意get方法接受是一个&str类型。
当我们重复对同一个键赋值时,后面的会覆盖之前的。如果需要判断是否存在键,不存在插入数据;存在则不做任何操作
map.entry(String::from("green")).or_insert("green");entry的or_insert()方法在键存在时会返回这个值的可变引用。不存在则将参数作为新值插入并返回值的可变引用。
一个示例,通过 HashMap 统计字符串中出现的字符数。
let s1 = String::from("hboot");
let mut map = HashMap::new();
for c in s1.chars() {
    let num = map.entry(c).or_insert(0);
    *num += 1;
}

dbg!("{:?}", map);HashMap默认使用了叫做 SipHash 的哈希函数,可以抵御哈希表的拒绝服务攻击。
泛型、trait 和生命周期

泛型是具体类型和其他属性的抽象替代。定义时不必知道这里实际代表什么,比如之前的实例中的Option / Vec都已经接触了。
泛型

通过定义泛型,可以抽离一些重复的代码逻辑。使得我们的代码更具维护性、适应性更强。
创建一个泛型函数。类型参数声明必须在函数名称和参数列表中间尖括号里面。
fn largest<T: std::cmp::PartialOrd>(list: &) -> &T {
    let mut large = &list;

    for val in list {
      if val > large {
            large = val;
      }
    }

    large
}

fn main(){
    let v1 = vec!;
    let v2 = vec!;

    dbg!(largest(&v1));
    dbg!(largest(&v2));
}实例中为了找出给定 vector 结构数据中的最大值。但是调用的两次结构实例是不同的数据类型i32、f64,使用泛型则可以只写一个公用的函数。
泛型函数中通过遍历结构中的数据进行对比排序。但是泛型是任何类型,存在有的数据类型不能进行排序,rust 在编译阶段会报错。所以增加了泛型限制,std::cmp::PartialOrd 标识传入的类型都可以进行排序。
在结构体使用泛型,作为数据类型。
struct Size<T>{
    width:T,
    height:T
}也可以传入多个泛型,对应不同的字段数据类型Size
在枚举中使用泛型。之前已经使用的枚举Option
enum Status<T, U> {
    YES(T),
    NO(U),
}也可以在结构体、枚举的方法定义中使用泛型。此时需要在impl后声明泛型T
impl<T> Size<T> {
    fn width(&self) -> &T {
      &self.width
    }
}如果在方法中,指定了具体的数据类型,那么创建的实例,不是该数据类型时,则不能调用该方法。
impl Size<u8> {
    fn height(&self) -> &u8 {
      &self.height
    }
}

fn main(){
    let size1: Size<u8> = Size {
      width: 34,
      height: 45,
    };
    let size2: Size<f64> = Size {
      width: 34.12,
      height: 45.34,
    };

    size1.height(); // size1 实例上有height方法。size2则没有
}泛型不会使程序比具体类型运行的慢。rust 通过在编译时进行泛型代码的单态化,也就是重复将泛型声明为具体的定义。
trait定义共同行为

什么是 trait,在之前的描述已多次出现。它定义了某个特定类型拥有可能与其他类型相同的功能。

[*]可以通过trait以一种抽象的方式定义共享的行为。
[*]可以使用trait bounds指定泛型是任何拥有特定行为的类型。
类比接口行为。抽象定义属性、方法,然后其他的实例创建实现接口中的方法。
通过trait定义一个抽象方法。
trait Log {
    fn log(&self)->String;
}声明一个Logtait,包含了一个方法 log。它用来记录实例创建产生行为后日志记录。
每个声明的集合数据都必须实现这个方法。
struct Size<T> {
    width: T,
    height: T,
}

// std::fmt::Debug 是为了打印输出
impl<T: std::fmt::Debug> Log for Size<T> {
    fn log(&self) -> String {
      let str = format!("{:?}-{:?}", &self.width, &self.height);
      println!("变更值:{str}");
      str
    }
}

fn main(){
    let mut size2: Size<f64> = Size {
      width: 34.12,
      height: 45.34,
    };

    size2.width = 45.111;
    size2.log();
}也可以提供一个默认实现,这样可以选择重载这个方法或者保留默认实现。
trait Log {
    fn entry_log(&self) -> String {
      String::from("entry log...")
    }
}然后在其他类型实现 trait 时,可以保留默认的行为。
// 在上方实现的结构体size2,可以直接调用

println!("{}", size2.entry_log());也可以在默认实现中,调用其他方法。
trait Log {
    fn log(&self) -> String;
    fn entry_log(&self) -> String {
      let entry = String::from("entry log...");
      println!("{}", entry);
      // 调用log方法
      let content = self.log();
      format!("{}", content)
    }
}

fn main(){
    // size2 实现不变,仅需要调用entry_log方法即可
    // entry.log();
    size2.entry_log();
}实现了trait这些定义后,如何将其作为参数传递呢。使用impl trait语法
fn notify(item: &impl Log) {
    println!("Log! {}", item.entry_log());
}

fn main(){
    // 通过传递实例 size2直接调用该方法
    notify(&size2);
}也可以通过泛型来定义参数,专业术语称为trait bound
fn notify<T: Log>(item: &T) {
    println!("Log! {}", item.entry_log());
}这种方式在对于多个参数的书写友好。可以通过泛型限制参数的类型。
fn notify<T: Log>(item: &T,item1:&T) {
    println!("Log! {}", item.entry_log());
}也可以通过+指定多个 trait。
fn notify(item: &(impl Log + Display)) {}
// 或者使用泛型
fn notify<T: Log + Display>(item: &T) {}调用传参时的实例则必须实现Log和Display,但是当有很多个 trait 时,书写起来就会很多。
可以通过where关键字简化书写,看起来更加的清晰。
fn notify<T, U>(item: &T, item2: &U)
where
    T: Log + Display,
    U: Clone + Display,
{
}也可以通过函数返回某个实现了trait的类型实例
fn return_log() -> impl Log {
    Size {
      width: 23,
      height: 45,
    }
}

fn main(){
    let size3 = return_log();
    size3.entry_log();
}在闭包和迭代器场景中十分有用。但是这种适用于返回单一类型的情况。
通过trait bound可以有条件的控制实例可调用的类型方法。只有类型实现了某些方法,实例才会有指定的方法。
生命周期

也就是对于引用、借用的有效作用域的限制。在引用或借用之前,保证被引用或借用的变量在当前作用域一直有效。
这个特性避免了悬垂引用,防止了程序引用未定义数据的问题;如下例子:
fn main(){
    let a;
    {
      let b = "admin";
      a = &b;
    }

    println!("{}",a)
}运行cargo run这段代码,将会报错,变量a得到了局部作用域变量b的引用,在最后的作用域中使用了a。但是变量b在局部作用域结束时就已经释放了,导致引用它的a在使用时就会报错。
在 rust 中,通过借用检查器来检测作用域之间的借用是否都是有效的。并在编译阶段给出错误提示,上面的代码不需要运行,也可以看到编译器给出的错误提示。
为了解决上面这问题,我们可以将 b的所有权交出去,因为b作用域结束,并没有什么作用了。
{
    let b = "admin";
    a = b;
}还有在第一篇文章所有权的问题
fn print_info() -> String {
    let str = String::from("hboot");

    // 这是错误的,函数执行完毕,必须交出所有权
    // &str
    // 直接返回创建的字符串
    str
}生命周期注解

还有一些问题,在函数调用的时候,需要传参处理完后返回某个参数的值。如下示例:
fn main(){
    let a = String::from("abcd");
    let b = String::from("efg");
    println!("{}", longest(&a, &b));
}

fn longest(a: &str, b: &str) -> &str {
    if a.len() > b.len() {
      a
    } else {
      b
    }
}编译器直接就会提示错误信息,我们执行cargo run看详细的错误信息。错误也很明确expected named lifetime parameter,并且给出了解决示例。
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() {
      a
    } else {
      b
    }
}'a就是生命周期的注解语法。

[*]它可以给描述多个引用生命周期的关系,而不影响参数a、b的生命周期。
[*]在函数指定了泛型生命周期,函数就可以接受任何生命周期的引用。
[*]生命周期参数以'撇号开头,小写的名称。
[*]它位于引用&之后,它表明了被借用的变量存在的时间和借用变量的生命周期存在的一样久
[*]它保证了在引用值a、b中作用域最短的那个生命周期结束之前有效
需要注意的的就是最后一个,它的存在时间长久在于作用域最短的那一个
fn main(){
    let a = String::from("abcd");

    let result;
    {
      let b = String::from("efg");
      result = longest(&a, &b);
    }
    println!("{}", result);
}函数的调用在b的局部作用域中,调用结束后的结果值result使用超出了b的作用域,编译器报错。
可以把result的使用范围局限在b的作用域内。
{
    let b = String::from("efg");
    result = longest(&a, &b);
    println!("{}", result);
}结构体中的生命周期注解

同设置泛型一样,在结构体名称后面使用简括号声明泛型生命周期。
struct User
页: [1]
查看完整版本: rust 集合、错误处理、泛型、Trait、生命周期、包