banner
魔法少女秋小日

Qiuxiaori

github
email

《Rust 语言圣经》学习笔记

在线阅读地址:Rust 语言圣经

阅读进度:60%

image


开发工具#

VSCode#

插件

RustRover#

插件

设置

  • 代码格式

image.png

image.png


Cargo#

使用 cargo 工具的最大优势就在于,能够对该项目的各种依赖项进行方便、统一和灵活的管理。

Cargo.toml 和 Cargo.lock#

  • Cargo.toml 是 cargo 特有的项目数据描述文件。它存储了项目的所有元配置信息,如果 Rust 开发者希望 Rust 项目能够按照期望的方式进行构建、测试和运行,那么,必须按照合理的方式构建 Cargo.toml
  • Cargo.lock 文件是 cargo 工具根据同一项目的 toml 文件生成的项目依赖详细清单,因此我们一般不用修改它,只需要对着 Cargo.toml 文件撸就行了。

在 Cargo.toml 中,主要通过各种依赖段落来描述该项目的各种依赖项:

[dependencies]
rand = "0.3"
hammer = { version = "0.5.0"} // 基于 Rust 官方仓库 crates.io,通过版本说明来描述
color = { git = "https://github.com/bjz/color-rs" } // 基于项目源代码的 git 仓库地址,通过 URL 来描述
geometry = { path = "crates/geometry" } // 基于本地项目的绝对路径或者相对路径,通过类 Unix 模式的路径来描述
  • Cargo.toml 文件内容

    Cargo.toml 文件是 Rust 项目中用来配置和管理项目依赖、编译选项、元数据等信息的配置文件。以下是 Cargo.toml 文件中常见的部分和它们的用途:

    1. [package]#

    这个部分定义了包的基本信息,包括名称、版本、作者、描述等。

    [package]
    name = "my_project"         # 项目名称
    version = "0.1.0"           # 项目版本
    edition = "2021"            # Rust 语言版本
    authors = ["Your Name <[email protected]>"] # 作者
    description = "A brief description of the project"  # 项目描述
    license = "MIT"             # 项目使用的许可证
    repository = "<https://github.com/yourusername/my_project>"  # 项目源码仓库
    
    

    2. [dependencies]#

    在这个部分中,定义了项目所依赖的库及其版本。

    [dependencies]
    serde = "1.0"               # 添加 serde 库作为依赖
    rand = { version = "0.8", features = ["std"] }  # 指定版本和附加特性
    
    

    3. [dev-dependencies]#

    这个部分用于定义开发环境中所需的依赖,这些依赖通常只在测试或开发工具中使用。

    [dev-dependencies]
    tokio = { version = "1", features = ["full"] }  # 仅用于开发阶段
    
    

    4. [build-dependencies]#

    这个部分用于定义构建脚本 (build.rs) 中的依赖。

    [build-dependencies]
    cc = "1.0"                  # 用于构建原生代码的库
    
    

    5. [features]#

    用来定义项目的可选特性(功能)。这些特性可以让用户选择性地启用或禁用某些功能。

    [features]
    default = ["serde"]         # 默认启用的特性
    extras = ["tokio"]          # 可选特性
    
    

    6. [package.metadata]#

    这个部分通常用于自定义工具的配置,它的内容和结构因工具而异。

    [package.metadata]
    docs = { rsdoc = true }     # 自定义文档生成器的配置
    
    

    7. [workspace]#

    如果项目是一个工作空间(包含多个子项目),则使用这个部分定义工作空间的结构。

    [workspace]
    members = ["project1", "project2"]  # 工作空间中的成员项目
    
    

    8. [patch][replace]#

    用于替换或修补依赖项的版本,通常在处理依赖冲突或开发中使用。

    [patch.crates-io]
    serde = { git = "<https://github.com/serde-rs/serde>", branch = "master" }
    
    [replace]
    foo = { path = "../foo" }   # 替换依赖库
    
    

    9. [profile]#

    这个部分允许你为不同的编译配置(如 dev, release)自定义编译选项。

    [profile.dev]
    opt-level = 1               # 在开发模式下使用的优化级别
    
    [profile.release]
    opt-level = 3               # 在发布模式下使用的优化级别
    
    

    10. [badges]#

    用于在文档生成工具或其他服务上显示状态徽章(如构建状态、代码覆盖率等)。

    [badges]
    travis-ci = { repository = "rust-lang/crates.io" }  # 显示 Travis CI 徽章
    
    

    这是 Cargo.toml 文件中常见的部分及其用途。根据项目的复杂性和需求,文件内容可以更加丰富或简单。

