Rust权威指南 读书笔记

2023-05-16

bookcover|200

《Rust权威指南》
本书由 Rust 核心开发团队编写而成,由浅入深地探讨了 Rust 语言的方方面面。从学习函数、选择数据结构及绑定变量入手,逐步介绍所有权、trait、生命周期、安全保证等高级概念,模式匹配、错误处理、包管理、函数式特性、并发机制等实用工具,以及两个完整的项目开发实战案例。 作为开源的系统级编程语言,Rust 可以帮助你编写出更为快速且更为可靠的软件,在给予开发者底层控制能力的同时,通过深思熟虑的工程设计避免了传统语言带来的诸多麻烦。 本书被视为 Rust 开发工作的必读书目,适合所有希望评估、入门、提高和研究Rust语言的软件开发人员阅读。

[!tip]- 作者简介

Steve Klabnik,Rust文档团队负责人,Rust核心开发者之一,Rust布道者及高产的开源贡献者,此前致力于Ruby等项目的开发。
Carol Nichols,Rust核心团队成员,i32、LLC联合构建者,Rust Belt Rust会议组织者。
毛靖凯,游戏设计师,一直专注于游戏领域研发,曾负责设计和维护了多个商业游戏的基础框架。业余时间活跃于Rust开源社区,并尝试使用Rust来解决游戏领域中的诸多问题。
唐刚,资深开发者,Rustcc社区创始人和维护者之一。目前就职于Cdot Network。使用Rust从事区块链共识协议的开发工作。
沙渺,嵌入式开发者,国内Rust语言社区和Raspberry Pi(树莓派)开发社区早期参与者。负责维护多个RISC-V架构硬件平台的基础函数库。


🗒️我的笔记

Rust简介

主要是看视频 https://www.bilibili.com/video/BV1hp4y1k7SV?p=2&spm_id_from=pageDriver

Rust 特别擅长的领域:

  • 高性能 Web Service
  • WebAssembly
  • 命令行工具
  • 网络编程
  • 嵌入式设备
  • 系统编程

Rust优点:

  • 性能
  • 安全性
  • 无所畏惧的并发

install

https://www.rust-lang.org/
安装 in mac
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

更新:
rustup update

卸载Rust
rustup self uninstall

验证安装:
rustc --version

查看本地文档:
rustup doc

hello world

rustc main.rs 编译
Rust的缩进是4个空格而不是tab
println! 是个宏。函数是没有!的。
rustc 只适合简单的Rust程序。

hello Cargo

复杂的程序得用Cargo。
Cargo是Rust的构建系统和包管理工具。

  • 构建代码、下载依赖的库、构建这些库…

创建项目 cargo new xxx

cargo build 编译构建项目。会生成target目录,可执行文件就在里面。
cargo run 会编译 并运行项目。
cargo check 检查项目,比build快很多。

为发布构建,得加参数--release
cargo build --release 编译时会进行优化,编译的更慢,生成的执行文件运行的更快。

猜数游戏

use std::io;   // 引用标准库
  
fn main() {  
    println!("猜数!");  
    println!("测试一个数");   
    let mut guess = String::new();   // let 默认声明常量。加上mut才是变量。 new类似于静态方法调用。
    io::stdin().read_line(&mut guess).expect("无法读取行");  // &表示按引用传递。 expect表示错误处理。 Can 
    println!("你猜测的数是:{}", guess);  //{}是占位符。类似于%s
}

rust中库不叫lib, 而是叫crate.

为了生成随机数,得引用一个叫rand的crate。
直接在Cargo.toml里添加即可。

[dependencies]  
rand = "0.3.14"

版本依赖会存到Cargo.lock 中,更新版本时也会更新Cargo.lock

trait 类似于其他语言的接口。

match guess.cmp(&secret_number) {  
    Ordering::Less=>println!("Too small!"),  
    Ordering::Greater => println!("Too big!"),  
    Ordering::Equal => println!("You win"),  
} // match 类似于其他语言里的switch.

match 类似switch

loop 引入无限循环。break 退出循环。

如果要处理异常,而不是直接panic, 就需要去掉expect, 改成match自己匹配了。

完整的代码:

use std::cmp::Ordering;  
use std::io;  
use rand::Rng;  
  
