rust 基础知识2

178 阅读8分钟

模块

rust 模块分为:

  • package
    • 至少包含一个 cargo.toml 文件
    • 默认情况下,src/main.rspackage 的入口
  • crate
    • 在创建项目时 cargo new --libsrc/lib.rscrate 的入口
    • rust 最小编译单元
  • module
    • 使用 mod 关键字定义

如果这个模块需要被外部访问,需要使用 pub 关键字修饰

mod foo {
    pub mod bar {
        pub fn baz() -> String {
            "Hello, world!".to_string()
        }
    }
}
fn main() {
    println!("{}", foo::bar::baz());
}

可见性

rust 中模块内部的代码,结构体,函数等类型默认是私有的,通过 pub 关键字可以将其公开

  • pub:成员对模块可见
  • pub(self):成员对模块内的子模块可见
    mod foo {
        pub(self) const NUMBER: i32 = 10;
        pub mod bar {
            pub fn get_number() -> i32 {
                super::NUMBER
            }
        }
    }
    fn main() {
        println!("{}", foo::bar::get_number());
    }
    
  • pub(crate):成员对整个 crate 可见
    mod foo {
        pub(crate) const NUMBER: u32 = 10;
    }
    fn main() {
        println!("{}", foo::NUMBER);
    }
    

super 和 self

superself 关键字用于访问父模块和当前模块的变量

fn function() {
    println!("function");
}
mod mod1 {
    pub fn function() {
        println!("mod1::function");
        super::function();
    }
    pub mod mod2 {
        pub fn function() {
            println!("mod1::mod2::function");
            super::super::function();
        }
        pub fn function2() {
            self::function();
        }
    }
}

泛型

PartialOrd 是一个部分顺序比较,对应的还有一个 Ord 是全序关系,区别就是 PartialOrd 有些值之间的大小关系是未定义的,而 Ord 是所有值之间都有大小关系

fn largest<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
    if a > b {
        a
    } else {
        b
    }
}

fn main() {
    let a = largest(1, 2);
    println!("The largest number is {}", a);
}

泛型约束

泛型约束是 Clone + PartialOrd,因为在返回的时候需要返回 xy 的副本

因为直接返回 xy 会导致所有权转移,所以我们就需要对泛型约束,需要那个类型实现 ClonePartialOrd 这两个 trait

而类型 i32 是实现了 ClonePartialOrd 这两个 trait 的,所以就可以使用了

use std::cmp::PartialOrd;

struct Point<T> {
    x: T,
    y: T,
}
impl<T: Clone + PartialOrd> Point<T> {
    fn largest(&self) -> T {
        if self.x > self.y {
            self.x.clone()
        } else {
            self.y.clone()
        }
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };
    println!("The largest number is {}", p.largest());
}

trait

trait 是一种定义共享行为的方法,类似于其他语言的接口

下面代码 impl<T: Display> Display for Point<T> 的意思是:

  • Display 是一个 trait
  • for Point<T> 是为 Point<T> 实现 Display

方法 fmt 签名是 fn fmt(&self, f: &mut Formatter<'_>) -> Result,这个方法是 Display 的一个方法,用于格式化输出,它的第二个参数 f 的意思是,作为一个"输出目标"或"写入器",输出的内容需要写到 f

use std::fmt::{Display, Formatter, Result};
struct Point<T> {
    x: T,
    y: T,
}

impl<T: Display> Display for Point<T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y)
    }
}
fn main() {
    let p = Point { x: 5, y: 10 };
    println!("{}", p);
}

trait 实现

show 函数需要接收一个实现了 Display trait 的类型,所以它可以接收 Point 类型

use std::fmt::{Display, Formatter, Result};
struct Point<T> {
    x: T,
    y: T,
}

impl<T: Display> Display for Point<T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y)
    }
}

fn show<T: Display>(p: T) {
    println!("show {}", p);
}

fn main() {
    let p = Point { x: 5, y: 10 };
    show(p);
}

fn show<T: Display>(p: T) {} 函数可以换成下面这种语法糖

fn show(p: impl Display) {
    println!("show {}", p);
}

自动派生

自动派生是 rust 编译器自动为我们的结构体实现的一些 traits,比如 DebugPartialEqDefault

比如下面代码中打印 p 的时候,rust 会报错:Point doesn't implement Debug

这是因为 Point 结构体没有实现 Debug 这个 trait,所以我们需要手动实现

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 5, y: 10 };
    println!("{}", p);
}

Debug 这个 trait 使用的占位符是 {:?},下面代码就能正常打印 p

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 5, y: 10 };
    println!("{:?}", p);
}

还有 PartialEq 这个 trait,用于比较两个值是否相等,rust 也可以自动为我们实现

Point 上加上 PartialEq 这个 trait,两个实例就可以直接比较大小了,这种本来你需要手动实现 p1.x == p2.x && p1.y == p2.y 的操作

#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 5, y: 10 };
    let p2 = Point { x: 5, y: 10 };
    println!("{:?}", p1 == p2);
}

还有 Default 这个 trait,用于给结构体一个默认值,rust 也自动为我们实现了

Point::default() 会为我们自动添加上对应类型的默认值

#[derive(Debug, Default)]
struct Point {
    x: i32,
    y: i32,
    z: f64,
}

fn main() {
    let p = Point::default();
    println!("{:?}", p);      // Point { x: 0, y: 0, z: 0.0 }
}

所有权

所有权的规则:

  1. rust 中每个值都绑定一个变量,称为该值的所有者
  2. 每个值只有一个所有者,而且每个值都有它的作用域
  3. 一旦当这个值离开作用域,这个值占用的内容将被回收