镜像加速#

下载依赖太慢了? - Rust 语言圣经 (Rust Course)

[http]
check-revoke = false
# proxy = "http://x.x.x.x:8888" // 于虚拟机内未联网开发时 作代理使用

[source.crates-io]
replace-with = 'rsproxy'
[source.rsproxy]
registry = "https://rsproxy.cn/crates.io-index"
[source.rsproxy-sparse]
registry = "sparse+https://rsproxy.cn/index/"
[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"

[net]
git-fetch-with-cli = true

泛型和特征#

泛型就是一种多态。泛型主要目的是为程序员提供编程的便利,减少代码的臃肿,同时极大地丰富语言本身的表达能力。

  • 枚举
enum Option<T> {
		Some(T),
		None,
}
  • 结构体
struct Point<T> {
    x: T,
    y: T,
}
  • 方法
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

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

泛型的性能#

在 Rust 中泛型是零成本的抽象,在使用泛型时,完全不用担心性能上的问题,但是會损失编译速度和增大了最终生成文件的大小。

Rust 通过在编译时进行泛型代码的 单态化(monomorphization) 来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。

编译器所做的工作正好与我们创建泛型函数的步骤相反,编译器寻找所有泛型代码被调用的位置并针对具体类型生成代码。

特征的多重约束#

pub fn notify(item: &(impl Summary + Display)) {} // 語法糖形式
pub fn notify<T: Summary + Display>(item: &T) {} // 特征約束的形式

Where 约束

当特征约束变得很多时,函数的签名将变得很复杂:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}

通过 where 对其做一些形式上的改进:

fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{}

动态数组#

https://doc.rust-lang.org/std/collections/

创建#


let mut v1 = Vec::with_capacity(10);
v1.extend([1, 2, 3]);    // 附加数据到 v
println!("Vector 长度是: {}, 容量是: {}", v.len(), v.capacity());

常用方法#

let mut v =  vec![1, 2];
assert!(!v.is_empty());         // 检查 v 是否为空

v.reserve(100);                 // 调整 v 的容量,至少要有 100 的容量

v.insert(2, 3);                 // 在指定索引插入数据,索引值不能大于 v 的长度, v: [1, 2, 3] 
assert_eq!(v.remove(1), 2);     // 移除指定位置的元素并返回, v: [1, 3]
assert_eq!(v.pop(), Some(3));   // 删除并返回 v 尾部的元素,v: [1]
assert_eq!(v.pop(), Some(1));   // v: []
assert_eq!(v.pop(), None);      // 记得 pop 方法返回的是 Option 枚举值
v.clear();                      // 清空 v, v: []

let mut v1 = [11, 22].to_vec(); // append 操作会导致 v1 清空数据,增加可变声明
v.append(&mut v1);              // 将 v1 中的所有元素附加到 v 中, v1: []
v.truncate(1);                  // 截断到指定长度,多余的元素被删除, v: [11]
v.retain(|x| *x > 10);          // 保留满足条件的元素,即删除不满足条件的元素

let mut v = vec![11, 22, 33, 44, 55];
// 删除指定范围的元素,同时获取被删除元素的迭代器, v: [11, 55], m: [22, 33, 44]
let mut m: Vec<_> = v.drain(1..=3).collect();    

let v2 = m.split_off(1);        // 指定索引处切分成两个 vec, m: [22], v2: [33, 44]

let v2 = vec![11, 22, 33, 44, 55]; // 通过切片截取数组
let slice = &v2[1..=3];
assert_eq!(slice, &[22, 33, 44]);