fn main() {  
    println!("猜数!");  
    let secret_number = rand::thread_rng().gen_range(1,101); // i32 u32 i64  
    // println!("神秘数字是:{}", secret_number);  
    loop {  
        println!("测试一个数");  
        let mut guess = String::new();  
        io::stdin().read_line(&mut guess).expect("无法读取行");  
        println!("你猜测的数是:{}", guess);  
        // shadow  
        let guess:u32 = match guess.trim().parse() {  
            Ok(num) => num,  
            Err(_) => continue,  
        };  
        match guess.cmp(&secret_number) {  
            Ordering::Less=>println!("Too small!"),  
            Ordering::Greater => println!("Too big!"),  
            Ordering::Equal => {println!("You win");break;},  
        }  
    }  
  
}

通用的编程概念

变量与可变性。

默认let 声明的是 不可变的。可以指定变量类型,也可自动推导。
要指明可变需要在变量前加mut 修饰符。

  • 常量与不可变的变量不一样
    • 得用const关键词声明,不能用mut修饰符。
    • 需要标注类型
    • 常量可以在任何作用域内进行声明,包括全局作用域
    • 常量只能绑定到常量表达式,无法绑定到函数的调用结果或运行才能计算的。(必须编译期间确定值)
  • 在程序运行期间,常量在其作用域内一直有效
  • 命名规范,全大写,用_分隔。 如 MAX_POINT

Shadowing(隐藏)
可以声明同名变量 覆盖他。类型可以不一样。一般是类型转换时这么做。

数据类型

标量类型:一个标量类型代表一个单个的值

  • 整数类型 i8,i16,i32,i64,i128,u8,u16,u32,u64,u128. arch和架构相关。开发模式会检查溢出。发布模式不会检查。
  • 浮点类型 f32,f64(默认类型)
  • 布尔类型(bool) true/false
  • 字符类型(char) 4字节大小。

复合类型: 将多个值放在一个类型里。
Rust提供两种: 元组(长度固定,元素可以是多种类型)、数组(长度固定,元素类型统一)。

Tuple示例

fn main() {  
    let tup: (i32, f64, u8) = (500, 6.4, 1);  
    let (x,y,z) = tup;  
    println!("{},{},{}", x,y,z);   // 通过结构访问
    println!("{},{},{}", tup.0,tup.1,tup.2);   // 按索引访问
}

数组示例(实际用Vector更多)

数组是分配在栈上的连续内存。
索引越界时 编译会通过,运行时会panic。

申明方式有3种:

  1. 字面量 let a = [1, 2, 3, 4, 5];
  2. 指明类型和长度 let a: [i32; 5] = [1, 2, 3, 4, 5];
  3. 重复的内容 let a = [3; 5]; // [3,3,3,3,3]

函数

关键词是fn , 命名规范为snake case。 入口为main.

参数:

  • parameters, 形参,定义时的参数
  • arguments, 实参。实际传进来的值。

函数签名必须声明类型。

函数体中的语句与表达式

  • 函数体由一系列语句组成,可选的由一个表达式结束
  • Rust是一个基于表达式的语言
  • 语句是执行一些动作的指令
  • 表达式会计算产生一个值
  • 函数的定义也是语句
  • 语句不返回值,所以不可以使用let将一个语句赋值给变量。

函数的返回值:

  • 需要声明(-> 后面),不能命名
  • 返回值是最有一个表达式的值
  • 如果想提前返回,使用return , 并指定一个值

注意,表达式不要加’;'号,否则变成了语句。

分支语句

if
每种情况都叫arm, 必须是bool类型。 按顺序执行。
不用三元运算符。 let number = if condistion { 5 } else {6 }; 这种写法就可以了。

match 类似于其他语言的switch, 处理多分支的。

loop
反复运行。无限循环。 loop 快里最后一个表达式可以返回值。

while条件循环。

for循环遍历集合。(由于其安全、简介。是用的最多的)

fn main() {  
    let a = [10, 20, 30, 40, 50];  
  
    for element in a {  
        println!("the value is: {}", element);  
    }  
}

Range 协助循环。

fn main() {  
    for number in (1..4).rev() {  // (1..4) 就是 Range 生成[1,2,3] rev()为反转
        println!("{}!", number);  
    }  
    println!("LIFTOFF!!!");  
}

所有权

什么是所有权

Rust的核心机制。

其他语言要么是手动内存管理,要么是gc 自动管理。

Rust采用了第三种方式

  • 内存是通过一个所有权系统来管理的,其中包含一组编译器在编译时检查的规则。
  • 当程序运行时,所有权特性不会减慢程序的运行速度。

栈内存 vs 堆内存

所有存储在Stack上的数据必须拥有已知的固定的大小。
- 编译时大小未知的数据或运行时大小 可能发生变化的数据 必须存放在heap上。

使用栈内存 性能更高。因为不需要分配内存( 查找可用空间 需要时间)。访问也更快。

