Rust learning:from germ to grave

Rust官方推荐的三个资料,分别是The Rust programming language,Rust by examples以及ruslings,已经相当充足了.包括相对全面的书,代码例子以及方便的互动式exercises.个人觉得,the book相当于字典,虽然其实还有内容更多的reference,而examples更加易懂上手,rustlings相当于刷题,把关键东西了解一遍.

所以从这三个东西入手开始Rust学习之旅,一些地方会跟c++对比.

宏macro

术语宏指的是Rust中的一系列特性:带有macro_rules的声明性宏!还有三种过程宏:

  • Custom #[derive] macros that specify code added with the derive attribute used on structs and enums
  • Attribute-like macros that define custom attributes usable on any item
  • Function-like macros that look like function calls but operate on the tokens specified as their argument
1
2
3
4
5
6
7
8
9
10
11
12
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}

变量binding

知识点:

  1. 不变性 Rust默认不可变,不像c++到处声明const
  2. scope和shadowing 主要有variable shadowing,也就是可以重声明,在c++中不允许
  3. 将一个mut的值赋值给non-mut的值,在那个域内,non-mute的值也不能被改变
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
27
fn main() {
let mut x = 3;
println!("Number {}", x);
x = 5;
println!("Number {}", x);
}

fn main() {
let number = "T-H-R-E-E"; // don't change this line
println!("Spell a Number : {}", number);
let number = 3; // don't rename this variable
println!("Number plus two is : {}", number + 2);
}
fn main() {
let mut _mutable_integer = 7i32;

{
// Shadowing by immutable `_mutable_integer`
let mut _mutable_integer = _mutable_integer;

_mutable_integer = 50;

// `_mutable_integer` goes out of scope
}

// Ok! `_mutable_integer` is not frozen in this scope
_mutable_integer = 3;

Primitives

字面量和操作符

  1. 推荐在使用字面量是后面加上类型.
  2. 原生类型本身可以printable

元组、数组与slices.

元组,通过()表示,通过.num索引,可以使用#[derive(Debug)]实现方法打印.

数组 [T;length]声明,编译时已知.

切片(Slices)与数组类似,但它们的长度在编译时是未知的。相反,切片是一个由两个字(word)组成的对象:第一个字是指向数据的指针,第二个字是切片的长度。字的大小与 usize 类型相同,由处理器架构决定,例如在 x86-64 上为 64 位。切片可用于借用数组的一部分,它的类型签名为 &[T]

1
2
3
4
5
6
7
8
9
10
11
// This function borrows a slice.
fn analyze_slice(slice: &[i32]) {
println!("First element of the slice: {}", slice[0]);
println!("The slice has {} elements", slice.len());
}

let xs: [i32; 5] = [1, 2, 3, 4, 5];
// All elements can be initialized to the same value.
let ys: [i32; 500] = [0; 500];
println!("Borrow the whole array as a slice.");
analyze_slice(&xs);

自定义类型

structures

使用 struct 关键字可以创建三种类型的结构体(struct):

  1. 元组结构体(Tuple structs),基本上是命名元组。
  2. 经典的 C 风格结构体。
  3. 无字段的单元结构体(Unit structs),在泛型编程中很有用。
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
#[derive(Debug)]
struct Person {
name: String,
age: u8,
}
// A unit struct
struct Unit;

// A tuple struct
struct Pair(i32, f32);

// A struct with two fields
struct Point {
x: f32,
y: f32,
}
// A tuple struct
struct Pair(i32, f32);
// Instantiate a tuple struct
let pair = Pair(1, 0.1);

// Access the fields of a tuple struct
println!("pair contains {:?} and {:?}", pair.0, pair.1);

// Destructure a tuple struct
let Pair(integer, decimal) = pair;

Enums

enum关键字允许创建一个可以是几种不同变体(variant)之一的类型。任何在结构体(struct)中有效的变体,在枚举(enum)中也是有效的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum IpAddrKind {
V4,
V6,
}

struct IpAddr {
kind: IpAddrKind,
address: String,
}

let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};

constants

Rust 有两种不同类型的常量,可以在任何作用域(包括全局)中声明。两种常量都需要显式的类型注解:

  1. const: 不可变的值(最常见的情况)。
  2. static: 可能是可变的变量,拥有 'static 生命周期。'static 生命周期是被推断出来的,不需要显式指定。访问或修改可变的 static 变量是不安全的