排序#

fn main() {
    let mut vec = vec![1.0, 5.6, 10.3, 2.0, 15f32];    
    vec.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
    assert_eq!(vec, vec![1.0, 2.0, 5.6, 10.3, 15f32]);
}

结构体的排序

// 自定义一个按照年龄倒序排序的对比函数
people.sort_unstable_by(|a, b| b.age.cmp(&a.age));

排序需要实现 Ord 特性,如果把结构体实现了该特性,就不需要自定义对比函数了,但是,实现 Ord 需要我们实现 OrdEqPartialEqPartialOrd 这些属性,可以 derive 这些属性:

#[derive(Debug, Ord, Eq, PartialEq, PartialOrd)] // derive 属性
struct Person {
    name: String,
    age: u32,
}

impl Person {
    fn new(name: String, age: u32) -> Person {
        Person { name, age }
    }
}

fn main() {
    let mut people = vec![
        Person::new("Zoe".to_string(), 25),
        Person::new("Al".to_string(), 60),
        Person::new("Al".to_string(), 30),
        Person::new("John".to_string(), 1),
        Person::new("John".to_string(), 25),
    ];

    people.sort_unstable();

    println!("{:?}", people);
}
// => [Person { name: "Al", age: 30 }, Person { name: "Al", age: 60 }, Person { name: "John", age: 1 }, Person { name: "John", age: 25 }, Person { name: "Zoe", age: 25 }]

derive 的默认实现会依据属性的顺序依次进行比较,如上述例子中,当 Person 的 name 值相同,则会使用 age 进行比较。


HashMap#

创建#

fn main() {
	use std::collections::HashMap;
	
	let teams_list = vec![
	    ("中国队".to_string(), 100),
	    ("美国队".to_string(), 10),
	    ("日本队".to_string(), 50),
	];
	
	let teams_map: HashMap<_,_> = teams_list.into_iter().collect();
	println!("{:?}",teams_map)
}

生命周期🌟#

悬垂指针#

生命周期的主要作用是避免悬垂引用,它会导致程序引用了本不该引用的数据

{
    let r;
    {
        let x = 5;
        r = &x;
    }
    println!("r: {}", r);
}

语法#

生命周期的语法以 ' 开头,名称往往是一个单独的小写字母,一般用 'a 来作为生命周期的名称。 如果是引用类型的参数,那么生命周期会位于引用符号 & 之后,并用一个空格来将生命周期引用参数分隔开:

&i32        // 一个引用
&'a i32     // 具有显式生命周期的引用
&'a mut i32 // 具有显式生命周期的可变引用

函数签名

使用生命周期参数,需要先声明 <'a>

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

静态生命周期

在 Rust 中有一个非常特殊的生命周期,那就是 'static,拥有该生命周期的引用可以和整个程序活得一样久。

在之前我们学过字符串字面量,提到过它是被硬编码进 Rust 的二进制文件中,因此这些字符串变量全部具有 'static 的生命周期:

let s: &'static str = "我没啥优点,就是活得久,嘿嘿";

生命周期消除#

在开始之前有几点需要注意:

  • 消除规则不是万能的,若编译器不能确定某件事是正确时,会直接判为不正确,那么你还是需要手动标注生命周期

  • 函数或者方法中,参数的生命周期被称为 输入生命周期,返回值的生命周期被称为 输出生命周期

  • 三条消除规则

    编译器使用三条消除规则来确定哪些场景不需要显式地去标注生命周期。其中第一条规则应用在输入生命周期上,第二、三条应用在输出生命周期上。若编译器发现三条规则都不适用时,就会报错,提示你需要手动标注生命周期。

    1. 每一个引用参数都会获得独自的生命周期

      例如一个引用参数的函数就有一个生命周期标注: fn foo<'a>(x: &'a i32),两个引用参数的有两个生命周期标注:fn foo<'a, 'b>(x: &'a i32, y: &'b i32), 依此类推。

    2. 若只有一个输入生命周期 (函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期

      例如函数 fn foo(x: &i32) -> &i32x 参数的生命周期会被自动赋给返回值 &i32,因此该函数等同于 fn foo<'a>(x: &'a i32) -> &'a i32

    3. 若存在多个输入生命周期,且其中一个是 &self 或 &mut self,则 &self 的生命周期被赋给所有的输出生命周期

      拥有 &self 形式的参数,说明该函数是一个 方法,该规则让方法的使用便利度大幅提升。

