[toc]

Rust

Rustacean

rust toolchain包含了 rustc, cargo, rustfmt, clippy,rust-src, rust-docs, rust-std,通过rustup管理。rust分为stable、beta、nightly版本。

常用工具

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh // 安装rust
rustc --version // 显示当前版本
rustup update // 升级rust版本,注意rustup版本和rustc&cargo版本会有差异
rustup self uninstall // 卸载rust和rustup
rustup show // 显示toolchain
rustup component list --installed // 查看安装了toolchain中的哪些工具
rustup component add clippy // 安装某个toolchain的工具

// 使用工具
cargo fmt
cargo clippy

// 使用特定版本
rustup install 1.65.0
rustup default 1.65.0
rustup default beta/nightly

// 仅当前目录使用某个rust版本
rustup override set nightly/1.65.0

编译运行

main.rs:

fn main() {
      println!("Hello, world!");
}

然后执行:

rustc main.rs

编译出来的文件直接可运行

./main

输出:Hello, world!

Cargo

在安装rust的时候,自带了cargo,使用rustup update升级rust的时候,也自动升级了cargo

使用 cargo 创建项目:

cargo new hello_cargo

cargo使用toml组织内容:

[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/
reference/manifest.html

[dependencies]
// 在java中,依赖可以叫jar包,在rust中,叫crate
rand = "0.8.5"

构建,默认是 debug build

cargo build

构建以后,可执行文件在 ./target/debug/ 目录中

可以直接使用 cargo run 实现构建+运行

也可以使用 cargo check,只检查不编译,速度比较快

构建release:

cargo build --release

在根目录还会生成 cargo.lock 文件,它确保使用的 crates 的版本固定,这样不会因为依赖包产生问题。

更新某个 crate:

cargo update // 它只会更新小版本,比如从 0.8.5 更新到 0.8.9,并且写入到 lock 文件中

文档功能:

cargo doc --open // 生成依赖的crates的文档,并且打开

其他:

cargo clean // 类型 mvn clean,会把target目录删掉
cargo add axum@0.7.2 // 添加crate到dependencies里面
cargo tree // 查看依赖版本

指定registry:

[dependencies]
my_crate = { version = "1.0", registry = "private-registry" }

[registries]
private-registry = { index = "https://your-private-cargo-registry.com" }

依赖不自动传递,如果你依赖了a和b,而且他们都分别依赖了c,并且是不同的版本。

那么首先,你的项目代码中,不可以直接使用c的接口,需要显式依赖。

cargo会下载不同版本的c,并且a和b在链接的时候,分别使用自己依赖的版本的c。

在rust的生态中,很多项目管理包的模式,更喜欢把一个包做大,比如tokio,然后提供不同的features,在依赖tokio的时候需要指定features。这个是因为rust是静态编译的语言,它会在编译的过程中裁剪掉不需要的代码,使用一个大的包,对编译其实是比较友好的。简化依赖管理、提高性能、减少代码重复和编译时间。

Clippy: Clippy 是 Rust 的一部分,通常与 cargo 工具链一起使用,帮助开发者检测代码中的潜在问题、性能瓶颈、错误和风格不一致等

rustup component add clippy // 安装clippy,默认是安装的
cargo clippy // 启动clippy检查
// 通过 clippy.toml 文件或者直接在代码中配置 Clippy 的行为

通常,插件会自动集成clippy

Clippy 是一个静态分析工具,主要用来 运行时 检查代码,不会提供即时的编辑器提示。它通常需要通过命令行(cargo clippy)运行,或者通过 IDE 集成在代码检查中查看警告。

rust-toolchain.toml 文件:

# 指定使用 nightly 版本
[toolchain]
channel = "nightly"

# 或者指定一个具体版本
[toolchain]
channel = "1.58.0"

targets = ["x86_64-unknown-linux-gnu"]
components = ["rustfmt", "clippy"] // 你还可以指定需要的附加组件,例如 clippy、rustfmt、miri 等。这些组件会在安装 Rust 时自动包含,确保你的开发环境配置完备。
override = true

这个文件指定了运行环境的工具链版本,如果在跑rust build的时候,本地没有相关的版本,会自动下载指定的工具链版本

使用 rustup show 可以查看当前的工具链版本

基础语法

use std::io; // 同 java 中的 import,当然你也可以不import,使用全限定名

fn main() {
  let mut a = String::new(); // :: 代表关联函数,针对某个类型实现的函数

  io::stdin()
    .read_line(&mut guess) // 传递 “可变” “引用”
      .expect("Failed to read line"); // Result 对象的处理,Ok/Err

  println!("You guessed: {}", guess); // 宏
  
  let guess: u32 = guess.trim().parse().expect("Please type a number!"); // shadowing,复用 guess 这个变量名,类型已经变成了 u32
  
  println!("You guessed: {guess}");
  
  match guess.cmp(&secret_number) { // match 模式匹配,需要穷举
      Ordering::Less => println!("Too small!"),
      Ordering::Greater => println!("Too big!"),
      Ordering::Equal => println!("You win!"),
  }
}

模式匹配:

let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

常见编程概念

let 定义变量,可变变量和不可变变量。

const 定义常量,必须指明类型,永远不可变,同时它可以定义在任何作用域

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

复用名字,成为 shadowing,它和原来的对象其实没什么关联,只不过共用了同一个名字而已。

rust 数据类型:

i8 u8
i16 u16
i32 u32
i64 u64
i128 u128
isize usize
f32 f64 // 默认是f64
let t: bool = true
let c: char = 'a'; // rust 中的 char 和 java(只能表示ascii)不一样,它用于表示一个unicode标量值,占4字节,可表示中文、emoji等。

let tup: (i32, u32) = (100, 1); // 复合类型:元组,tup.0, tup.1
let a: [i32; 5] = [1,2,3,4,5]; // 复合类型:数组,a[1], a[4],rust 不允许数据越界访问,panic

字面值:

123_456 // 十进制
0xff // Hex 十六进制
0o123 //Octal 八进制
0b11100 // Binary 二进制
b'A' // 单字节字符(仅限于u8)
b"abc" // 字节字符串字面量(byte string literal),等价于 &[115, 115, 115] 这种,也就是字节数组

rust 函数参数需要指定类型和返回值类型(如果有返回值)

fn plus_one(x: i32) -> i32 { 
  x+1
}

rust 函数有一个特点,最后一行不带分号,代表其是返回值:

fn main() {
    let y = {
			let x = 3;
			x+1 
    };
    println!("The value of y is: {y}"); // y=4
}

rust 使用 // 注释代码

rust 中的 if 和 java 几乎是一样的,只不过 rust 不喜欢加括号。另外 if 是一个表达式,所以可以用在 let 中:

let number = if condition { 5 } else { 6 };

rust 中的循环,可以用 loop, while, for,都非常简单,rust 的 for 比较方便做遍历:

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

全局变量

使用 static,并且必须在声明的时候初始化。

static G1: i32 = 10;

// 也可以使用 mut 修饰,但是之后的所有的读写都需要使用 unsafe
static G2: i32 = 0;
unsafe {
  G2 = 5;
}

可以在任意地方定义全局变量,它不在栈上分配,所以即使方法结束了,这个全局变量的生命周期在程序退出才结束。

常量使用 const 定义。

const GLOBAL: i32 = 0;

常量不允许使用 mut,const 定义的常量不一定会分配内存空间,可能直接在编译器就被内联优化了。

如果 tuple 只有一个元素,最后应该加一个逗号:

let a = ("1",);

Rust 中的 ownership

rust 无需 GC 就可以保证内存安全,跟所有权的设计息息相关,在编译阶段就检查出问题代码。

内存:栈和堆

栈的速度快,但是需要计算好需要多少内存,当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。

所有权3个规则:

  1. Rust 中的每一个值都有一个所有者(owner)。
  2. 值在任一时刻有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。

移动 move:

对于基础类型,在栈上进行复制操作:

let x = 5;
let y = x; // 生成一个值 x 的拷贝并绑定到 y,栈上有两个 5

对于复杂类型,有点类似于 java 的浅拷贝,但是rust同时使第一个变量失效了,所以这个操作被称为移动(move)

let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!"); // s1 此时不再可用,编译错误

使用 clone 方法可以做深拷贝。

基础类型实现了 Copy trait,如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。包括元组这种,如果里面的值都是基础类型,那也是实现了Copy trait。

Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。

引用,借用(borrowing)

fn main() {
      let s1 = String::from("hello");
      let (s2, len) = calculate_length(s1);
      println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
		let length = s.len(); // len() 返回字符串的长度 
  	(s, length) 
}

在这段代码中,s1的所有权让渡给了calculate_length方法,在调用这个calculate_length方法以后,s1已经不能再继续使用,为了达到s1还能继续使用的目的,我们在方法中需要返回这个所有权,赋给s2,后续使用 s2。

这样做,多多少少有点大病,所以才需要使用引用:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
    s.len()
}