下面代码,a 的所有权会被转移给 b,所以 a 不能再使用了

fn main() {
    let a = String::from("Hello");
    let b = a;
    println!("a: {}, b: {}", a, b);
}

下面代码,a 的所有权不会被转移给 b,所以 a 还可以继续使用

fn main() {
    let a = "Hello";
    let b = a;
    println!("a: {}, b: {}", a, b);
}

因为 String 是在堆上分配的,而字符串字面量是存储在程序的只读数据段中的,ab 实际上是对这个固定数据的引用,这种引用实现了 Copy trait,意味着赋值时会进行复制而不是移动

借用

借用是指在不转移所有权的情况下,允许对值进行访问

fn main() {
    let a = String::from("Hello");
    let b = &a;
    println!("a: {}, b: {}", a, b);
}

引用分为可变引用和不可变引用,不可变引用可以有多个,但是可变引用只能有一个

fn main() {
    let mut a = String::from("Hello");
    let b = &a;
    let c = &a;
    let d = &mut a;
    println!("a: {}, b: {}, c: {}", a, b, c);
}

不可变引用

let a = String::from("Hello");
let b = &a;
let c = &a;
let d = &a;

可变引用

let mut a = String::from("Hello");
let b = &mut a;
let c = &mut a; // error

下面两段代码中第二段代码会报错,因为 b 的不可变引用和持续到了 println! 中,所以出现了报错

fn main() {
    let mut a = String::from("Hello");
    let b = &a;
    let c = &mut a;
}

fn main() {
    let mut a = String::from("Hello");
    let b = &a;
    let c = &mut a; // error
    println!("a: {}, b: {}, c: {}", a, b, c);
}

生命周期注解

在大多数情况下 rust 编译器可以自动推到变量的生命周期,但是有时候编译器无法推到,就需要我们手动指定

比较常见的场景是与 &str 交互时

比如下面的代码,在 rust 中是编译不通过的,因为字符串 a 有自己的生命周期,而 b 也有自己的生命周期,rust 编译器无法推到返回的字符串的生命周期,所以编译就报错了

fn bigger(a: &str, b: &str) -> &str {
    if a > b {
        a
    } else {
        b
    }
}
fn main() {
    println!("bigger: {}", bigger("a", "b"));
}

为了让编译通过,就需要手动指定生命周期,'a 是一个生命周期注解,表示返回的字符串的生命周期与参数 a 的生命周期一样

fn bigger<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a > b {
        a
    } else {
        b
    }
}
fn main() {
    println!("bigger: {}", bigger("a", "b"));
}

要知道的是生命周期注解不会改变变量本身的生命周期,只是告诉编译器如何推到返回值的生命周期

这里为什么要加生命周期注解呢?因为 name 是一个引用,同时 Person 的实例化也是一个引用,所以 name 的生命周期必须与 Person 的生命周期一样

#[derive(Debug)]
struct Person<'a> {
    name: &'a str,
}
fn main() {
    let p = Person { name: "Peter" };
    println!("Name: {:?}", p);
}

错误

rust 中错误是可恢复的,异常是不可恢复的

不可恢复的错误有:

  • panic!("error")
  • assert!(1 == 2)
  • assert_eq!(1, 2)
  • unimplemented!() 未实现的函数
  • unreachable!() 不可能到达的代码

可恢复的错误:是一个泛型枚举 Result,有两个值 OkErr

fn main() {
    let a: Result<u32, &'static str> = Result::Ok(1);
    println!("{:?}", a);  // Ok(1)
    let b: Result<u32, &'static str> = Result::Err("result error");
    println!("{:?}", b);  // Err("result error")
}

例如在读取文件时,如果文件不存在,就会进入 Err 分支,如果文件存在,就会进入 Ok 分支

fn main() {
    let r = std::fs::read("src/main.rs");
    match r {
        Ok(data) => println!("{:?}", std::str::from_utf8(&data).unwrap()),
        Err(e) => println!("Error: {:?}", e),
    }
}

错误传递

有时候在捕获到错误时,是希望继续向上传递的,而不是直接崩溃,在 rust 中使用问号表达式是可以做到这一点的

但是问号表达式有一个要求:需要调用函数的错误类型与当前的错误类型相同时,才可以使用

fn bar() -> Result<u32, &'static str> {
    // Ok(42)
    Err("error msg")
}

fn foo() -> Result<i32, &'static str> {
    let a = bar()?;
    Ok(a as i32)
}
fn main() {
    match foo() {
        Ok(v) => println!("value: {}", v),
        Err(e) => println!("error: {}", e),
    }
}

将其他的错误包装成自己定义的错误

#[derive(Debug)]
pub enum Error {
    IO(std::io::ErrorKind),
}

impl From<std::io::Error> for Error {
    fn from(error: std::io::Error) -> Self {
        Error::IO(error.kind())
    }
}

fn do_read_file() -> Result<(), Error> {
    let _file = std::fs::read("src/main2.rs")?;
    let data_str = std::str::from_utf8(&_file).unwrap();
    println!("{}", data_str);
    Ok(())
}
fn main() {
    match do_read_file() {
        Ok(_) => println!("File read successfully"),
        Err(e) => println!("Error reading file: {:?}", e),
    }
}

最后

main 函数可以有返回值

fn main() -> Result<(), Error> {
    do_read_file()?;
    Ok(())
}

问号表达式可以一路写下去,直到最后,如果其中一个报错,会终止这个函数

fn main() -> Result<(), Error> {
    do_read_file()?;
    do_read_file()?;
    do_read_file()?;
    Ok(())
}