一个复杂例子:泛型、特征约束#

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>( // 声明生命周期和范型
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {}", ann); // 因为要用格式化 {} 来输出 ann,因此需要它实现 Display 特征。
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

用 Fn 特征解决闭包生命周期#

fn main() {
   let closure_slision = fun(|x: &i32| -> &i32 { x });
   assert_eq!(*closure_slision(&45), 45);
   // Passed !
}

fn fun<T, F: Fn(&T) -> &T>(f: F) -> F {
   f
}

NLL (Non-Lexical Lifetime)#

之前我们在引用与借用那一章其实有讲到过这个概念,简单来说就是:引用的生命周期正常来说应该从借用开始一直持续到作用域结束,但是这种规则会让多引用共存的情况变得更复杂:

fn main() {
   let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    // 新编译器中,r1,r2作用域在这里结束

    let r3 = &mut s;
    println!("{}", r3);
}

按照上述规则,这段代码将会报错,因为 r1 和 r2 的不可变引用将持续到 main 函数结束,而在此范围内,我们又借用了 r3 的可变引用,这违反了借用的规则:要么多个不可变借用,要么一个可变借用

好在,该规则从 1.31 版本引入 NLL 后,就变成了:引用的生命周期从借用处开始,一直持续到最后一次使用的地方

按照最新的规则,我们再来分析一下上面的代码。r1 和 r2 不可变借用在 println! 后就不再使用,因此生命周期也随之结束,那么 r3 的可变借用就不再违反借用的规则。

Reborrow 再借用#

https://course.rs/advance/lifetime/advance.html#reborrow - 再借用

  • todo

错误处理#

Panic#

https://course.rs/basic/result-error/panic.html#panic - 原理剖析

backtrace 栈展开

如果我们想知道程序抛出 panic 之前经过了哪些调用环节,按照提示使用 RUST_BACKTRACE=1 cargo run 或 $env:RUST_BACKTRACE=1 ; cargo run 来运行程序。

panic 时的两种终止方式

当出现 panic! 时,程序提供了两种方式来处理终止流程:栈展开直接终止

其中,默认的方式就是 栈展开,这意味着 Rust 会回溯栈上数据和函数调用,因此也意味着更多的善后工作,好处是可以给出充分的报错信息和栈调用信息,便于事后的问题复盘。直接终止,顾名思义,不清理数据就直接退出程序,善后工作交与操作系统来负责。

对于绝大多数用户,使用默认选择是最好的,但是当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,例如下面的配置修改 Cargo.toml 文件,实现在 release 模式下遇到 panic 直接终止:

[profile.release]
panic = 'abort'

包和模块#

引入项再导出#

当外部的模块项 A 被引入到当前模块中时 **,它的可见性自动被设置为私有的,** 如果你希望允许其它外部代码引用我们的模块项 A,那么可以对它进行再导出:


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 代表引入 hosting 模块到当前作用域,pub 表示将该引入的内容再度设置为可见。

当你希望将内部的实现细节隐藏起来或者按照某个目的组织代码时,可以使用 pub use 再导出,例如统一使用一个模块来提供对外的 API,那该模块就可以引入其它模块中的 API,然后进行再导出,最终对于用户来说,所有的 API 都是由一个模块统一提供的。


注释和文档#

代码注释#

// 行注释

/**
	块注释
*/

文档注释#

/// `add_one` 将指定值加1
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}

/** `add_two` 将指定值加2

let arg = 5;
let answer = my_crate::add_two(arg);

assert_eq!(7, answer);

*/
pub fn add_one(x: i32) -> i32 {
    x + 1
}

常用文档标题