所有权解决的问题:

  • 跟踪代码的哪些部分正在使用heap的哪些数据。
  • 最小化heap上的重复数据量
  • 清理heap上未使用的数据以避免空间不足。

所有权规则

  • 每个值都有一个变量,这个变量是该值的所有者
  • 每个值同时只能有一个所有者
  • 当所有者超出作用域时,该值将被删除。

为什么String类型的值可以修改,而字符串字面值却不能修改。
因为他们处理内存的方式不同。

字符串字面值,在编译时就知道它的内容了,其文本内容直接被硬编码到最终的可执行文件里

  • 速度快、高效。是因为其不可变性。

String类型,为了支持可变性,需要在heap上分配内存来保存编译时未知的文本内容:

  • 操作系统必须在运行时来申请内存。
  • 当用完String之后,需要使用某种方式将内存返回给操作系统。(在Rust中,对于某个值来说,当拥有它的变量走出作用域范围时,内存会立即自动的交还给操作系统)

drop函数。离开作用域范围时自动调用。

变量与数据交互的方式:

  1. 移动(move)
    • 多个变量可以与同一个数据使用一种独特的方式来交互。let x=5;let y=x;
let s1 = String::from("hello"); 
let s2 = s1;// 这里 就是移动。。所有权从s1移动到了s2
println!("{}, world!", s1); // 编译通不过。s1被废弃掉了。否则会出现两次释放内存的问题。

![[Pasted image 20220530235851.png]]
2. 克隆(clone)

let s1 = String::from("hello");
let s2 = s1.clone(); // 如下图所示。heap上的内存也复制了一分。
println!("s1 = {}, s2 = {}", s1, s2);

![[Pasted image 20220531000207.png]]
Stack上的数据:复制
由于Stack上复制数据没啥性能消耗。所以不存在heap上的废弃问题。
Copy trait, 可以用于像整数这样完全存放在stack上面的类型(就是大小很明显的值)
如果一个类型实现了Copy这个trait, 那么旧的变量在赋值后仍然可用。
如果一个类型或者该类型的一部分实现了Drop trait, 那么Rust不允许它再去实现Copy trait了

一些拥有Copy trait的类型

  • 任何简单标量的组合类型都可以是Copy的
  • 任何需要分配内存或某种资源的都不是Copy的
  • 一些拥有Copy trait的类型
    • 所有的整数类型,如u32
    • bool
    • float32, float64
    • char
    • Tuple, 如果其所有内容都是Copy的

所有权和函数

  • 在语义上,将值传递给函数和把值赋值给变量是类似的:
    • 将值传递给函数会发生移动或复制
  • 返回值与作用域
    • 函数在返回值的过程中同样会发生所有权的转移
  • 一个变量的所有权总是遵循同样的模式:
    • 把一个值赋给其他变量时就会发生移动
    • 当一个包含heap数据的 变量离开作用域时,它的值就会被drop函数清理,除非数据的所有权移动到另一个变量上了

引用与借用

&表示引用: 允许你引用某些值 而 不取得其所有权
![[Pasted image 20220602223830.png]]
如图。s就是s1的引用。

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

    let len = calculate_length(&s1); // s1的所有权 不会转移

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

我们把引用作为函数参数这个行为叫做借用。
不能修改借用的东西
和变量一样,引用默认是不可变的。

加上mut可以把引用改成可变引用,这样就可以修改了。

不过在特定的作用域内,可变引用只能有一个。

  • 这样做的好处 是可在编译时防止数据竞争。

以下三种行为会发生数据竞争:
- 两个或多个指针同事访问同一个数据。
- 至少有一个指针用于写入数据。
- 没有使用任何机制来同步对数据的访问。

可以通过创建新的作用域,来允许非同时的创建多个可变引用。

另一个限制:

不可以同时拥有一个可变引用和一个不变的引用

[!quote]- 悬空引用

悬空指针:一个指针引用了内存中的某个地址,而这块内存可能已经释放并分配给其他人使用了。

在Rust里,编译器可保证引用永远都不是悬空引用;

[!quote]- 引用的规则

在任何给定的时刻,只能满足下列条件之一;

一个可变的引用
任意数量的不可变的引用。

引用必须一致有效

切片

Rust 的另外一种不持有所有权的数据类型: 切片

[!quote]- 字符串切片
字符串切片就是指向字符串中一部分内容的引用。
示例: &a[1..5]

注意:

  • 字符串切片的范围索引必须发生在有效的UTF-8字符边界内。
  • 如果尝试从一个多字节的字符串中创建字符串切片,程序会报错并退出。