工具

强大的包管理系统

一个rust项目可能是lib也可以是main,分别代表生成库和可执行程序. 注意可以既存在main.rs和lib.rs.

  • Packages: A Cargo feature that lets you build, test, and share crates
  • Crates: A tree of modules that produces a library or executable
  • Modules and use: Let you control the organization, scope, and privacy of paths
  • Paths: A way of naming an item, such as a struct, function, or module

crate根文件是Rust编译器启动的源文件,它构成了crate的根模块

包(Packages)是提供一组功能的一个或多个crate的集合。一个包包含一个Cargo.toml。描述如何构建这些crate的Toml文件。Cargo实际上是一个包,其中包含用于构建代码的命令行工具的二进制crate。Cargo包还包含一个二进制包所依赖的库包。其他项目可以依赖Cargo库crate来使用Cargo命令行工具使用的相同逻辑。

crate有两种形式:二进制(binary)crate或库(library )crate。二进制crate是可以编译为可运行的可执行文件的程序,例如命令行程序或服务器。每个程序都必须有一个名为main的函数,用于定义可执行程序运行时发生的情况。

library crate没有main函数,也不能编译成可执行文件。相反,它们定义了旨在与多个项目共享的功能。

项目结构
  1. 从crate根目录开始:当编译一个crate时,编译器首先查找crate根文件(通常是src/lib.rs,为库crate或src/main.rs表示二进制文件)用于编译代码。

  2. 声明模块:在crate根文件中,你可以声明新的模块;假设你用mod garden;声明了一个“花园”模块。编译器将在这些地方查找模块的代码:

    内联下载mod garden之后

    在src/garden.rs文件中

    在src/garden/mod.rs文件中 查找模块的规律就是同级目录同名文件或者同名子目录中的mod.rs文件

  3. 声明子模块:在crate根目录之外的任何文件中(除了src/main/lib.rs的文件中),都可以声明子模块。例如,你可以声明mod vegetables;在src/garden.rs。编译器将在以下地方以父模块命名的目录中查找子模块的代码:

    内联,直接跟在mod vegetables后面

    在src/garden/vegetables.rs文件中

    在src/garden/vegetables/mod.rs文件中 查找子模块的规律就是与文件同名子目录中的同名模块文件或者同名模块目录中的mod.rs文件

  4. 模块中的代码路径:一旦模块成为crate的一部分,只要隐私规则允许,就可以使用代码路径从同一crate中的任何其他地方引用该模块中的代码。例如,garden vegetables模块中的Asparagus类型可以在crate::garden::vegetables::Asparagus中找到。

  5. 私有vs.公共:默认情况下,模块内的代码对其父模块是私有的。要使一个模块为公共,请使用pub mod而不是mod声明它。要使公共模块中的项也为公共,请在声明它们之前使用pub。

  6. use关键字:在作用域中,use关键字创建项的快捷方式,以减少长路径的重复。在任何可以引用crate::garden::vegetables::Asparagus的作用域中,你可以使用crate::garden::vegetables::Asparagus创建一个快捷方式;从那时起,你只需要编写Asparagus就可以在作用域中使用该类型。

    ​ 注意,use是用于减少名称重复的,pub mod才是用于引入的.

    使用mod组织代码结构,提到src/main.rs和src/lib.rs被称为crate roots.命名的原因是这两个文件的内容在crate的模块结构(称为模块树)的根位置形成了一个名为crate的模块

    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
    27
    mod front_of_house {
    mod hosting {
    fn add_to_waitlist() {}

    fn seat_at_table() {}
    }

    mod serving {
    fn take_order() {}

    fn serve_order() {}

    fn take_payment() {}
    }
    }
    /// 模块树,注意
    """
    crate
    └── front_of_house
    ├── hosting
    │ ├── add_to_waitlist
    │ └── seat_at_table
    └── serving
    ├── take_order
    ├── serve_order
    └── take_payment
    """

    Path

    在模块树中如何找到一个item

    绝对路径是从crate根目录开始的完整路径;对于来自外部crate的代码,绝对路径以crate名称开头,而对于来自当前crate的代码,它以文字crate开头。

    相对路径从当前模块开始,使用当前模块中的self、super或标识符。