这种情况允许我们访问这个值,但是不获取它的使用权。

创建引用的过程,我们称为 borrowing。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。我们并不拥有它。

默认情况下,引用不允许修改引用的值,所以引申出来可变引用:

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
}
fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

可变引用会暂时夺得使用权,然后再归还。

显然rust不允许同时 borrow 多个可变引用:

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;

当然这里面也包含说,如果一个值已经存在了不可变引用,那么不允许borrow可变引用,这样会产生 race condition问题。

let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题

多个不可变引用是可以的,因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。

但是不可变引用的用户可不希望在他们的眼皮底下值就被意外的改变了,所以不能和可变引用同时存在。

另外要注意一点,一个引用的作用域从声明的地方开始一直持续到最后一次使用为止,比如下面的代码:

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在println以后就没用了,所以他们的作用域到此为止。

Slice

slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。slice 是一种引用,所以它没有所有权

字符串 slice(string slice)是 String 中一部分值的引用,它看起来像这样:

let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];

在其内部,slice 的数据结构存储了 slice 的开始位置和长度。

所以对于 let world = &s[6..11]; 的情况, world 将是一个包含指向s 索引6的指针和长度值5的slice。

fn first_world(source: &String) -> &str {
  let bytes = source.as_bytes();
  
  for (i, &item) in bytes.iter().enumerate() {
    if item == b' ' {
      return &s[0..i];
    }
  }
  &s[..]
}

String slice 会变相 borrow 值的不可变引用,这样可以解决下面的一个问题:

let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // 错误!
println!("the first word is: {}", word);

word 是 s 的不可变引用,并且它的作用域没有结束,但是 s.clear() 这行使用了可变引用,显然是不行的。

字符串字面值就是 slice

let s = "Hello, world!";

这里s 的类型是&str: 它是一个指向二进制程序特定位置的slice。这也就是为什么字符串字面值是不可变的,&str 是一个不可变引用

我们把 first_world 改写成使用 slice 的方式:

fn first_world(s: &str) -> &str {}

参数中的 &str 有很多种方式可以获取:

let my_string = String::from("hello world");

// `first_word` 适用于 `String`(的 slice),部分或全部
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);

// `first_word` 也适用于 `String` 的引用,
// 这等价于整个 `String` 的 slice
let word = first_word(&my_string);

let my_string_literal = "hello world";
// `first_word` 适用于字符串字面值,部分或全部
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);

// 因为字符串字面值已经 **是** 字符串 slice 了,
// 这也是适用的,无需 slice 语法!
let word = first_word(my_string_literal);

字符串 slice 是针对字符串的,还有针对其他类型的 slice,也是同样的道理,本质上都是 borrow 一个不可变引用。

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);

Self, borrowing, ref, deref

Self:

struct Foo(i32);

impl Foo {
    // No `self`.
    fn new() -> Self {
        Self(0)
    }

    // Consuming `self`.
    fn consume(self) -> Self {
        Self(self.0 + 1)
    }

    // Borrowing `self`.
    // &self 其实是 self: &Self 的缩写
    fn borrow(&self) -> &i32 {
        &self.0
    }

    // Borrowing `self` mutably.
    fn borrow_mut(&mut self) -> &mut i32 {
        &mut self.0
    }
}

// This method must be called with a `Type::` prefix.
let foo = Foo::new();
assert_eq!(foo.0, 0);

// Those two calls produces the same result.
let foo = Foo::consume(foo);
assert_eq!(foo.0, 1);
let foo = foo.consume();
assert_eq!(foo.0, 2);

// Borrowing is handled automatically with the second syntax.
let borrow_1 = Foo::borrow(&foo);
let borrow_2 = foo.borrow();
assert_eq!(borrow_1, borrow_2);

// Borrowing mutably is handled automatically too with the second syntax.
let mut foo = Foo::new();
*Foo::borrow_mut(&mut foo) += 1;
assert_eq!(foo.0, 1);
*foo.borrow_mut() += 1;
assert_eq!(foo.0, 2);

Struct

结构体整体来说,相对比较简单。

它就是用来组织相关联的数据

定义:

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

使用:

let mut user1 = User {
    active: true,
    username: String::from("someusername123"),
    email: String::from("someone@example.com"),
    sign_in_count: 1,
};
let user2 = User {
    email: String::from("another@example.com"),
    ..user1
};

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,
        email,
        sign_in_count: 1,
    } 
}

// tuple struct
// 元祖结构体,没有字段名
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

// unit-like struct
// 没有任何字段的结构体
struct AlwaysEqual;

结构体如果要使用引用,需要使用生命周期。比如下面这样是不行的:

struct User {
    active: bool,
    username: &str,
    email: &str,
    sign_in_count: u64,
}

如果要打印结构体,需要用到 Display trait。可以让结构体加上 Debug trait,然后使用 Debug 的输出格式 ”:?”

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}
fn main() {
    let rect1 = Rectangle {
      width: 30,
      height: 50,
    };
    println!("rect1 is {rect1:?}"); // rect1 is Rectangle { width: 30, height: 50 }
}

所有实现了 Debug trait的结构体,会自动实现 Display trait,具备 to_string() 能力。

结构体中的方法

方法类似于函数,只不过它的第一个参数总是 self

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

&self 实际上是 “self: &Self” 的缩写

方法可以选择获得 self 的所有权,或者像我们这里一样不可变地借用 self,或者可变地借用 self,就跟其他参数一样

fn area(self: Self) {}
fn area(self: &Self){}
fn area(self: &mut Self){}

使用:

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    rect1.area(); // 也可以用 Rectangle::area(rect1);
}

方法具备自动引用和解引用,下面是等价的

p1.distance(&p2);
(&p1).distance(&p2);

关联函数

如果我们在 impl 的函数中,不使用 self 作为第一个参数,那就是关联函数,比如 String::new() 这种方法。

impl Rectangle {
  fn square(size: u32) -> Self {
      Self {} 
}

另外就是,我们可以使用多次 impl 块来定义同一个结构体的方法或关联函数。虽然大部分场景没必要,但是是允许的。

others

fn main() {
  let mut p = Point {x: 0, y: 0};
  
  let x = &mut p.x; // 这里获取了 p 和 p.x 的可变引用
}

如果只有一个属性,通常就不要指定属性名字了:

pub struct Keypair(ed25519_dalek::Keypair);

Enum

rust 的 enum 比较典型的在于,它的每一个值,可以是不同的类型,可以是结构体,可以是元组:

enum Message {
	Quit,
	Move { x: i32, y: i32 },
	Write(String),
	ChangeColor(i32, i32, i32),
}

枚举和结构体有一个相似的,就是可以给它们定义方法:

enum Message {
  Quit,
  Move { x: i32, y: i32 },
  Write(String),
}

impl Message {
  fn call(&self) {
    
  }
}

fn main() {
  let m = Message.Write(String::from("sssss"));
  m.call();
}

使用结构体:

enum IpAddr {
  V4,
  V6
}
// 使用 IpAddr::V4

enum IpAddr {
    V4(String),
    V6(String),
}
// 直接让枚举来hold值
IpAddr::V4(String::from("xxxx"))

Option

**Rust 语言是没有设计 null 的。**如果我们要表达类似的语义,可以使用枚举 Option<T>

enum Option<T> {
    None, 
    Some(T), 
}

Option 是 prelude 的,所以我们不需要显式 use。

处理 option 的值,需要使用 match。match 比较有趣的点,在于它可以获取到枚举中的值。

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

match 的分支必须穷尽所有的可能。

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    other => move_player(other),
}