fn main() {  
    let mut s = String::from("hello world");  
  
    let word = first_word(&s);  //这里是不可变引用
  
    s.clear(); // error!  所以这里不能改数据了
  
    println!("the first word is: {}", word);  
}  
  
  
fn first_word(s: &str) -> &str {  // &str表示字符串切片。
    let bytes = s.as_bytes();  
  
    for (i, &item) in bytes.iter().enumerate() {  
        if item == b' ' {  
            return &s[0..i];  
        }  
    }  
  
    &s[..]  
}

使用structs结构化相关数据

定义和实例化结构体

例子

// 定义
  
struct User {  
    active: bool,  
    username: String,  
    email: String,  
    sign_in_count: u64,  
}  
  
fn main() {  
	// 实例化
    let mut user1 = User {  
        email: String::from("someone@example.com"),  
        username: String::from("someusername123"),  
        active: true,  
        sign_in_count: 1,  
    };  
    // 引用字段
    user1.email = String::from("anotheremail@example.com");  
}

注意: 一旦struct的实例是可变的,那么实例中所有的字段都是可变的

[!quote]- Tuple struct
可定义类似tupe的struct, 叫做tuple struct
整体有名字,但是里面的元素没有名字。

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

[!quote]- Unit-Like Struct(没任何字段)
适用于需要在某个类型上实现某个trait, 但是在里面又没有想要存储的数据。

struct数据的所有权

只要struct实例是有效的,那么里面的字段数据也是有效的。
struct里也可以存放引用,需要生命周期
生命周期保证只要struct实例是有效的,那么里面的引用也是有效的。

格式化:

std::fmt::Display {}
std::fmt::Debug
     {:?}  单行打印
	 {:#?}  美化打印
#[derive(Debug)] 

struct方法

#[derive(Debug)]  
struct Rectangle {  
    width: u32,  
    height: u32,  
}  
  
impl Rectangle {  
    fn area(&self) -> u32 {  
        self.width * self.height  
    }  
}  
  
fn main() {  
    let rect1 = Rectangle {  
        width: 30,  
        height: 50,  
    };  
  
    println!(  
        "The area of the rectangle is {} square pixels.",  
        rect1.area()  
    );  
}

方法与函数的不同之处:

  • 方法是在struct(或enum、trait对象)的上下文中定义
  • 第一个参数是self, 表示方法被调用的struct实例

[!quote]- 关联函数
可以再impl块里定义不把self作为第一个参数的函数,他们叫关联函数(不是方法)

示例:

#[derive(Debug)]  
struct Rectangle {  
    width: u32,  
    height: u32,  
}  
  
impl Rectangle {  
    fn square(size: u32) -> Rectangle {  
        Rectangle {  
            width: size,  
            height: size,  
        }  
    }  
}  
  
fn main() {  
    let sq = Rectangle::square(3);  // 调用
}

枚举与模式匹配

定义枚举

// 定义
enum IpAddrKind {  
    V4,  
    V6,  
}  
  
fn main() {  
    let four = IpAddrKind::V4;  // 使用
    let six = IpAddrKind::V6;  
  
    route(IpAddrKind::V4);  
    route(IpAddrKind::V6);  
}  
  
fn route(ip_kind: IpAddrKind) {}

将数据附加到枚举的变体中

enum IpAddrKind {  
    V4(String),  
    V6(String),  
} 

优点:

	不需要额外使用struct
	每个变体可以拥有不同的类型以及关联的数据量。
enum IpAddrKind {  
    V4(u8,u8,u8,u8),  
    V6(String),  
} 

[!quote]- Option枚举
定义于标准库中
在Prelude(预导入模块)中
描述了: 某个值可能存在(某种类型) 或不存在的情况

在Rust中没有Null这个概念,而是用Option表达的。

enum Option<T> {  
    None,  // Nome和Some可以直接使用
    Some(T),  
}

[!tip]- Option<T>比Null好在哪?

Option<T>和T是不同的类型,不可以把Option<T>直接当做T
若想使用Option<T>中的T, 必须将她转化为T

match控制流

[!quote]- 强大的控制流运算符-match
允许一个只与一系列模式进行匹配,并执行匹配的模式对应的代码
模式可以是字面值、变量名、通配符…

enum Coin {  
    Penny,  
    Nickel,  
    Dime,  
    Quarter,  
}  
  
fn value_in_cents(coin: Coin) -> u8 {  
    match coin {  
        Coin::Penny => 1,  
        Coin::Nickel => 5,  
        Coin::Dime => 10,  
        Coin::Quarter => 25,  
    }  
}  
  
fn main() {}

[!quote]- 绑定值的模式
匹配的分支可以绑定到被匹配对象的部分值。
因此,可以从enum变体中提取值。
匹配Option(T)

#[derive(Debug)]  
enum UsState {  
    Alabama,  
    Alaska,  
    // --snip--  
}  
  
enum Coin {  
    Penny,  
    Nickel,  
    Dime,  
    Quarter(UsState),  
}  
  
fn value_in_cents(coin: Coin) -> u8 {  
    match coin {  
        Coin::Penny => 1,  
        Coin::Nickel => 5,  
        Coin::Dime => 10,  
        Coin::Quarter(state) => {  
            println!("State quarter from {:?}!", state);  
            25  
        }  
    }  
}  
  
fn main() {  
    value_in_cents(Coin::Quarter(UsState::Alaska));  
}

[!tip]- match匹配必须穷举所有可能
用_ 表示没列举的可能性。

if let 控制流

用于替代 match只关心一种匹配的情况。

fn main() {  
    let config_max = Some(3u8);  
    match config_max {  
        Some(max) => println!("The maximum is configured to be {}", max),  
        _ => (),  
    }  
}

改成if let

fn main() {  
    let config_max = Some(3u8);  
    if let Some(max) = config_max {  
        println!("The maximum is configured to be {}", max);  
    }  
    // 可以有ese 分支 处理其他情况
}

代码组织

代码组织主要包括:

  • 哪些细节可以暴露,哪些细节是私有的
  • 作用域内哪些名称有效

[!quote]- 模块系统

  • Package(包): Cargo的特性,让你构建、测试、共享crate
  • Crate(单元包): 一个模块树,它可以产生一个library或可执行文件
  • Module(模块)、use: 让你控制代码的组织、作用域、私有路径
  • Path(路径): 为struct、function或module等项命名的方式

Package 和 Crate

  • Crate的类型
    • binary
    • library
  • Crate Root:
    • 是源代码文件
    • Rust编译器从这开始,组成你的Crate的根Module
  • 一个Packge:
    • 包含一个Cargo.toml, 它描述了如何构建这些Crates
    • 只能包含0-1个library crate
    • 可以包含任意数量的binary crate
    • 必须至少包含一个crate(library或binary)

Cargo的惯例

  • src/main.ts:
    • binary crate的crate root
    • crate 名与package名相同
  • src/lib.ts:
    • package包含一个library crate
    • library crate的crate root
    • crate 名与package名相同
  • 一个Package可以同时包含src/main.rs 和 src/lib.rs
    • 一个binary crate, 一个libary crate
    • 名称与package名相同
  • 一个Package可以有多个binary crate:
    • 文件放在src/bin
    • 每个文件可以是单独的binary crate

[!quote]- Crate的作用
将相关的功能组合到一个作用域内,便于在项目间进行共享-防止冲突

定义module来控制作用域和私有性

  • Module
    • 在一个crate内,将代码进行分组
    • 增加代码可读性,易于复用
    • 控制项目(item)的私有性。public、private
  • 建立module:
    • mod 关键字
    • 可嵌套
    • 可包含其它项(struct、enum、常量、trait、函数等)的定义

路径 Path

  • 为了在Rust的模块中找到某个条目,需要使用路径
  • 路径的两种形式
    • 绝对路径: 从crate root开始,使用crate名或字面值crate
    • 相对路径:从当前模块开始,使用self,super或当前模块的标识符
  • 路径至少由一个标识符组成,标识符之间用::

私有边界

  • 模块不仅可以组织代码,还可以定义私有边界。
  • 如果想把函数或struct等设为私有,可以将它放到某个模块中。
  • Rust中所有条目默认都是私有的
  • 父级模块无法访问子模块中的私有条目
  • 子模块里可以使用所有祖先模块中的条目

[!quote]- pub
加上pub就变成公有了。

super引用上级

use关键词

可以使用use关键字将路径导入导作用域内
可以使用相对路径/绝对路径

use的习惯用法:

  • 函数:引入到父级
  • struct, enum, 其他: 指定完整路径
  • 同名条目: 指定到父级

可用 as 取别名。
use 引入的模块默认是私有的。外部模块无法访问。
加上pub 之后 外部模块也就可以访问了。

使用外部包

  1. 在Cargo.toml里添加依赖的package
    1. Cargo会从https://crates.io/ 下载包及其依赖项
  2. use引入

标准库也被当做外部包。由于内置了。不需要修改配置文件。但使用的时候还得用use

更多参考 https://download.csdn.net/download/goodparty/86824159?spm=1001.2014.3001.5503

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Rust权威指南 读书笔记 的相关文章

随机推荐