父模块中的项不能使用子模块中的私有项,但子模块中的项可以使用其祖先模块中的项。这是因为子模块封装并隐藏了它们的实现细节,但是子模块可以看到它们被定义的上下文。

sibling之间可以调用模块,比如同在crate下的函数和模块,这个函数可以直接调用模块而不使用pub.

相对路径

使用super,self等引入.

1
2
3
4
5
6
7
8
9
10
fn deliver_order() {}

mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}

fn cook_order() {}
}

注意使得模块中的enum,structs,函数等pub才能访问.

一个包可以同时包含src/main。Rs二进制crate根以及src/lib。默认情况下,两个crate都有包名。通常,具有这种既包含库又包含二进制crate模式的包在二进制crate中会有足够的代码来启动调用库crate中的代码的可执行文件。这使得其他项目可以从包提供的大部分功能中受益,因为库crate的代码可以共享

使用use将paths引入作用域,要使用的话就要在同一mod作用域中.

1
2
3
4
5
6
7
8
9
10
11
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}

使用use将函数的父模块带入作用域意味着我们必须在调用函数时指定父模块。在调用函数时指定父模块可以清楚地表明该函数不是本地定义的,同时仍然可以最大限度地减少完整路径的重复。此外在使用struct、enum和其他项时,习惯上指定完整路径。

对于将两个具有相同名称的类型带入相同作用域的问题,除了引入父模块,还有另一种解决方案:在路径之后,可以为类型指定as和一个新的本地名称或别名。

1
2
3
4
5
6
7
8
9
10
use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
// --snip--
}

fn function2() -> IoResult<()> {
// --snip--
}

使用use关键字将名称引入作用域时,在新作用域中可用的名称是private。为了使调用代码的代码能够引用该名称,就好像它是在该代码的作用域中定义的一样,可以组合pub和use。这种技术被称为再导出

1
2
3
4
5
6
7
8
9
10
11
12
// restaurant  src/lib.rs
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();
}

在更改之前,外部代码必须通过restaurant::front_of_house::hosting::add_to_waitlist()来调用add_to_waitlist函数,这也需要将front_of_house模块标记为pub.现在外部代码可以使用restaurant::hosting::add_to_waitlist()代替。

标准std库也是一个包外部的crate。因为标准库是随Rust语言一起提供的,所以不需要更改Cargo.toml。但是需要用use来引用它,以便将其中的项引入包的作用域。

推荐模块命名不要为mod.rs,使用名为mod.rs文件的风格的主要缺点是项目最终可能会有许多名为mod.rs的文件,当同时在编辑器中打开它们时,这可能会令人困惑。

使用Cargo构建大项目

Release

在debug和release构建时可以使用不同的选项,在Cargo.toml

1
2
3
4
5
[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

Cargo有两个主要配置profiles:运行Cargo构建时使用的开发配置文件和运行Cargo构建—发布时使用的发布配置文件。运行cargo build --release使用release profile.

当没有显式地添加任何profiles时,Cargo对应用的每个profiles都有默认设置。通过添加[profile.]*]部分,自定义的任何配置文件,覆盖任何子集的默认设置。

option-level设置控制Rust将应用于代码的优化数量,范围为0到3。应用更多的优化会延长编译时间,因此,如果您正在开发并经常编译代码,那么您可能希望通过更少的优化来加快编译速度,即使结果代码运行得更慢。

因此dev的默认选项级别为0。准备发布代码时,最好花更多的时间进行编译。将只在发布模式下编译一次,但是将多次运行编译后的程序,因此发布模式以较长的编译时间换取运行速度更快的代码

使用cargo publish进行发布,注意需要注册crates.io账号,如果需要更新版本更改version再推送即可

1
2
3
4
5
6
7
8
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"

[dependencies]

使用cargo yank --vers [versionNumber]可以阻止使用这个项目的某个版本的依赖.

yank一个版本可以防止新项目依赖于该版本,同时允许所有依赖于该版本的现有项目继续进行。

workspaces

工作区是一组共享Cargo.lock,和输出目录的packages. 常见工作流是一个可执行文件的packages和多个生成库的packages.