最后一个分支,常用的就是通配符,比如上面的other,名字随意,用于处理其他情况,或者我们不关心值的时候,直接使用 _ 这个符号。

如果我们始终用 match 来处理 Option 的值,会有些麻烦:

let a = Some(10);
match a {
  Some(v) => println!("v: {}", v),
  _ => ()
}

if let 就是用来解决这个问题的。

let a = Some(10);
if let Some(v) = a {
  println!("v: {}", v);
}

If let 是 match的语法糖,它忽略了其他的情况(我不关心其他情况,相当于空实现),使得枚举得不到穷尽,但是有利有弊。

当然可以用 else 来替代 match 中 _ 的使用。

let a = Some(10);
if let Some(v) = a {
  
} else {
  
}

模块管理

main.rs 作为app的起点

lib.rs 作为库crate的起点

在 main.rs 或 lib.rs 中使用 mod xxx 来声明模块,那么需要存在 src/xxx.rs 或 src/xxx/mod.rs

子模块在 xxx.rs 中使用 mod 或 pub mod 定义。

pub use 重导出,这样导出来的api可以屏蔽结构:

mod utils {
    pub mod math {
        pub fn add(a: i32, b: i32) -> i32 {
            a + b
        }
    }
}

mod api {
    pub use crate::utils::math::add; // 引入并重新导出

    pub fn calculate() {
        let result = add(2, 3); // 当前模块可以使用
        println!("Result: {}", result);
    }
}

fn main() {
    api::calculate();

    // 外部模块也可以通过 `api` 访问 `utils::math::add`
    let result = api::add(5, 7); // 可以直接访问
    println!("Result: {}", result);
}

一个典型的结构:

//main.rs
use project_name::models::enum

// lib.rs
mod models;
mod structs;

models.rs
models/enum.rs // models 下面的子模块

几个关键字 crate, self, super, {project_name}

比较喜欢的组织方式:

// 1. main.rs:
mod model
mod api
mod config

// 2. /model/mod.rs:
mod ad_search_model
mod xxx

pub use ad_search_model::*;

集合

讨论 vector,string 和 HashMap

Vec

Vec 就是 java 中的 list。

let v: Vec<i32> = Vec::new();

rust 内置了宏来处理简单的场景:

let v = vec![1,2,3,4,5];

读取 vec 中的元素,非常简单:

let a = &v[2];
//
let a: Option<i32> = v.get(2);

同样的道理,这里 borrow 了一个不可变引用。所以编译器会阻止可变引用的一些操作。

遍历:

for i in &v {
  println!("{i}");
}

如果想要改变其中的值,需要可变引用:

let mut v = vec![100, 32, 57];
for i in &mut v {
		*i += 50; 
}

为了修改可变引用所指向的值,在使用+= 运算符之前必须使用解引用运算符(* )获取i 中的值。

当然,我们不可以在 for 循环中插入或删除项。

vector 只能存放相同的类型数据,有时候不方便,这个时候可以用枚举套一层。

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。

let v = vec![1,2,3];

// 不可变借用
for i in &v {}
for i in v.iter() {}

// 可变借用
for i in &mut v {}
for i in v.iter_mut() {}

//转移所有权
for i in v {}
for i in v.into_iter() {}

iter(), iter_mut(), into_iter() 这几个方法还蛮重要的,基本上大部分的API都是这么设计的。

字符串

Rust 的核心语言中只有一种字符串类型: 字符串 slice str ,它通常以被借用的形式出现,&str 。

字符串(String )类型由 Rust 标准库提供,而不是编入核心语言,它是一种可增长、可变、 可拥有、UTF-8 编码的字符串类型。

String 本质上是一个 vec:

pub struct String {
    vec: Vec<u8>,
}

它使用 UTF-8 编码,所以可以包含任何可以正确编码的数据。

append 字符串的 push_str 使用的是 slice,并不需要获取所有权。

let mut a = String::from();
let b = "hello";
a.push_str(b);

// b 依然可用

使用 + 号拼接以后,会失去所有权:

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用

这个 + 相当于调用了一个 add 方法:

fn add(mut self, other: &str) -> String {
  self.push_str(other);
  self
}

这个方法会获取 s1的所有权,然后返回结果的所有权

这里面还有一个细节,&s2,s2是 String 类型,但是这里做了一次 deref 强制转换,转成 &str 类型。

format 宏用于拼接:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");

format 宏不会获取任何参数的所有权。

遍历字符串不可以用索引法,&s[index] 会 panic,可以使用 chars() 方法,它会返回字形簇:

for c in "Зд".chars() {
    println!("{c}");
}

另外bytes 方法返回每一个原始字节:

for b in "Зд".bytes() {
  println!("{b}")
}
将打印出:
208
151
208
180

每个 Unicode 标量值需要两个字节存储

HashMap

let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// 这里 field_name 和 field_value 不再有效,

对于像i32 这样的实现了Copy trait的类型,其值可以拷贝进哈希map。对于像String 这样拥有所有权的值,其值将被移动而HashMap 会成为这些值的所有者。

如果将值的引用插入哈希 map,而不是所有权转移,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的,这个需要生命周期来确保引用有效。

获取值:

let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
// get 返回 Option<&V>,copied 方法返回 Option<V>

// 另一个方法就是遍历:
for (key, value) in &scores {
   println!("{key}: {value}");
}

insert 方法,如果有旧值,会进行覆盖。

如果要实现只有当值没有时才插入:

let count = scores.entry(String::from("Yellow")).or_insert(50);

or_insert 会返回值的可变引用:

*count += 1;

错误处理

use std::fs::File;
use std::io::ErrorKind;
fn main() {
    let greeting_file_result = File::open("hello.txt"); // 返回 io::Result<T> = result::Result<T, io::Error>
    let greeting_file = match greeting_file_result {
        Ok(file) => file,	
        Err(error) => match error.kind() {
              ErrorKind::NotFound => match File::create("hello.txt") {
									Ok(fc) => fc,
                  Err(e) => panic!("Problem creating the file: {:?}", e),
              },
              other_error => {
                  panic!("Problem opening the file: {:?}", other_error);
							} 
      	},
    };
}

使用 match 容易造成这种很深的嵌套,可以使用闭包:

let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
    if error.kind() == ErrorKind::NotFound {
        File::create("hello.txt").unwrap_or_else(|error| {
            panic!("Problem creating the file: {:?}", error);
        })
    } else {
        panic!("Problem opening the file: {:?}", error);
}

失败时panic的简写:unwrapexpect

let greeting_file = File::open("hello.txt").unwrap();
let greeting_file = File::open("hello.txt")
          .expect("hello.txt should be included in this project");

传播错误:

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");
    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e), // 这里 return
	  };
    let mut username = String::new();
    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
	} 
}

传播错误更简单的方式就是使用 ?

  fn read_username_from_file() -> Result<String, io::Error> {
      let mut username_file = File::open("hello.txt")?;
      let mut username = String::new();
      username_file.read_to_string(&mut username)?;
      Ok(username)
}