在文档注释中除了 # Examples  这个标题,还有一些常用的,可以在项目中酌情使用:

  • Panics:函数可能会出现的异常状况,这样调用函数的人就可以提前规避
  • Errors:描述可能出现的错误及什么情况会导致错误,有助于调用者针对不同的错误采取不同的处理方式
  • Safety:如果函数使用 unsafe 代码,那么调用者就需要注意一些使用条件,以确保 unsafe 代码块的正常工作

包和模块级别的注释#

也分为行注释 //! 和块注释 /*! ... */

查看文档#

cargo doc 

cargo doc --open

文档测试#

https://course.rs/basic/comment.html# 文档测试 doc-test

保留测试,隐藏文档

在某些时候,我们希望保留文档测试的功能,但是又要将某些测试用例的内容从文档中隐藏起来:


/// ```
/// # // 使用#开头的行会在文档中被隐藏起来,但是依然会在文档测试中运行
/// # fn try_main() -> Result<(), String> {
/// let res = world_hello::compute::try_div(10, 0)?;
/// # Ok(()) // returning from try_main
/// # }
/// # fn main() {
/// #    try_main().unwrap();
/// #
/// # }
/// ```
pub fn try_div(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("Divide-by-zero"))
    } else {
        Ok(a / b)
    }
}

以上文档注释中,我们使用 # 将不想让用户看到的内容隐藏起来,但是又不影响测试用例的运行,最终用户将只能看到那行没有隐藏的 let res = world_hello::compute::try_div(10, 0)?;


格式化输出#

https://course.rs/basic/formatted-output.html

{} 与#

与其它语言常用的 %d%s 不同,Rust 特立独行地选择了 {} 作为格式化占位符,事实证明,这种选择非常正确,它帮助用户减少了很多使用成本,你无需再为特定的类型选择特定的占位符,统一用 {} 来替代即可,剩下的类型推导等细节只要交给 Rust 去做。

与 {} 类似,{:?} 也是占位符:

  • {} 适用于实现了 std::fmt::Display 特征的类型,用来以更优雅、更友好的方式格式化文本,例如展示给用户
  • {:?} 适用于实现了 std::fmt::Debug 特征的类型,用于调试场景

两者的选择很简单,当你在写代码需要调试时,使用 {:?},剩下的场景,选择 {}

**Display 特征 **

与大部分类型实现了 Debug 不同,实现了 Display 特征的 Rust 类型并没有那么多,往往需要我们自定义想要的格式化方式:


let i = 3.1415926;
let s = String::from("hello");
let v = vec![1, 2, 3];
let p = Person {
    name: "sunface".to_string(),
    age: 18,
};
println!("{}, {}, {}, {}", i, s, v, p);

运行后可以看到 v 和 p 都无法通过编译,因为没有实现 Display 特征,但是你又不能像派生 Debug 一般派生 Display,只能另寻他法:

  • 使用 {:?} 或 {:#?}
  • 为自定义类型实现 Display 特征
  • 使用 newtype 为外部类型实现 Display 特征

下面来一一看看这三种方式。

{:#?} 与 {:?} 几乎一样,唯一的区别在于它能更优美地输出内容:


// {:?}
[1, 2, 3], Person { name: "sunface", age: 18 }

// {:#?}
[
    1,
    2,
    3,
], Person {
    name: "sunface",
}

因此对于 Display 不支持的类型,可以考虑使用 {:#?} 进行格式化,虽然理论上它更适合进行调试输出。

** 为自定义类型实现 Display 特征 **

如果你的类型是定义在当前作用域中的,那么可以为其实现 Display 特征,即可用于格式化输出:


struct Person {
    name: String,
    age: u8,
}

use std::fmt;
impl fmt::Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "大佬在上,请受我一拜,小弟姓名{},年芳{},家里无田又无车,生活苦哈哈",
            self.name, self.age
        )
    }
}
fn main() {
    let p = Person {
        name: "sunface".to_string(),
        age: 18,
    };
    println!("{}", p);
}

如上所示,只要实现 Display 特征中的 fmt 方法,即可为自定义结构体 Person 添加自定义输出

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。