在根目录下创建Cargo.toml,其中添加packages名字,假设根目录名字add

1
2
3
4
5
[workspace]

members = [
"adder",
]
1
cargo new adder

目录结构如下

1
2
3
4
5
6
7
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target

工作区在顶层有一个目标目录,编译后的工件将被放置到该目录中;adder包没有自己的目标目录。即使要从adder目录中运行cargo构建,编译后的工件仍然会在add/target而不是add/adder/target中结束。Cargo在工作区的目标目录中采用这样的结构,因为工作区的crate是相互依赖的。

如果每个crate都有自己的目标目录,那么每个crate都必须重新编译工作空间中的其他crate,以便将工件放置在自己的目标目录中。通过共享一个目标目录,crate可以避免不必要的重新构建。

1
cargo new add_one --lib

继续添加crate,加入workspace中.

1
2
3
4
5
[workspace]
members = [
"adder",
"add_one",
]

添加本地项目中的依赖

1
2
3
// adder/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }

在顶层目录运行cargo build,然后指定-p运行指定可执行程序cargo run -p adder

如果要使workspace下两个crate使用同一版本同一个依赖,可以使得其中一个crate依赖另一个依赖,build之后在顶层cargo.lock中会有相关依赖信息.

Cargo将确保工作空间中使用rand包的每个包中的每个crate都使用相同的版本,只要它们指定rand的兼容版本,就可以节省空间并确保工作空间中的crate彼此兼容

workspace中的项目测试,在顶层目录中cargo test运行每个crate中的测试,使用cargo test -p指定,发布项目同理,cargo publish -p.

cargo install命令在本地安装和使用二进制crate.

所有使用cargo install安装的二进制文件都存储在安装根目录的bin文件夹中。如果没有任何自定义配置,这个目录将是$HOME/.cargo/bin

如果$PATH中的二进制文件名为cargo-something,则可以通过运行Cargo something来将其作为Cargo子命令运行。在运行cargo——list时会列出这样的自定义命令。

cargo fmt/clippy

分别用于格式化和语法提示

cargo doc

非常方便的生成文档的工具

1
cargo doc --no-deps --open

test

单元测试 集成测试 benchmarks

1
cargo test
1
2
3
4
5
6
7
8
9
#[cfg(test)]
mod test{
use super::*;
#[test]
func(){
assert_eq!();
}

}

此外也有doc-test,用于测试文档中的代码.尝试使用criterionCriterion.rs - Criterion.rs Documentation (bheisler.github.io)进行benchmark测试.

log

1
2
3
4
5
6
use log::{error,warn,info,debug,trace};
error!();
warn!();
info!();
debug!();
trace!();

dyn

Rust 编译器需要知道每个函数的返回类型需要多少空间。这意味着所有函数都必须返回一个具体类型。与其他语言不同,如果你有个像 Animal 那样的的 trait,则不能编写返回 Animal 的函数,因为其不同的实现将需要不同的内存量。

但是,有一个简单的解决方法。相比于直接返回一个 trait 对象,我们的函数返回一个包含一些 AnimalBox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
trait walk {
fn xx();
}
struct a {

}
impl walk for a {
fn xx(){}
}
struct b {

}
impl walk for b {
fn xx(){}
}
fn main() {
let t = vec![Box::new(a{]}),Box::new(b{})]; // Box<dyn walk>
}

box 只是对堆中某些内存的引用。因为引用的大小是静态已知的,并且编译器可以保证引用指向已分配的堆 Animal,所以可以从函数中返回 trait.

每当在堆上分配内存时,Rust 都会尝试尽可能明确。因此,如果函数以这种方式返回指向堆的 trait 指针,则需要使用 dyn 关键字编写返回类型

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
27
28
29
30
31
32
33
34
35
36
struct Sheep {}
struct Cow {}

trait Animal {
// 实例方法签名
fn noise(&self) -> &'static str;
}

// 实现 `Sheep` 的 `Animal` trait。
impl Animal for Sheep {
fn noise(&self) -> &'static str {
"baaaaah!"
}
}

// 实现 `Cow` 的 `Animal` trait。
impl Animal for Cow {
fn noise(&self) -> &'static str {
"moooooo!"
}
}

// 返回一些实现 Animal 的结构体,但是在编译时我们不知道哪个结构体。
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());
}