? 的大致意思就是,如果有 Error 就返回 Error。它涉及到 From trait

? 消除了部分样板代码,比如甚至可以简写成下列的格式:

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();
    File::open("hello.txt")?.read_to_string(&mut username)?;
    Ok(username)
}

? 也可以用于 Option:

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

最佳实践

core::error::Error 是一个trait:

pub trait Error: Debug + Display {...}

我们在 Result 中使用的是 std::fmt::Error,它是一个 struct,它没有任何的成员:

pub struct Error;

同时,它实现了 Error trait:

#[stable(feature = "fmt_error", since = "1.11.0")]
impl Error for crate::fmt::Error {
    #[allow(deprecated)]
    fn description(&self) -> &str {
        "an error occurred when formatting an argument"
    }
}

通常,我们会使用anyhow的Error来替代fmt::Error,然后自己定义自己的 Result:

type Result<T> = std::result::Result<T, anyhow::Error>;

当然这种情况直接使用 anyhow::Result就可以了:

pub type Result<T, E = Error> = core::result::Result<T, E>;

anyhow为 Result 实现了 Context trait,这样就可以添加 context:

f().with_context(||"");

如果想自定义error枚举,类似java的做法,通常我们会使用 thiserror 这个 crate:

#[derive(thiserror::Error, Debug)]
pub enum MyError {

    #[error("File not found: {0}")]
    FileNotFound(String),
}

它通过使用宏来实现让我们的 MyError 自动实现标准库的 Error trait。

这样,我们也可以结合 anyhow 和 thiserror 来使用,总体使用 anyhow 来管理错误,自定义错误用 thiserror:

fn test() -> anyhow::Result<String> {
    Err(anyhow::anyhow!("This is an error!"))
}

fn t() -> anyhow::Result<(), MyError> {
    Err(MyError::FileNotFound("config.toml".to_string()))
}

#[derive(thiserror::Error, Debug)]
pub enum MyError {

    #[error("File not found: {0}")]
    FileNotFound(String),
  
    #[error("Invalid configuration")]
    InvalidConfig,
}

如果是业务异常,使用 thiserror 定义好的枚举类返回,如果是一些更宽泛的异常,使用 anyhow 返回。

泛型、trait、lifetime

泛型

函数中使用泛型:

fn fun<T>(arg: &T) -> &T {}

在结构体中使用泛型:

struct Point<T> {
  x: T,
  y: T,
}

枚举:

enum Option<T> {
  Some(T),
  None,
}

方法定义中的泛型:

impl<T> Point<T> {
  fn x(&self) ->&T {
    &self.x
  }
}

impl Point<f32> {
  ...
}

Rust 的泛型和其他语言不同,它是在编译器进行推导的,所以不存在性能问题。

在编译过程实现单态化。

Trait

rust 中的 trait 有点类似 java 的接口,但是区别也蛮大的。

trait 代表某个功能,所以 rust 中的 trait 命名上往往都是动词,比如 Copy, Debug, From 等。

pub trait Summary {
      fn summarize(&self) -> String;
}

trait里面可以只定义方法签名,也可以默认实现。

为某个类型添加 trait,使用 impl SomeTrait for SomeStruct:

impl Summary for NewsArticle {
  fn summarize(&self) -> String {
    
  }
}

只有在 trait 或者类型至少有一个属于当前 crate 时,我们才能对类型实现该trait。

这样是为了防止其他人编写的代码不会破坏你的代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。

我们可以使用 trait 作为函数参数:

pub fn notify(item: &impl Summary) {
    println!( "Breaking news! {}", item.summarize());
}

trait bound 语法:

trait bound 指 trait 约束

// 
pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

//
pub fn notify<T: Summary>(item1: &T, item2: &T) {
  
}

// 通过 + 指定多个 trait bound
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 { 

可以简化为:

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

可以提升代码的可阅读性。

函数返回实现了trait的类型

fn fun() -> impl Summary {
  NewsArticle {
    xxx
  }
}

跟 java 有点像,Runnable, Callable 这类接口在 lambda 表达式中非常有用,在 rust 中,trait 在闭包中也会非常好用。

另外,还可以使用 trait bound 有条件地实现方法,比如:

struct Pair<T> {
	x: T,
	y: T,
}
impl<T> Pair<T> {
	fn new(x: T, y: T) -> Self {
		Self { x, y } 
  }
}
impl<T: Display + PartialOrd> Pair<T> {
	fn cmp_display(&self) {
    // 
  }
}

也就是说,它只为实现了 Display 和 PartialOrd 的类型添加这个cmp_display实现。

比如标准库为所有实现了 Display trait 的类型实现了 ToString traint,像这样:

impl<T: Display> ToString for T {
	fn to_string() {
    
  }
}

lifetime

Rust 中的 borrow checker 负责比较作用域,来确保所有的borrow都是有效的。

确保引用是有效的。防止悬垂引用(导致程序引用到它不应该引用的数据)。

生命周期注解的语法:紧跟在引用 & 的后边,用空格与引用类型分开,如 &‘a i32

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

这个生命周期注解,告诉 borrow checker,我们希望参数 x,y 和返回的引用,存活的一样久。

它的实际含义是longest 函数返回的引用的生命周期与函数参数所引用的值的生命周期的较小者一致。这些关系就是我们希望 Rust 分析代码时所使用的。

下面这段代码不能编译,因为 result 的生命周期和较短的 string2 相同

let string1 = String::from("long string is long");
let result;
{
    let string2 = String::from("xyz");
    result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);

下面的代码可以正常运行,因为 result 的生命周期和 string1/string2 一致。

let string1 = String::from("long string is long");
let string2 = String::from("xyz");
let result;
{
    result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);

深入理解生命周期

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

这段代码表明,返回引用的生命周期和参数 x 一致。

如果返回的引用 没有 指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,这将会是一个垂悬引用 dangling ref,因为它将会在函数结束时离开作用域。

比如这段代码:

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

result 在这个函数结束的时候,它的数据就会被清除。所以返回值就会变成一个垂悬引用,但是rust是不允许我们创建垂悬引用的。

最好的解决方案,是返回一个有所有权的数据类型而不是一个引用,让函数调用者拥有这个所有权,并且负责清理这个值。

综上,生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的。一旦它们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。

结构体中的生命周期

之前的结构体定义都是包含拥有所有的类型。

struct ImportantExcerpt<'a> {
	part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

和函数中的生命周期同理,上面的代码中,结构体 i 的生命周期,和 part 字段的引用 first_sentence 生命周期一致。

编译器会尽可能推导出生命周期,只有在推导不出来的时候,才需要强制指定,总共有三条规则:

1、编译器会为每个参数都分配一个生命周期参数

fn foo<'a, 'b>(x: &'a i32, y: &'b i32)

2、如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数

fn foo<'a>(x: &'a i32) -> &'a i32

3、如果方法有多个输入生命周期参数,并且其中一个参数是 &self 或 &mut self,说明是个对象方法,那么所有输出生命周期参数被赋予 self 的生命周期。这条规则使得方法更容易读写,因为只需更少的符号。

方法定义中的生命周期注解

对于 struct 中的生命周期注解,impl 和类型后面都需要加上注解:

struct ImportantExcerpt<'a> {
  part: &'a str,
}

// impl 和 类型
impl<'a> ImportantExcerpt<'a> {
  
  // 根据第1条规则,我们不需要为 self 添加生命周期注解
  fn level(&self) -> i32 {
    3
  }
  
  // 编译器能根据第3条规则推导出跟 self 同样的生命周期
  fn announce_and_return_part(&self, announcement: &str) -> &str {
    self.part
  }
}

静态生命周期

‘static 其代表生命周期能够存活于整个程序期间。所有的字符串字面值都拥有 ‘static 生命周期,我们也可以标注出来:

let s: &'static str = "hello world";

闭包

最宽松的闭包应该是FnOnce,其次是FnMut。

**FnOnce 是最宽松、最通用的 trait**

闭包类似于java中的lambda表达式。

闭包 = 匿名函数 + 捕获外部变量。

let f = |x| -> i32 {x+1}
let f = |x| {x+1}
let f = |x:i32| {x}
let f = |x| x // 等待编译器在执行的代码中推导类型,但是一旦确定类型,不可再改变

闭包其实就是一个结构体,所有闭包名称唯一:

hello_cargo::main::closure-0

hello_cargo::main::closure-1(0xfff01)

hello_cargo::main::closure-2(0xfff01, 0xee012)

Fn 表示捕获方式为不可变引用,FnMut 表示捕获方式为可变引用,FnOnce 表示捕获方式为获取所有权。

fn test<T>(f: T) where
	T: Fn()
{
  f();
}

fn main() {
  let s = String::from("xxx");
  let f = || {println!("{}", s);}; // 不可变引用
  test(f);
}

关于所有权的问题,闭包和函数参数有点像,总共三种方式:不可变borrow,可变borrow,以及获取所有权。

1、不可变借用

let list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);

let only_borrows = || println!("From closure: {:?}", list);

println!("Before calling closure: {:?}", list);
only_borrows();
println!("After calling closure: {:?}", list);

2、可变借用

let mut list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);

let mut borrows_mutably = || list.push(7); // 可变借用开始

//... 这个中间,list 一直处于可变借用在外的状态,此时是不可以产生其他借用的。
// println!("list: {:?}", list); 这里不可行。

borrows_mutably(); // 可变借用结束
println!("After calling closure: {:?}", list);

3、获取所有权。在闭包中使用 move。在多线程编程中比较有用。

let list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);
thread::spawn(move || println!("From thread: {:?}", list))
	.join()
	.unwrap();

一旦闭包捕获了定义它的环境中的一个值的引用或所有权,也就影响了什么会被移进闭包,闭包体中的代码定义了稍后在闭包计算时对引用或值如何操作,也就影响了什么会被移出闭包。

闭包体可以做以下任何事:将一个捕获的值移出闭包,修改捕获的值,既不移动也不修改值,或者一开始就不从环境中捕获值。

根据闭包如何处理值,闭包自动、渐进地实现一个、两个或三个 Fn trait。

1、FnOnce。

适用于能被调用一次的闭包,当然所有闭包都至少实现了这个 trait。一个会将捕获的值移出闭包体的闭包,只实现 FnOnce trait,因为它只能被调用一次。

2、FnMut 适用于不会将捕获的值移出闭包体的闭包,但它可能会修改被捕获的值,这类闭包可以被调用多次。

3、Fn 适用于既不将捕获的值移出闭包体,也不修改被捕获的值的闭包,也包括不从环境中捕获值的闭包。这类闭包可以被调用多次而不改变他们的环境,这在会多次并发调用闭包的场景中十分重要。

下面是 Option 中 unwrap_or_else的定义:

impl<T> Option<T> {
      pub fn unwrap_or_else<F>(self, f: F) -> T
      where
          F: FnOnce() -> T
      {
        	match self {
            Some(v) => v,
            None => f(),
        }
			} 
}

这里泛型 F 的 trait bound 时 FnOne()->T,意味着 F 必须能够被调用一次,没有参数并返回一个 T。

闭包的继承关系

大致是说:

  • 所有实现了 Fn 的闭包,也一定实现了 FnMut 和 FnOnce。
  • 所有实现了 FnMut 的闭包,也一定实现了 FnOnce。

用Java的接口来说,FnOnce处于最顶层,如果我们要传递 FnOnce 参数,由于 Fn 实现了 FnOnce,所以也可以用 Fn 的闭包。

fn call_once<F: FnOnce()>(f: F) {
    f();
}

fn call_mut<F: FnMut()>(mut f: F) {
    f();
    f();
}

fn call_fn<F: Fn()>(f: F) {
    f();
    f();
}

这几个方法,要求传递闭包作为参数,如果我们的闭包是这样的:

let x = 10;
let c = || println!("{}", x);

由于 c 只是捕获一个不可变引用,所以它实现的是 Fn 闭包。也就是说,我们可以这样:

call_once(c); // OK
call_mut(c);  // OK
call_fn(c);   // OK

迭代器

在 rust 中,迭代器是 lazy 的。

迭代器都实现了 Iterator trait,它的定义看起来像这样:

pub trait Iterator {
  type Item;
  
  fn next(&mut self) -> Option<Self::Item>;
}

智能指针

rust 中的引用和借用,它们只是借用了数据的指针。而智能指针拥有它们指向的数据。

我们自己可以实现智能指针。

智能指针通常使用结构体实现。不同之处在于智能指针实现了 Deref 和 Drop trait。

Box<T> 指向堆中的数据

box 允许你将一个值放在堆上而不是栈上。 留在栈上的则是指向堆数据的指针。

let arr = [0;10]; //栈上分配
let arr1 = arr; // 栈上拷贝,所以不发生所有权转移
let point = Point {x:0, y: 0}; // 栈上分配

除了数据被储存在堆上而不是栈上之外,box 没有性能损失。

enum List {
      Cons(i32, Box<List>),
      Nil,
}

// 通过解引用符 * 来获取 Box<T> 中的 T
let box = Box::new(Point{x: 0, y: 0});
let unboxed: Point = *box; // *(box.deref()) 

Box::leak() 它可以消费掉 Box,并且强制目标值从内存中泄漏,改变了变量的生命周期

Deref 和 Drop trait

对于智能指针来说,最重要的就是这两个trait,如果我们要定义自己的智能指针,需要实现这两个trait。

struct MyBox<T>(T);
impl<T> MyBox<T> {
	fn new(x: T) -> MyBox<T> {
		MyBox(x) 
  }
}
// 实现解引用
impl<T> Deref for MyBox<T> {
	type Target = T;
	fn deref(&self) -> &Self::Target {
		&self.0 // 解引用是获取到一个引用类型,所以 *mybox 其实就是 *(&T)
	} 
}

impl<T> Drop for MyBox<T> {
  fn drop($mut self) {
    println!("blabla");
  }
}

当实例离开作用域 Rust 会自动调用 drop ,并调用我们指定的代码。变量以被创建时相反的顺序被丢弃。

Rc<T>

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));

Rc::clone 会增加引用计数,可以通过 Rc::strong_count 得到引用计数:

println!("count after creating a = {}", Rc::strong_count(&a));

来自chatgpt的例子:

// 引入 Rc 类型
use std::rc::Rc;

// 定义一个简单的结构体
#[derive(Debug)]
struct Person {
    name: String,
    age: u8,
}

fn main() {
    // 创建一个 Rc 指向 Person 实例的引用计数指针
    let person_rc = Rc::new(Person {
        name: String::from("Alice"),
        age: 30,
    });

    // 克隆 Rc 指针,增加引用计数
    let person_rc_clone1 = Rc::clone(&person_rc);
    let person_rc_clone2 = Rc::clone(&person_rc);

    // 打印每个引用计数指针的所有者数量
    println!("Reference count after cloning: {}", Rc::strong_count(&person_rc));

    // 输出 Person 实例的信息
    println!("Person: {:?}", person_rc);
    println!("Person Clone 1: {:?}", person_rc_clone1);
    println!("Person Clone 2: {:?}", person_rc_clone2);
}

Arc<T>

由于 Rc<T> 是非线程安全的,所以 Arc 是 Rc 的多线程版本,它代表 Atomic Reference Counting。

// 引入 Arc 类型
use std::sync::Arc;
use std::thread;

// 定义一个简单的结构体
#[derive(Debug)]
struct SharedData {
    data: i32,
}

