Rust | 02. 包管理、集合(String、Vector、HashMap)、错误处理

  • 代码组织需要包括哪些细节可以暴露,哪些细节可以私有,作用域内哪些名称有效等等;Rust的代码组织(也可以叫模块系统)如下:
    • Package(包),是Cargo的特性,让你构建、测试、共享crate
    • Crate(单元包),一个模块树,它可以产生一个library或可执行文件
    • Module(模块)、use,让你控制代码的组织、作用域、私有路径
    • Path(路径),是为Struct、function、module等项命名的方式
  • Crate有两种类型,即binary或library;Crate root是指源代码文件,Rust编译器会从这里开始,组成你Crate的根Module;一个Package包含了一个Cargo.toml文件(用于描述了如何构建这些Crates),一个package只能包含0-1个library crate,以及任意数量的binary crates,另外,一个library至少包含一个crate(library或binary)

  • 通常而言,src/main.rs是binary crate的crate root,且crate名与package名相同;而在library crate中,则会生成一个叫做src/lib.rs的文件,其是该library crate的crate root,此时crate的名字也与package名相同;一个package中也可以同时这两个文件,此时该package中包含两个crate,一个binary一个library,都与package名相同;一个package可以有多个binary crate,文件放在src/bin目录下,每个文件是一个单独的binary crate

  • Crate的作用:将相关功能组合到一个作用域内,便于在项目间共享,防止冲突;例如rand crate,就可以使用它的名字访问其相关功能

  • 定义Module可以控制作用域和私有性:在一个crate内,将代码进行分组,增强可读性,易于复用,可以使用public、private关键字控制项目(item)的私有性;使用mod关键字建立module,其可以嵌套,类似于如下的实例

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    mod front_of_house{
        mod hosting{
            fn add_to_waiting(){ }
            fn seat_at_table(){ }
        }
    
        mod serving{
            fn take_order(){ }
            fn serve_order(){ }
            fn take_payment(){ }
        }
    }
    
  • 路径用于在rust中找到某个模块,有两种形式:

    • 绝对路径,从crate root开始,使用crate名或字面值"crate"
    • 相对路径,从当前模块开始,使用self、super或当前模块标识符
  • 路径至少由一个标识符组成,标识符之间用::分隔,例如

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    // src/lib.rs
    
    mod front_of_house{
        // 需要声明公开,否则无法外部访问
        pub mod hosting{
            pub fn add_to_waiting(){ }
        }
    }
    
    pub fn eat_at_restaurant(){
        // 使用绝对路径,从顶部crate起始,逐级向下(使用crate字面量)
        crate::front_of_house::hosting::add_to_waiting();
    
        // 相对路径,因为和顶级模块在同一级,可从模块名开始调用
        front_of_house::hosting::add_to_waiting();
    }
    
  • PS:定义的位置和使用的位置始终一起移动时,可以选择使用相对路径;但为了避免错误,有经验的程序员会直接写成绝对路径

  • 模块不仅仅可以组织代码,还可以定义私有边界;Rust中的所有条目(函数、方法、struct、模块、常量)都是私有的;父级模块无法访问子模块中的私有条目,而在子模块中可以使用所有祖先模块中的条目

  • 使用pub关键字可以使模块对外暴露;根级模块可以相互调用,无论共有还是私有

  • super关键字用于访问父级模块中的内容,相当于文件系统中的两个点..

    1
    2
    3
    4
    5
    6
    7
    8
    
    fn serve_order(){ }
    mod back_of_house{
        fn fix_incorrect_order(){
            cook_order();
            super::serve_order(); //相对路径
        }
        fn cook_order(){ }
    }
    
  • 当将pub关键字放在struct前,struct就是公共的,但字段如果没有加pub修饰时,字段仍旧是私有的;即是说,struct的字段需要单独设置pub来变成共有

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    mod back_of_house {
        pub struct Breakfast {
            pub toast: String,
            seasonal_fruit: String,
        }
    
        impl Breakfast {
            pub fn summer(toast: &str) -> Breakfast {
                Breakfast{
                    toast: String::from(toast),
                    seasonal_fruit: String::from("peaches"),
                }
            }
        }
    }
    
    pub fn eat_at_restaurant(){
        let mut meal = back_of_house::Breakfast::summer("Rye"); // 使用关联函数初始化
        meal.toast = String::from("Wheat"); //pub公共字段可以修改
        println!("I'd like {} toast please", meal.toast);
        meal.seasonal_fruit = String::from("Blueberries"); // 这一句会报错,因为season_fruit是私有的
    }
    
  • 使用pub关键字在enum前,可以把枚举类型变为公共类型,需要注意的是,当枚举类型为公共时,其变体默认也为公共类型(只有公共的才有用)

  • 可以使用use关键字将路径导入到当前作用域内(类似Linux的符号链接),但仍旧遵循私有原则

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    mod front_of_house {
        pub mod hosting {
            pub fn add_to_waitlist(){ }
        }
    }
    
    use crate::front_of_house::hosting; // 使用绝对路径
    use front_of_house::hosting; // use也可以使用相对路径
    
    pub fn eat_at_restaurant(){
        hosting::add_to_waitlist();
        hosting::add_to_waitlist();
        hosting::add_to_waitlist();
    }
    
  • 在上面的案例中,有经验的程序员在引入一个函数时,是引入到函数的上一级,而不是直接到函数名(crate::front_of_house::hosting::add_to_waitlist),这是想要强调,该函数是在hosting模块下的,如果直接引入函数名,则会与本地定义函数混淆、无法区分

  • 但对于引入struct、enum类型时,可以直接指定完整路径到其本身;但对于同名条目,还是需要指定到父级,否则会混淆

    1
    2
    3
    4
    5
    
    use std::collections::HashMap;
    fn main(){
        let mut map = HushMap::new();
        map.insert(1, 2);
    }
    
    1
    2
    3
    4
    5
    
    use std::fmt;
    use std::io;
    fn f1() -> fmt::Result { }
    fn f2() -> io::Result{ }
    fn main() { }
    
  • as关键字可以为引入的路径指定一个本地的别名

    1
    2
    3
    4
    5
    
    use std::fmt::Result;
    use std::io::Result as IoResult;
    fn f1() -> Result { }
    fn f2() -> IoResult { }
    fn main() { }
    
  • 使用use将路径(名称)导入到作用域后,该名称在此作用域内是私有的。例如下面的代码,函数eat_at_restaurant()对于外部代码而言是公开的;但名称hosting却对外面是默认私有的,需要在use语句前使用pub关键字,将其公开

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    mod front_of_house {
        pub mod hosting {
            pub fn add_to_waitlist(){ }
        }
    }
    
    pub use crate::front_of_house::hosting;
    
    pub fn eat_at_restaurant(){
        hosting::add_to_waitlist();
        hosting::add_to_waitlist();
        hosting::add_to_waitlist();
    }
    
  • 小结一下,pub use的作用就是重导出,其不仅仅将条目引入当前作用域,也使该条目可以被外部代码引入到他们的作用域内

  • 可以使用嵌套语句,来在同一行内同时引入相同包/模块下的多个条目,其格式为:

    use 路径相同部分::{路径, 不同, 部分}
    1
    2
    3
    4
    
    use std::cmp::Ordering;
    use std::io;
    
    use std::{cmp::Ordering, io};
    
    1
    2
    3
    4
    
    use std::io;
    use std::io::Write;
    
    use std::io::{self, Write};
    
  • 可以使用通配符*,可以将路径中所有公共条目都引入到当前作用域内;需要注意的是,通配符需要谨慎使用,其仅仅在测试阶段(将所有需要测试代码引入到tests模块)或预导入模块(prelude)

    1
    
    use std::collections::*;
    
  • Cargo.toml文件中添加外部包的名字和版本号,cargo build时就会自动从https://crates.io/下载到本地环境,随后便可使用use将特定条目引入到作用域

  • 可以在\Users\user\.cargo\config目录下写配置文件来更换清华源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    [source.crates-io]
    registry "https://github.com/rust-lang/crates.io-index"
    
    replace-with 'tuna'
    [source.tuna]
    registry "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
    
    [net]
    git-fetch-with-cli true
    
  • Rust的标准库std也被当做是外部包,需要使用use引入特定条目到当前作用域,但因其本身写入到rust语言本体之中,不需要修改Cargo.toml文件来包含std

  • 定义模块时,模块名后可以直接写分号;,而不是代码块,此时Rust会从与模块同名的文件中加载内容(即实现了将模块拆分到不同的文件内)

    1
    2
    3
    4
    5
    6
    7
    8
    
    src 
    ├─ lib.rs
       mod front_of_house{
           pub mod hosting;{
               pub fn add_to_waitlist(){ }
           }
       }
    └─ main.rs
    
    1
    2
    3
    4
    5
    6
    7
    8
    
    src
    ├─ front_of_house.rs
       pub mod hosting;{
           pub fn add_to_waitlist(){ }
       }
    ├─ lib.rs
       mod front_of_house;
    └─ main.rs
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    src
    ├─ lib.rs
       mod front_of_house;
    ├─ main.rs
    ├─ front_of_house.rs
       mod hosting;
    └─ front_of_house
        └─ hosting.rs
            pub fn add_to_waitlist(){ }
    
  • 可以使用Vector类型存储 类型相同的多个值,写作Vec<T>,其由标准库提供

    1
    2
    3
    4
    
    fn main(){
        let v: Vec<i32> = Vec::new();
        // 因为初始化时为空,编译器无法推断类型,需要手动标注i32类型
    }
    
    1
    2
    3
    4
    
    fn main(){
        let v = vec![1, 2, 3];
        // 使用宏vec!直接初始化,此时不需要显式指定类型
    }
    
  • 使用push()函数向其中添加元素

    1
    2
    3
    4
    5
    
    fn main(){
        let v = Vec::new();
        v.push(1); // 由于向其中添加了值,此时编译器可以推断类型
        v.push(2);
    }
    
  • 使用索引或get()方法获得其中的元素,索引从0开始,超出索引范围后会引发程序崩溃,而get方法返回Option枚举类型,需要手动处理

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    fn main(){
        let v = vec![1,2,3,4,5];
        let third: &i32 = &v[2];
        println!("The third element is {}", third);
    
        match v.get(2) {
            Some(third) => println!("The third element is {}", third),
            None => println!("There is no third elemrnt"),
        }
    }
    
  • 所有权和借用规则要求,不能在同一作用域内同时拥有可变和不可变引用,该规则仍旧适合vector;因为Vector的其元素成集合统一存储在heap上,因而不能只考虑一个元素是否可变,而需要关注整个vector是否可变

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    fn main(){
        let mut v = vec![1,2,3,4,5];
        let first = &v[0];
        v.push(6); // 因为前一行定义为不可变引用,这一行会报错
        println!("The first element is {}", first);
    
        // 需要修改时,需要界定为可变引用
        for i in &mut v{
            *i += 50;
        }
    
        for i in v{
            println!("{}", i);
        }
    }
    
  • 在Vector中只能存放相同类型的数据,当有需求存储不同类型数据时,可以基于枚举的如下特性,利用附加数据的枚举类型来完成

    • 枚举的变体可以附加不同类型的数据

    • 枚举变体的定义在同一个枚举类型下

    • PS:在使用该方法定义Vector时,需要知道详尽的数据类型,不能存在类型未知的情况;Trait对象对于解决该问题有帮助

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      
      enum SpreadsheetCell {
          Int(i32),
          Float(f64),
          Text(String),
      }
      
      fn main(){
          let row = vec![
              SpreadsheetCell::Int(3),
              SpreadsheetCell::Text(String::from("blue")),
              SpreadsheetCell::Float(3.5),
          ]
      }
      
  • 字符串是Byte的集合,能够提供一些方法将byte解析为文本的类型,在Rust中,通常意义上所说的字符串是指String类型或&str类型;此外,标准库中也还提供了许多其他的字符串类型,如OsString、OsStr、CString、CStr等

  • 据说Rust的字符串很劝退,原因可能在于字符串本身的复杂性、Rust倾向于暴露可能的错误、Rust使用utf-8编码

  • 在Rust的核心语法层面,只有一个字符串类型,是字符串切片(str或&str),其是对储存在其他地方、utf-8编码的字符串的引用

  • 而字符串String是来自标准库,是可增长、可修改、可拥有的类型;由于其本身就是Byte的集合,因而许多Vec<T>的操作也都可用于String,可以使用String::new()创建字符串,也可以直接使用to_string()方法(该方法可用于实现了Display trait的类型,包含字符串字面值)或String::from()函数创建初始化一个字符串

    1
    2
    3
    4
    5
    6
    
    fn main(){
        let data = "initial contents ⭐";
        let s1 = data.to_string();
    
        let s2 = String::from("initial contents");
    }
    
  • String的大小是可以增减的,其内容也可以更改

    • 使用push_str()方法可以向字符串尾部添加内容,该方法的参数是字符串切片(因而可以直接填写字面值),因为是传入引用,是借用字符串,不获取其所有权,因而修改后的字符串仍旧可以正常使用

    • 使用push()方法,可以把单个字符添加到字符串尾部

      1
      2
      3
      4
      5
      6
      
      fn main(){
          let mut s = String::from("foo");
          s.push_str("bar");
          s.push('x');
          println!("{}", s);
      }
      
    • 使用+运算符直接拼接字符串,其相当于使用了类似签名fn add(self, s:&str) -> String {...}这样的方法(实际上是一个泛型,会更复杂),因而需要加号前为字符串,加号后为字符串切片,在相加后前面的变量所有权移交到add内部,离开后会失效,而后面的变量则可以继续保留所有权而可以继续使用

      1
      2
      3
      4
      5
      6
      7
      
      fn main(){
          let s1 = String::from("Hello, ");
          let s2 = String::from("World!");
      
          let s3 = s1 + &s2;
          println!("{}", s3);
      }
      

      PS:在该例子中,加号后面使用&s2是字符串引用类型,并不是上面要求的字符串切片,这是因为Rust存在一个解引用强制转换(deref coercion)的机制

    • 也可以使用format!宏来完成字符串的拼接,其更灵活也不会取得任何参数的所有权

      1
      2
      3
      4
      5
      6
      7
      8
      
      fn main(){
          let s1 = String::from("tic");
          let s2 = String::from("tac");
          let s3 = String::from("toe");
      
          let s = format!("{}-{}-{}", s1, s2, s3);
          println!("{}", s);
      }
      
  • 按照索引语法访问String的某部分时会报错,即是说Rust字符串不支持索引语法访问;实际上,String是对Vec<u8>的包装,可以使用len()方法返回其字节长度

    1
    2
    3
    4
    5
    6
    7
    
    fn main(){
        let len1 = String::from("abcdefg").len();
        let len2 = String::from("æɑðɜɣʝʪ").len();
    
        println!("{}, {}", len1, len2);
        // 输出为7, 14
    }
    

    之所以输出24,是因为里面的每个字母占用了2字节;这里所谓的字母称之为Unicode标量值,即是说,rust中不是所有的字节索引都能对应到一个有效的Unicode标量值,因而Rust在开发早期就为了杜绝此类错误,直接拒绝了使用索引值访问

  • Rust有三种看待字符串的方式:字节Bytes、标量值Scalar values和字形簇Grapheme cluster;其中字形簇才是最接近我们所理解的“字母”;可以使用bytes()chars()方法获得其字节和标量值的迭代器,用于遍历字符串;对于字形簇的获取,其很复杂,标准库为提供

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    fn main(){
        let s = "æɑðɜɣʝʪ";
    
        for b in s.bytes(){
            println!("{}", b);
        }
    
        for b in s.chars(){
            println!("{}", b);
        }
    }
    
  • 此外,Rust不允许对String进行索引,还因为每次索引操作都消耗一个常量时间O(l),而String无法保证这个时间,因为要确认这个时间需要遍历整个字符串来计算有多少个合法字符

  • 可以使用[]和一个范围来创建字符串的切片(切割字符串),但是需要谨慎使用,如果切割时跨越了字符边界,程序就会panic

    1
    2
    3
    4
    5
    
    fn main(){
        let s = "æɑðɜɣʝʪ";
        let s1 = &s[0..4]; //可以切割出两个字符
        let s2 = &s[0..3]; //编译不会报错,运行会报错,说索引3不是一个字符的边界
    }
    
  • 小结,Rust选择将正确处理String作为程序员的默认行为,要求程序员必须在处理utf-8数据之前投入更多精力,从而防止在开发后期处理涉及非ASCII字符时的错误

  • 哈希表(HashMap)是以键值对的形式存储数据的一种方式(Heap上存储),相当于python或C#中的字典,在rust中以HashMap<K, V>来表示;其不在prelude中,需要手动导入

  • 可以使用new()函数创建HashMap,使用insert()方法插入键值对,如果没有数据初始化或随后插入数据的话,需要指定数据类型;HashMap是同构的,其所有的Key类型相同,所有的Value类型相同;标准库对其支持很少,没有内置的宏来创建HashMap

    1
    2
    3
    4
    5
    6
    
    use std::collection::HashMap;
    
    fn main() {
      let mut score: HashMap<String, i32> = HashMap::new();
      score.insert(String::from("Blue"), 10);
    }
    
  • 也可以使用collect()方法来创建HashMap,需要手动指定collect之后的数据类型

    1
    2
    3
    4
    5
    6
    
    use std::collection::HashMap;
    fn main() {
      let teams = vec![String::from("Blue"), String::from("Yellow")];
      let intial_scores = vec![10,50];
      let scores: HashMap<_,_> = teams.iter().zip(intial_scores.iter()).collect();
    }
    
  • HashMap和所有权:

    • 对于实现了Copy trait的类型(例如i32),值会被复制到HashMap中
    • 对于拥有所有权的值(例如String),值会被移动,所有权会转移给HashMap
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    use std::collection::HashMap;
    fn main() {
      let field_name = String::from("Favorite color");
      let field_value = String::from("Blue");
      let mut map = HashMap::new();
    
      map.insert(field_name, field_value);
      println!("{}, {}", field_name, field_value); // 所有权移交HashMap,值会失效
    
      map.insert(&field_name, &field_value);
      println!("{}, {}", field_name, field_value); // 插入值的引用,值本身不会移动
      // 在HashMap有效的期间,被引用的值必须保持有效
    }
    
  • HashMap值的访问可以使用get()方法,其可以返回一个Option<T>枚举类型;可以使用for循环遍历HashMap的引用,保留其所有权

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    use std::collection::HashMap;
    fn main() {
      let mut scores = HashMap::new();
      scores.insert(String::from("Blue"), 10);
      scores.insert(String::from("Yellow"), 50);
    
      let team_name = String::from("Blue");
      let score = scores.get(&team_name);
    
      match score {
        Some(s) => println!("{}", s),
        None => println!("Team not exist"),
      };
    
      for (k, v) in &scores {
        println!("{}: {}", k, v);
      }
    }
    
  • HashMap的更新:

    • 可以直接插入(k, v),当k相同时,v会将原来值覆盖掉
    • 使用entry检查指定的k是否有对应的v,其参数为k,返回值为enum Entry类型(存在时返回值的可变引用),配合or_insert()方法来更新值
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    use std::collection::HashMap;
    fn main() {
      let mut scores = HashMap::new();
      scores.insert(String::from("Blue"), 10);
      scores.insert(String::from("Blue"), 50);
      println!("the value of {} is {}", k, v);
    
      scores.entry(String::from("Yellow")).or_insert(50);
      scores.entry(String::from("Blue")).or_insert(70);
      println!("{:?}", scores);
    }
    
  • 一个文本计数的案例

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    use std::collection::HashMap;
    fn main() {
      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);
    }
    
  • hash函数决定了如何在内存中存放K和V,标准库中的HashMap使用带加密功能的Hash函数,可以抵抗拒绝服务攻击(DoS),其不是最快的,但更安全。可以指定不同的hasher来更换Hash函数(hasher是实现BuildHasher trait的类型)

  • Rust是高可靠语言,在大部分时候,在编译阶段就会提示错误,并要求处理;在Rust中,没有异常处理机制,而是使用Result<T,E>泛型来处理可恢复错误,使用panic!宏处理不可恢复错误
  • 通常来说,错误可以分为两大类:
    • 可恢复错误,如文件未找到
    • 不可恢复错误,即bug,如访问索引超出范围
  • panic!()可以是自己写的,也可以是程序依赖代码调用的;使用set RUST_BACKTRACE=1 && cargo run命令可以在执行代码的同时查看到错误的回溯信息(找到panic!的发生位置)

    1
    2
    3
    4
    5
    
    fn main{
      // panic!("Stop Program!");
      let v = vec![1,2,3];
      v[100]
    }
    
  • panic!执行时,程序会做以下操作:打印错误信息、展开(unwind)并清理调用栈(stack)、退出程序;展开调用栈的操作意味着Rust沿着调用栈往回走,清理而每个遇到的函数中的数据,其工作量巨大;可以手动设置立即中止(abort)调用栈,不由程序进行清理而是直接停止程序,交由操作系统对内存进行清理,这样的配制可以让二进制文件更小;设置的方法是在Cargo.toml中修改profile设置,添加panic=‘abort’即可

    1
    2
    
    [profile.release]
    panic = 'abort'
    
  • panic!的使用场景是,当你认为你可以代替调用者,判断出错误不可恢复时,才会使用;通常情况下,在定义一个可能失败的函数时,优先考虑返回Result。在建议的规范中,只有很少数情景会使用panic!,如编写实例演示概念、原型代码(如编写unwrap或expect)、使用unwrap()或expect()在测试代码中标记错误

  • 有的时候,你可以获得比编译器多的信息,肯定其结果不会发生错误的时候,也可以使用unwrap()处理数据类型的转化,如下场景中,手动输入的IP地址一定是对的,但parse()方法仍旧返回Result类型,可以直接使用unwrap()获取需要的结果

    1
    2
    3
    4
    
    use std::net::IpAddr;
    fn main(){
      let home: IpAddr = "127.0.0.1".parse().unwrap();
    }
    
  • 当代码最终可能处于损坏状态(bad state)时,最好使用panic!(),此时某些假设、保证、约定或不可变性被打破,例如非法的值、矛盾的值或空缺的值被传入或如下可能的场景

    • 破坏状态并不是预期能够偶尔发生的事情
    • 在此之后,代码处于破坏状态就无法运行了
    • 在所使用的类型中,没有一个好的方法来编码这些信息 一些具体的例子:
    • 调用你的代码,传入无意义的参数值:panic!
    • 调用外部不可控代码,返回非法状态,你无法修复:panic!
    • 如果失败是可预期的:Result
    • 当你的代码对值进行操作,首先应该验证这些值的合法性,不合法时:panic!
  • 为了验证值的合法性,可以创建新的类型,把验证逻辑放在实例的构造函数里;这里对之前猜数游戏的案例做部分修改

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    pub struct Guess{
      value: i32, //私有的,只提供get不提供set,确保值只能通过new()设置,必须经过检查
    }
    impl Guess {
      //关联函数new(),作为构造函数
      pub fn new(value: i32) -> Guess{
        if value > 1 || value > 100 {
          panic!("Guess value must be between 1 and 100, got {}", value);
        }
        Guess { value }
      }
      pub fn value(&self) -> i32 {
        self.value
      }
    }
    fn main() {
      loop {
        // ...
        let guess = "32";
        let guess: i32 = match guess.trim().parse(){
          Ok(num) => num, 
          Err(_) => continue,
        };
        let guess = Guess::new(guess); //在创建实例的时候验证,如果创建成功则说明通过验证
      }
    }
    