智能指针

与C++类似,rust也使用了一套机制保证内存安全. 智能指针相比于借用(引用)来说,其在大多数情况下拥有所有权.

常用智能指针类型如下

  • Box<T> for allocating values on the heap
  • Rc<T>, a reference counting type that enables multiple ownership
  • Ref<T> and RefMut<T>, accessed through RefCell<T>, a type that enforces the borrowing rules at runtime instead of compile time

  • RefCell:智能指针,允许运行时动态获取可变引用,跟踪借用以保证安全性

  • Arc:线程安全的reference counting type

此外,有Ref:用于在不可变借用的情况下安全地访问数据和Cell分别用来安全访问数据.

Box

1
2
3
4
5
6
7
8
9
10
enum List {
Cons(i32, Box<List>),
Nil,
}

use crate::List::{Cons, Nil};

fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

Rc

你必须通过使用Rust类型Rc\显式地启用多重所有权,Rc\是引用计数的缩写。Rc\类型跟踪对一个值的引用次数,以确定该值是否仍在使用。如果对一个值的引用为零,则可以清除该值,而不会导致任何引用无效。

想要在堆上分配一些数据供程序的多个部分读取,并且在编译时无法确定哪个部分将最后使用该数据时,使用Rc\类型。如果知道哪一部分将最后完成,就可以将该部分设置为数据的所有者,并且在编译时强制执行的正常所有权规则将生效。

注意,Rc\仅用于单线程场景

1
2
3
4
5
6
7
8
9
10
11
12
13
enum List {
Cons(i32, Rc<List>),
Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
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));
}

Rust中,Cell 是标准库中的一个类型,它提供了一种在可变引用的限制下安全地更新数据的方法。Cell 是一个非线程安全的类型,主要用于单线程环境下的可变状态管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::cell::Cell;

fn main() {
// 创建一个 Cell
let count = Cell::new(0);

// 使用 get() 获取值
println!("Initial value: {}", count.get());

// 使用 set() 修改值
count.set(10);
println!("New value: {}", count.get());
}

与Rc\不同,RefCell\类型表示它所持有的数据的单一所有权.

通过引用和Box\,借用规则的不变量在编译时强制执行。使用RefCell\,这些不变量在运行时强制执行。对于引用,如果违反了这些规则,就会出现编译器错误。对于RefCell\,如果你违反了这些规则,你的程序就会panic并退出。

在编译时检查借用规则的优点是,在开发过程中可以更快地捕获错误,并且不会对运行时性能产生影响,因为所有的分析都是事先完成的。由于这些原因,在大多数情况下,在编译时检查借用规则是最好的选择,这就是为什么这是Rust的默认值。

在运行时检查借阅规则的优点是,在编译时检查不允许的情况下,允许某些内存安全的场景。静态分析和Rust编译器一样,本质上是保守的。代码的一些属性是不可能通过分析代码来检测的

与Rc\类似,RefCell\仅用于单线程场景,如果您尝试在多线程上下文中使用它,则会给您一个编译时错误。(尝试使用Mutex以及Arc)

Arc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();

*num += 1;
});
handles.push(handle);
}

for handle in handles {
handle.join().unwrap();
}

println!("Result: {}", *counter.lock().unwrap());
}

Weak

同c++中的weak_ptr,避免循环引用.

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
27
28
29
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});

println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});

*leaf.parent.borrow_mut() = Rc::downgrade(&branch);

println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

RefCell

Rc\允许同一数据的多个所有者;Box\和RefCell\具有单个所有者。

Box\允许在编译时检查不可变或可变借用(意味着只有单一读取和修改者,同时也是owner);Rc\只允许在编译时不可变借用(相当于可以有多个可读,不能更改值,可以考虑搭配RefCell修改);

RefCell\允许在运行时检查不可变或可变的借用。因为RefCell\允许在运行时检查可变借用,所以即使RefCell\是不可变的,你也可以改变RefCell\中的值。

内部可变性是Rust中的一种设计模式,它允许你改变数据,即使数据有不可变的引用;

这就是使用RefCell的目的.

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
27
28
29
30
31
32
33
34
35
36
37
38
pub trait Messenger {
fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}