fn main() {
    // 创建一个 Arc 指向 SharedData 实例的引用计数指针
    let shared_data_arc = Arc::new(SharedData { data: 42 });

    // 克隆 Arc 指针,增加引用计数
    let shared_data_arc_clone1 = Arc::clone(&shared_data_arc);
    let shared_data_arc_clone2 = Arc::clone(&shared_data_arc);

    // 创建一个新线程,使用共享数据
    let thread1 = thread::spawn(move || {
        println!("Thread 1: {:?}", shared_data_arc_clone1);
    });

    // 创建另一个新线程,同样使用共享数据
    let thread2 = thread::spawn(move || {
        println!("Thread 2: {:?}", shared_data_arc_clone2);
    });

    // 等待两个线程执行完成
    thread1.join().unwrap();
    thread2.join().unwrap();

    // 输出 Arc 指针的所有者数量
    println!("Reference count after threads: {}", Arc::strong_count(&shared_data_arc));
}

Cell

通过 Cell 实现共享引用的内部可变性

fn test() {
  use std::cell::Cell;
  
  let data: Cell<i32> = Cell::new(1_i32);
  // 创建多个 data 的共享引用,它们都可以访问并修改这个引用的数据
  let a: &Cell<i32> = &data;
  let b: &Cell<i32> = &data;
  
  a.set(2);
  b.set(3);
}

RefCell<T>

Cell 和 RefCell 用于绕过 rust 编译器非常严格的所有权检查。

对于引用和Box<T> ,借用规则的不可变性作用于编译时。对于RefCell<T> ,这些不可变性作用于 运行时

  • Rc 允许相同数据有多个所有者;Box 和RefCell 有单一所有者。

  • Box 允许在编译时执行不可变或可变借用检查;Rc 仅允许在编译时执行不可变借用检查;RefCell 允许在运行时执行不可变或可变借用检查。

  • 因为RefCell 允许在运行时执行可变借用检查,所以我们可以在即便RefCell 自身是不可变的情况下修改其内部的值。

use std::cell::RefCell;

struct Counter {
    value: RefCell<i32>,
}

impl Counter {
    fn new(value: i32) -> Counter {
        Counter {
            value: RefCell::new(value),
        }
    }

    fn increment(&self) { // 这里只是获取不可变引用,但是运行时其实会获取可变引用。
        let mut value: RefMut<i32> = self.value.borrow_mut(); // 注意这里的类型是 RefMut<i32>
        *value += 1; 
    }

    fn get_value(&self) -> i32 {
        *self.value.borrow()
    }
}

fn main() {
    let counter = Counter::new(0);

    // 第一次增加计数器值
    counter.increment();
    println!("Counter value after first increment: {}", counter.get_value());

    let _borrowed_value = counter.value.borrow();
	  // 尝试在不可变引用的情况下再次增加计数器值
    counter.increment(); // 这里会触发 panic
}

在这个例子中,我们创建了一个 Counter 实例,并在不可变引用的情况下增加了计数器的值。然后,我们尝试在获取计数器值的不可变引用后再次增加计数器值。由于 RefCell 会在运行时检查借用规则,发现已经有一个可变引用存在,因此这次增加操作会触发运行时 panic。

所以,运行时检查会在尝试违反借用规则(例如同时存在可变和不可变引用)时引发 panic。这就是我说的在运行时可能会出现 panic 的情况。

Rc, Arc, Cell, RefCell

共享所有权 Rc, Arc

当某个变量需要多个可变引用时,使用 Cell 或 RefCell

  • Cell: 以值传递的方式对内部变量进行操作
  • RefCell:可获得内部变量的可变引用

高级特性

unsafe

unsafe 块中可以进行 5 种操作。

解引用裸指针

裸指针的语法 as *const T 或 as *mut T

创建裸指针是安全的:

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

如果要读写裸指针,此时需要在 unsafe 块中进行:

unsafe {
	println!("r1 is: {}", *r1);
	println!("r2 is: {}", *r2);
}

创建一个指针不会造成任何危险;只有当访问其指向的值时才有可能遇到无效的值。

调用不安全函数或方法

通过在unsafe 块中调用不安全函 数,表明我们已经阅读过此函数的文档并对其是否满足函数自身的契约负责。

fn main() {
  unsafe fn dangerous() {}
  unsafe {
      dangerous();
	} 
}

不安全函数体也是有效的unsafe 块,所以在不安全函数中进行另一个不安全操作时无需新增 额外的unsafe 块。所以 dangerous 方法相当于一个大的 unsafe 块。

创建不安全代码的安全抽象

当我们知道某些事是可以的而 Rust 不知道的时候,就是触及不安全代码的时候了。

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();
    assert!(mid <= len);
    (&mut values[..mid], &mut values[mid..])
}

这个函数,在 rust 的视角中,我们borrow了 values两次,是不允许的,但是我们知道他们使用的是不同的两段。

使用 unsafe 改造:

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
	let len = values.len();
	let ptr = values.as_mut_ptr(); // 访问裸指针
	assert!(mid <= len);
	
	unsafe {
		(	
      slice::from_raw_parts_mut(ptr, mid),
    	slice::from_raw_parts_mut(ptr.add(mid), len - mid),
     )
  }
}

注意无需将split_at_mut 函数的结果标记为unsafe,并可以在安全Rust中调用此函数。我们创建了一个不安全代码的安全抽象。

使用extern函数调用外部代码

调用其他语言的接口。

extern 块中声明的函数在Rust代码中总是不安全的。

extern "C" {
	fn abs(input: i32) -> i32;
}
fn main() {
	unsafe {
		println!("Absolute value of -3 according to C: {}", abs(-3));
	}
}

在 extern “C” 块中,列出了我们希望能够调用的另一个语言中的外部函数的签名和名 称。“C” 部分定义了外部函数所使用的 应用二进制接口(application binary interface,ABI) —— ABI 定义了如何在汇编语言层面调用此函数。“C” ABI 是最常见的,并遵循 C 编程语言的 ABI。

同理,rust 如果要给其他语言提供接口也非常简单。

extern 的使用无需unsafe 。

访问或修改可变静态变量

目前为止全书都尽量避免讨论 全局变量(global variables),Rust 确实支持它们,不过这对于Rust 的所有权规则来说是有问题的。如果有两个线程访问相同的可变全局变量,则可能会造 成数据竞争。

全局变量对应的就是 java 中的 static 变量。

static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
	unsafe {
		COUNTER += inc;
	}
}
fn main() {
	add_to_count(3);
	unsafe {
  	println!("COUNTER: {}", COUNTER);
	}
}

对 static 变量的读写,都使用 unsafe 块。

实现不安全 trait

当trait中至少有一个方法中包含编译器无法验证的不变式(invariant)时trait是不安全的。可以在trait 之前增加unsafe 关键字将 trait 声明为 unsafe ,同时 trait 的实现也必须标记为 unsafe

unsafe trait Foo {
	// methods go here
}
unsafe impl Foo for i32 {
	// method implementations go here
}
fn main() {}

泛型

functions:

struct SGen<T>(T); // Generic type `SGen`.
fn gen_spec_i32(_s: SGen<i32>) {}
    
// Explicitly specified type parameter `char` to `generic()`.
generic::<char>(SGen('a'));

// Implicitly specified type parameter `char` to `generic()`.
generic(SGen('c'));

Implementation:

struct GenericVal<T>(T); // Generic type `GenericVal`

// impl of GenericVal where we explicitly specify type parameters:
impl GenericVal<f32> {} // Specify `f32`
impl GenericVal<S> {} // Specify `S` as defined above

// `<T>` Must precede the type to remain generic
impl<T> GenericVal<T> {}

这种情况,一个 struct 可以有多个 impl。

Trait:

// Non-copyable types.
struct Empty;
struct Null;