其定义结构如下,有两个变体,分别绑定了一个泛型参数。可以理解为,T为操作成功情况下,Ok变体里返回的数据的类型,E为操作失败情况下,Err变体里返回的错误的类型。使用match处理Result,当需要解析错误时也可以使用match做进一步的区分。

1
2
3
4
enum Result<T, E>{
  Ok(T),
  Err(E),
}
  • 例如下面的例子,File::open()返回的结果就是一个Result枚举类型,如果文件存在并读取成功,其返回的是File类型,如果不存在则返回Err;使用match处理Result,值得关注的是,Result及其变体类型,同Option一样,由prelude带入作用域,不需要额外手动标注;可以使用unwrap()方法来快速match不同类型并返回系统报告信息(match+panic!);也可以使用expect()方法在解析错误的同时打印自定义的错误信息

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    use std::fs::File;
    fn main() {
      let f = File::open("hello.txt");
      let f = match f {
        Ok(file) => file,
        Err(error) => { panic!("Error opening file {:?}", error); },
      };
      let f = File::open("hello.txt").unwrap();
      let f = File::open("hello.txt").expect("无法打开文件");
    }
    
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    use std::fs::File;
    fn main() {
      let f = File::open("hello.txt");
      let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
          ErrorKind::NotFound => match File::create("hello.txt") {
            Ok(fc) => fc,
            Err(e) => panic!("Error creating file: {:?}". e),
          },
          other_error => panic!("Error opening file {:?}", other_error);
        }
      };
    }
    
    • 在本例中嵌套了好几层match,其代码不够直观,之后会学习闭包(closure)来优化这一段代码(调用unwrap_or_else()
    1
    2
    3
    4
    5
    6
    7
    8
    
    let f = File::open("hello.txt").unwrap_or_else(|error| {
      if error.kind() == ErrorKind::NotFound {
        File::create("hello.txt").unwrap_or_else(|error| {
          panic!("Error creating file:{:?}", error);
        })
      } else{
        panic!("Error opening file:{:?}",error);
      });
    
  • 传播错误:在错误发生时,不仅仅可以在函数中处理该错误,也可以把错误返回给函数的调用者,让其解决该错误(相当于java中的throw),其代码逻辑如下

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    use std::fs::File;
    use std::io;
    use std::io::Read;
    
    fn read_from_file() -> Result<String, io::Error> {
      let f = File::open("hello.txt");
      let f = match f {
        Ok(file) => file,
        Err(e) => return Err(e), //中止函数并返回
      };
    
      let mut s = String::new();
      match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
      } // 这里没有分号,说明这是一个语句,作为函数的返回结果
    }
    
    fn main(){
      let result = read_from_file();
    }
    
  • Rust提供?操作符来快速解析结果并抛出异常,其效果等同于match+return;这里值得注意的是,?运算符只可以用在返回Result类型的函数中,其在执行时,会隐式地将捕获到的错误交由from函数进行处理和转化,将捕获到的错误类型转化为当前函数返回类型所定义的错误类型;from是Trait std::convert::From上的函数,用于错误之间的转化,只有此错误类型实现了目标类型的from函数,?才可以将不同错误原因,返回为同一种错误类型;此外,?还支持链式调用

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    use std::fs::File;
    use std::io;
    use std::io::Read;
    
    fn read_from_file() -> Result<String, io::Error> {
      let mut s = String::new();
      let mut f = File::open("hello.txt")?;
      f.read_to_string(&mut s)?;
      Ok(s)
    }
    
    fn read_from_file() -> Result<String, io::Error> {
      let mut s = String::new();
      File::open("hello.txt")?.read_to_string(&mut s)?;
      Ok(s)
    }
    
    fn main() -> Result<(), Box<dyn Error>>{
      let f = File::open("hello.txt")?;
      Ok(())
    }
    

    main()函数需要标注返回值类型才可以使用?Box<dyn Error>是一个trait对象,可以理解为任何可能的错误类型