pub fn set_value(&mut self, value: usize) {
self.value = value;

let percentage_of_max = self.value as f64 / self.max as f64;

if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
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
27
28
29
30
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;

struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}

impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}

impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}

#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--

assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}

使用RefCell\的常见方法是与Rc\结合使用。回想一下,Rc\允许您拥有某些数据的多个所有者,但它只提供对该数据的不可变访问。如果你有一个Rc\持有一个RefCell\,可以得到一个值,可以有多个所有者,你可以改变

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
// 在有多个owner情况下修改数据 使用Rc和RefCell
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
let value = Rc::new(RefCell::new(5));

let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

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

*value.borrow_mut() += 10;

println!("a after = {a:?}");
println!("b after = {b:?}");
println!("c after = {c:?}");
}

总结一下,没有特别情况可以使用Box,类似于c++中unique_ptr,Rc类似于shared_ptr.

Arc和Mutex用于多线程情况. RefCell本身是运行时改变借用可变性,在一些情况下可以使用.

Trait 对比Concept in C++20

Rust中trait与泛型结合很好,同时由于Rust没有类的继承,可以考虑使用泛型继承和组合实现类似效果. 可以使用:以及where和+搭配可以对trait的继承以及对泛型的限制进行描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pub train walk {
fn xxx();
fn yy(){println!("")};
fn zzz();
}
pub trait run:walk {
fn ttt();
}
fn func(s:dyn run){}
// 限制结构的泛型
pub struct Hi<T:run> {
T:value
}
// 实现结构的trait
impl walk for Hi<T> where T:walk {

}

此外trait也可以写泛型(这在c++中往往是常见行为). trait在继承时使用:+,在泛型使用时使用:where,+.

在c++中concept更偏向于限制泛型,而rust中trait还有接口的含义(通过实现接口而不是继承满足要求).

1
2
template <typename T>
concept integral = std::is_integral<T>::value;

声明concept如上,此外可以使用&&搭配,还可以使用requires

1
2
template <typename T>
concept Addable = requires(T a, T b) { a + b; }; // a + b 可通过编译即可
  1. requires { requirement-seq }
  2. requires ( parameter-list(optional) ) { requirement-seq }

requirements-seq 可以是:简单要求、类型要求、复合要求、嵌套要求.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <class T>
concept Check = requires(T a, T b) {
{ a.clear() } noexcept; // 支持clear,且不抛异常
{ a + b } noexcept->std::same_as<int>; // std::same_as<decltype((a + b)), int>
};
template <typename T>
concept C =
requires(T x) {
{*x}; // *x有意义
{ x + 1 } -> std::same_as<int>; // x + 1有意义且std::same_as<decltype((x + 1)), int>,即x+1是int类型
{ x * 1 } -> std::convertible_to<T>; // x * 1 有意义且std::convertible_to< decltype((x *1),T>,即x*1可转变为T类型
};

template <class T>
concept Check = requires(T a, T b) {
requires std::same_as<decltype((a + b)), int>;
};
// =>
template <class T>
concept Check = requires(T a, T b) {
{ a + b } -> std::same_as<int>;
};

而使用concept如下,使用和定义concept时都可以使用requires和&&.

1
2
3
4
5
6
7
8
template <typename T>
requires integral<T>
T inc(T a) { return ++a; } // 个人推荐
template <typename T>
T inc(T a) requires integral<T> { return ++a; } //
template <integral T>
T inc(T& a) { return ++a; } //
integral auto inc(integral auto a) { return ++a; } // 泛型函数 使用concept限制

后记

作为偏底层的编程语言,c/c++,rust,zig等目前都还在发展,即使c++已过五十年,但C++2a中Concepts,Modules,Coroutines(协程)等新特性都不断出现(虽然在其他语言中早就有了),所以还是地位仍在的.而后两者在前端工具构建上均大显身手,期待后续发展.

我也很喜欢使用C/C++,Go,Rust等写一些小程序demo.

FYI

一些语言高级特性

  1. 谈元编程与表达能力 - 面向信仰编程 (draveness.me)
  2. 从泛型 (Generics) 到元编程 (Metaprogramming) (tsinghua.edu.cn)

image-20240629202803323

image-20240629202943879

-------------本文结束感谢您的阅读-------------
感谢阅读.

欢迎关注我的其它发布渠道