// A trait generic over `T`.
trait DoubleDrop<T> {
    // Define a method on the caller type which takes an
    // additional single parameter `T` and does nothing with it.
    fn double_drop(self, _: T);
}

// Implement `DoubleDrop<T>` for any generic parameter `T` and
// caller `U`.
impl<T, U> DoubleDrop<T> for U {
    // This method takes ownership of both passed arguments,
    // deallocating both.
    fn double_drop(self, _: T) {}
}

fn main() {
    let empty = Empty;
    let null  = Null;

    // Deallocate `empty` and `null`.
    empty.double_drop(null);

    //empty;
    //null;
    // ^ TODO: Try uncommenting these lines.
}

bouds

fn printer<T: Display>(t: T) {
    println!("{}", t);
}

T 必须是实现了 Display 这个 trait 的类型。

Multiple bounds

fn compare_prints<T: Debug + Display>(t: &T) {
    println!("Debug: `{:?}`", t);
    println!("Display: `{}`", t);
}
fn compare_types<T: Debug, U: Debug>(t: &T, u: &U) {
    println!("t: `{:?}`", t);
    println!("u: `{:?}`", u);
}

Where clauses

impl <A: TraitB + TraitC, D: TraitE + TraitF> MyTrait<A, D> for YourType {}

// Expressing bounds with a `where` clause
impl <A, D> MyTrait<A, D> for YourType where
    A: TraitB + TraitC,
    D: TraitE + TraitF {}

Associated types

struct Container(i32, i32);

// A trait which checks if 2 items are stored inside of container.
// Also retrieves first or last value.
trait Contains {
    // Define generic types here which methods will be able to utilize.
    type A;
    type B;

    fn contains(&self, _: &Self::A, _: &Self::B) -> bool;
    fn first(&self) -> i32;
    fn last(&self) -> i32;
}

impl Contains for Container {
    // Specify what types `A` and `B` are. If the `input` type
    // is `Container(i32, i32)`, the `output` types are determined
    // as `i32` and `i32`.
    type A = i32;
    type B = i32;

    // `&Self::A` and `&Self::B` are also valid here.
    fn contains(&self, number_1: &i32, number_2: &i32) -> bool {
        (&self.0 == number_1) && (&self.1 == number_2)
    }
    // Grab the first number.
    fn first(&self) -> i32 { self.0 }

    // Grab the last number.
    fn last(&self) -> i32 { self.1 }
}

常见的trait

Debug

我们通常在 struct 上面使用这个 trait。

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

然后就可以打印了:

fn main() {
    let point = Point { x: 1, y: 2 };
    println!("{:?}", point);  // 单行,大概是 Point { x: 1, y: 2 }
    println!("{:#?}", point);  // 多行
  	
    // 也可以这么写
  	println!("{point:#?}");
}

Display

Display定义在标准库中:

pub trait Display {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}

我们需要实现它:

use std::fmt;

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

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

fn main() {
    let point = Point { x: 1, y: 2 };
    println!("{}", point);
}
  • 目的不同Debug 用于调试,输出类型的内部信息;Display 用于用户友好地展示类型信息。

  • 自动派生Debug 可以通过 #[derive(Debug)] 自动派生,Display 需要手动实现。

  • 格式化要求Display 的输出通常更加简洁和用户友好,而 Debug 的输出则更详尽。

ToString trait

实现了 Display trait 的类型会自动实现 ToString trait

use std::fmt;

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

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

fn main() {
    let point = Point { x: 1, y: 2 };
    let point_string = point.to_string(); // 这里
    println!("{}", point_string);
}

Clone, Copy

Clone:

  • 提供类型的深度拷贝能力。拷贝出来的对象是完全不同的另一个对象。
  • 可以通过 #[derive(Clone)] 自动派生。
  • 常用于需要复制对象的场景。
#[derive(Clone)]
struct MyStruct {
    field: i32,
}

Copy trait 表示类型可以按位复制。这种复制非常高效且快速,但有一定的限制,适用于小型的、不可变的数据结构,如基本类型。

#[derive(Copy, Clone)]
struct MyStruct {
    field: i32,
}

fn main() {
    let original = MyStruct { field: 42 };
    let copy = original; // 按位复制
    println!("Original: {}, Copy: {}", original.field, copy.field);
}

CopyClone 的区别

  • 语义不同Clone 表示深度拷贝,需要显式调用 clone 方法;Copy 表示按位复制,赋值操作会自动进行复制。
  • 性能差异Copy 更高效,因为它是按位复制;Clone 可能涉及更复杂的拷贝逻辑。
  • 适用范围Copy 仅适用于小型的、不可变的数据结构;Clone 适用于所有需要复制的场景。
  • 自动派生Copy 需要同时实现 Clone,但 Clone 不需要 Copy

Copy 要求所有的field都是可复制的,也就是说需要是基础数据类型,比如下面这个就不行:

#[derive(Copy, Clone)]
struct MyStruct {
    field: String, // String 不实现 Copy
}

可以简单理解 Clone:

  • 如果是基础类型,是在栈上进行的复制操作,let x = 10; let y = x.clone() 这种操作等价于 let y = x;
  • 如果是struct,但是实现了Copy trait,那么 let user2 = user1 这种操作也是同基础类型一致,都是栈上复制,不涉及所有权
  • String, Vec 这种在堆上分配空间的,clone() 是分配新的空间,进行的数据复制
  • 智能指针 Rc, Arc 这种实现 Clone 的方式比较特殊,调用 clone() 方法并没有做复制,而是多了一个共享的指针。

PartialEq 和 Eq

PartialEq 可以自动派生,也可以手动实现:

#[derive(PartialEq)]
struct MyStruct {
    field: i32,
}

fn main() {
    let a = MyStruct { field: 42 };
    let b = MyStruct { field: 42 };
    let c = MyStruct { field: 24 };

    println!("a == b: {}", a == b); // 输出: true
    println!("a != c: {}", a != c); // 输出: true
}
=================
struct MyStruct {
    field: i32,
}

impl PartialEq for MyStruct {
    fn eq(&self, other: &Self) -> bool {
        self.field == other.field
    }
}

fn main() {
    let a = MyStruct { field: 42 };
    let b = MyStruct { field: 42 };
    let c = MyStruct { field: 24 };

    println!("a == b: {}", a == b); // 输出: true
    println!("a != c: {}", a != c); // 输出: true
}

Eq trait 是 PartialEq 的子 trait,用于定义完全相等性。

Eq 和 PartialEq 的主要gap在于完全相等性。

实现了 Eq trait 的类型必须满足自反性(reflexive),即 a == a 必须为真。

浮点数类型 f32f64 实现了 PartialEq,但没有实现 Eq,因为浮点数的 NaN 值不满足完全相等性要求。

let a: f32 = std::f32::NAN;
let b: f32 = std::f32::NAN;
println!("a == b: {}", a == b); // 输出: false

PartialOrd 和 Ord

PartialOrd trait 用于定义类型的部分顺序关系。实现了 PartialOrd 的类型可以使用 <<=>>= 操作符进行比较。

Ord trait 是 PartialOrd 的子 trait。

#[derive(PartialOrd, Ord)]
struct MyStruct {
    field: i32,
}

这两个trait都可以手动实现。

当一个结构体有多个属性时,自动派生 PartialOrdOrd 会按照属性的声明顺序逐个比较属性。Rust 会从第一个属性开始比较,如果它们相等,则继续比较下一个属性,依此类推,直到找到不相等的属性或所有属性都比较完毕。

From, Into

类似于java中的converter。

通常情况下,你需要手动实现 From trait。例如,将一个 String 转换为 Person 结构体:

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

impl From<&str> for Person {
    fn from(name: &str) -> Self {
        Person {
            name: name.to_string(),
            age: 0,
        }
    }
}

fn main() {
    let person = Person::from("Alice");
    println!("Name: {}, Age: {}", person.name, person.age);
}

Into trait 是 From trait 的对偶关系。实现了 From trait 的类型会自动实现 Into trait。Into trait 提供了 into 方法进行类型转换。

也就是说,我们如果实现了 From trait,可以直接使用相应的 into() 方法:

fn main() {
    let person: Person = "Alice".into();
    println!("Name: {}, Age: {}", person.name, person.age);
}

Send

在 Rust 中,Send 是一个标志性(marker)trait,表示一个类型的值可以安全地从一个线程转移到另一个线程。它是 Rust 并发模型中的核心组件之一,保证了线程之间的内存安全。

pub trait Send { }
  • Send 是一个标志性 trait,不需要实现任何方法。Rust 编译器会自动为符合条件的类型实现 Send。

  • 类型默认是 Send,只要它们内部不包含非 Send 类型。

    • 比如 Rc,需要使用 Arc。还有Cell。
    • 裸指针(const T 和 mut T)**:裸指针不保证线程安全。
    • 特定的外部库类型:某些与底层系统交互的类型可能没有实现 Send

和 Send 相关的是 Sync trait:

  • Send:表示类型的值可以在线程之间安全传递。

  • Sync:表示类型的引用可以在线程之间安全共享。

如果一个类型是 Sync 且它的值是不可变的,那么它通常也会是 Send。但反之并不总是成立。

Sized

它与普通trait不同,编译器对它有特殊的处理。用户也不能针对自己的类型impl这个trait。一个类型是否满足Sized 约束是完全由编译器推导的,用户无权指定。

Thread

thread::spawn

handler.join()

use std::thread;
use std::time::Duration;

fn main() {
    // 启动一个新线程
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("来自子线程的消息: {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    // 主线程继续执行
    for i in 1..5 {
        println!("来自主线程的消息: {}", i);
        thread::sleep(Duration::from_millis(1));
    }

    // 等待子线程完成
    handle.join().unwrap();
}

Tokio

异步任务线程池:CPU 核心数

任务调度队列

  • 每个工作线程有本地任务队列
  • 全局共享任务队列
  • 支持工作窃取(Work Stealing)机制

调度算法

  • 轮询执行任务
  • 当任务遇到await时让出执行权
  • 其他就绪任务可以立即执行
use tokio::runtime::Runtime;

fn main() {
    let runtime = Runtime::builder()
        .worker_threads(4)     // 异步任务线程池
        .max_blocking_threads(20)  // 阻塞任务线程池
        .build()
        .unwrap();

    runtime.block_on(async {
        // 异步任务使用主线程池
        tokio::spawn(async {
            println!("异步任务");
        }).await.unwrap();

        // 阻塞任务使用独立线程池
        tokio::spawn_blocking(|| {
            println!("阻塞任务");
        }).await.unwrap();
    });
}

spawn_blocking()

  • 用于CPU密集型同步任务
  • 仅在无法异步时使用spawn_blocking()
  • 在专门的阻塞线程池中执行
  • 防止阻塞异步运行时
  • 默认最大值通常为CPU核心数的512倍
tokio::spawn_blocking(|| {
    // 复杂的CPU计算
    let result = (0..1_000_000).map(|x| x * x).sum::<u64>();
});

结论:尽可能使用异步库,选择spawn(),仅在无法异步时使用spawn_blocking()

Macro 宏

https://github.com/dtolnay/proc-macro-workshop

分为:声明宏和过程宏

声明宏就是 println! 这种,它的工作机制比较简单,就是正则表达式替换。

过程宏是类似 #[derive] 这种,包括属性宏,派生宏。。。

定义过程宏需要在一个独立的 crate 中。

过程宏

toml 中添加依赖:

[dependencies]
proc-macro2="1.0.7" // token stream 转换
syn={version="1.0.56", features=["full"]} // 生成它需要的 AST
quote="1" // token stream 转换

[lib]
proc-macro=true

异步

rust 的 future 都是惰性的。

async 其实只不过是一个语法糖:

async fn read_from_file() -> String {
  sleep(Duration::new(4, 0 ));
  String::from("test")
}

// 等价于:
fn read_from_file() -> impl Future<Output = String> {
  async {
    sleep(Duration::new(4, 0 ));
    String::from("test")
  }
}

Future 有一个 poll() 供 tokio 调用,返回值是 Pending 或 Ready(val) 的枚举。

如果没有准备好,那么会注册一个 waker,如果要通知 tokio 任务已经完成(比如监听到系统中断信号),那么就调用 waker 的唤醒方法,tokio 会再次调用 poll 方法。

异步涉及的crate:futures,tokio,async-std

use futures::executor::block_on

async fn hello() {
  println!("fdasf");
}

fn main() {
  let future = hello();
  block_on(future); // 这个时候才会执行。 此时线程会等待future完成。
}

// 使用 .await 可以让渡线程执行权

由于 .await 可能切换到其他线程执行,所以 async 体里面的变量必须能在线程间移动。

所以使用这些类型是不安全的:

  • Rc, &RefCell 和任何其他没有实现 Send trait 的类型,包括没实现 Sync trait 的引用。
  • 注意:调用 .await 时,只要这些类型不在作用域内,就可以使用它么

引用和指针

let e = &10;
assert(10, *e);

一个更复杂的例子:

let mut x = 10;
let ptr_x = &mut x as *mut i32;
let y = Box::new(20);
let ptr_y = &*y as *const i32;

unsafe{
  *ptr_x += *ptr_y;
}
assert_eq!(x, 30);

返回 dyn 的 trait

rust 编译器需要知道每个方法的返回值需要多大的空间,所以每个方法需要返回确定的类型。

这就限制了我们不能直接返回某个 trait,因为不同的实现,可能导致不同的内存大小需要。

struct Sheep {}
struct Cow {}

trait Animal {
    // Instance method signature
    fn noise(&self) -> &'static str;
}

// Implement the `Animal` trait for `Sheep`.
impl Animal for Sheep {
    fn noise(&self) -> &'static str {
        "baaaaah!"
    }
}

// Implement the `Animal` trait for `Cow`.
impl Animal for Cow {
    fn noise(&self) -> &'static str {
        "moooooo!"
    }
}

// Returns some struct that implements Animal, but we don't know which one at compile time.
fn random_animal(random_number: f64) -> Box<dyn Animal> {
    if random_number < 0.5 {
        Box::new(Sheep {})
    } else {
        Box::new(Cow {})
    }
}

fn main() {
    let random_number = 0.234;
    let animal = random_animal(random_number);
    println!("You've randomly chosen an animal, and it says {}", animal.noise());
}

一个很常用的case 就是,方法返回值为 Result<(), Box<dyn std::error::Error>>

很多时候,我们不想关心具体的异常信息,也不需要返回值。

函数或方法可能涉及多种不同的错误来源(如 IO、解析、自定义逻辑错误等),不希望为每种错误定义一个具体的枚举类型,尤其在某些初始化函数。使用 Box 可以将错误类型的管理推迟到运行时,而不是在编译时固定。

Mutex

type 别名

type Age = u32;
fn grow(age: Age, year: u32) -> Age {
  
}

type Double<T> = (T, Vec<T>);
那么 Double<i32> 就代表 (i32, Vec<i32>)

指针类型

Box<T> 指向类型T,有所有权,有权释放内存
&T 指向类型T的借用指针,也称为引用,无权释放内存,无权写数据
&mut T 指向类型T的借用指针,无权释放内存,有权写数据
*const T 指向类型T的只读裸指针,没有声明周期信息,无权写数据
*mut T 指向类型T的可读写裸指针,没有声明周期信息,有权写数据

类型转换

不允许隐式转换,如

let v1: i8 = 1;
let v2: i16 = v1;

需要使用 as

let v2: i16 = v1 as i16;

let i = 10;
let p = &i as *const i32 as *mut i32; // 多次转换