Rust tips #21 ~ #40

Tip #21

在 Rust 中,我们经常使用 Clone()Copy()。这两者之间的区别是什么?

  • Copy:支持 Copy 的类型可以安全地通过字节复制的方式进行复制,可以类比 C 语言中的 memcpy 函数。

  • Clone:支持 Clone 的类型也可以被复制,但它通常需要执行一些逻辑操作来完成深拷贝。

Tip #22

我之前忽略的一点是,Rust 中有 static 变量,可以用来追踪某些状态。当然, 可变的静态变量 (mutable static)是不支持的,但对于原始类型,可以考虑使用std::sync::atomic。这些可以被实例化为静态的,并且在后续可以被修改:

1
2
3
4
5
6
7
8
9
10
static COUNTER: AtomicUsize = AtomicUsize::new(0);
// Functions to get and increment *static* counter.
fn increment_counter() {
COUNTER.fetch_add(1, order: Ordering::Relaxed);
}
fn get_counter() -> usize {
COUNTER.load(order: Ordering::Relaxed)
}

Tip #23

对于大多数使用核心数据类型的结构体,你可以通过派生 Default trait自动生成一个基本的Default()实现:

1
2
[derive(Default)]
struct MyStruct { ... }

Tip #24

探索智能指针:

  • Box<T> 用于独占所有权,一旦它所在的作用域 {} 结束,它就会被释放。
  • Rc<T> 是一种引用计数的智能指针。只有当它的所有引用都不存在时,它才会被释放。
  • Arc<T>Rc<T> 的线程安全版本。

Tip #25

在Rust中,trait的工作方式类似于其他语言中的接口定义。实现某个trait的结构体或枚举,在契约上必须提供trait中指定签名的函数 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Define a trait `Describable`
trait Describable {
fn describe(&self) -> String;
}
struct Person {
name: String,
age: u32,
}
// Implement the `Describable` trait for `Person`
impl Describable for Person {
fn describe(&self) -> String {
format!("{} is {} years old.", self.name, self.age)
}
}

Tip #26

你知道Rust支持对大多数常见数据类型进行解构吗?这里有一个关于结构体的例子:

1
2
3
4
5
6
7
8
9
10
11
struct Rocket {
name: String,
num_engines: u32,
}
let falcon9: Rocket = Rocket{name: "Falcon 9".to_string(), num_engines: 9};
// Destructure name and engines
let Rocket {name: the_name: String, num_engines: the_engines: u32} = falcon9;
println!("Rocket name {}, num engines {}", the_name, the_engines);

这种解构方式允许你在一行中从结构体中提取多个字段,并给它们起新的名字或指定类型,这在处理复杂数据时非常有用。

Tip #27

Rust #区间表达式:

  • 包含区间(包含a到b,b也包括在内):a..=b
  • 半开区间(包含a到b-1):a..b
  • 从a开始:a..
  • 到b-1为止:..b
  • 到b为止(包括b):..=b
  • 完整区间:..

Tip #28

区间表达式(继续):
区间表达式可以应用于for循环,或用于创建迭代器。别忘了调用collect()来实际执行迭代器:

1
2
3
4
5
6
7
8
9
10
11
let mut squares_a: Vec<u32> = vec![];
for i: u32 in 1..=100 {
squares_a.push(i * i);
}
// Map creates an iterator, but does not perform the computation.
let squares_b_it: impl Iterator<Item = u32> = (1..=100).map(|x: u32| x * x);
// Apply collect to "run" the iterator.
let squares_b: Vec<u32> = (1..=100).map(|x: u32| x * x).collect();

Tip #29

迭代器可以通过 chain() 方法进行连续拼接。Rust 在处理可能含有或不含值的 Option 类型的连续操作时表现得尤为优雅。

1
2
3
4
5
6
7
let maybe_rocket = Some("Starship");
let rockets = vec!["falcon1", "falcon2"];
// Chain the two iterators together.
for i in rockets.iter().chain(maybe_rocket) {
println!("🚀 {}", i);
}

Tip #30

如果需要以非可变方式将向量(vector)传递给函数,你可以使用 &[T](等同于 &Vec<T>)类型的参数,这也就是所谓的 切片slice)。

切片的优势包括:它们避免了所有权的转移,并且对于 #并发或 #并行操作是安全的。

Tip #31

动态调度(dynamic dispatch)简单来说,是在程序运行时动态地处理不同类型的特性,通过一个公共的特质(trait)来实现,从而使得(具有 Rust 特色的)多态成为可能。

在 Rust 中,Box<dyn Trait> 通常表明使用了动态调度。

Tip #32

迭代器提供了一些非常方便的实用功能。其中之一是 all() 方法,它会检查迭代器中所有元素是否都满足给定的条件。

这使我们能够以优雅且符合习惯用法的方式重写难看的基于for循环的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 难看的代码
fn check_user_infos(user_infos: Vec<UserInfo>) -> bool {
for u in user_infos.iter() {
if !unique_tags.contains(&u.user_name.as_str()) {
return false;
}
}
true
}
// 优雅的代码
fn check_user_infos(user_infos: Vec<UserInfo>) -> bool {
user_infos.iter().all(|u| unique_tags.contains(&u.user_name.as_str()))
}

Tip #33

let a: Arc<Mutex<Vec<f32>>> 这样的声明在视觉上是否让你觉得困扰?这时可以使用 type关键字来定义类型 别名(alias):

1
type SharedSafeVec<T> = Arc<Mutex<Vec<T>>>

这样,你就可以使用 SharedSafeVec<f32> 来代替 Arc<Mutex<Vec<f32>>>

Tip #34

Option<T>.map() 是一种将选项Option)从一种类型转换为另一种类型的极佳方式。它能透明地处理 None 值的情况。

请看以下示例,我们将 Option<u32> 转换为 Option<String>

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let number_option: Option<u32> = Some(42);
let string_option: Option<String> = number_option.map(|num| num.to_string());
println!("{:?}", string_option); // 输出: Some("42")
let none_option: Option<u32> = None;
let empty_string_option: Option<String> = none_option.map(|num| num.to_string());
println!("{:?}", empty_string_option); // 输出: None
}

Tip #35

什么是 trait bound? 当我们向带有泛型参数的函数中传递额外的trait名称,以便限制该泛型类型时,就是在谈论trait bound

1
fn some_function<T: TraitA + TraitB>(param: T) { ... }

你可以使用 "+" 运算符来组合多个特质。这样一来,类型 T 就需要同时满足 TraitATraitB 这两个特质的要求。

Tip #36

如需从应用程序获取更详细的日志输出,尝试导出环境变量 RUST_LOG={error, warn, info, debug, trace}

以下是一个使用 actix-web 运行的服务在 trace 模式下的示例,它会提供超级详尽的日志输出:
![[Pasted image 20240608183308.png]]

Tip #37

元组结构体对于封装值并附加可通过 Rust 的类型系统验证的元数据非常有用。

元组结构体的一个妙用是模拟计量单位——这样就不会再混淆英尺和米了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Feet(i32); // 定义一个元组结构体,表示英尺
struct Meters(i32); // 定义另一个元组结构体,表示米
impl From<Feet> for Meter {
fn from(feet: Feet) -> Self {
Meter(feet.0 * 0.3048) // 将英尺转换为米
}
}
impl From<Meters> for Feet {
fn from(meters: Meters) -> Self {
Feet(meters.0 / 0.3048) // 将米转换为英尺
}
}
fn is_longer_enough(meters: Meters) -> bool {
meters.0 > 1
}
fn is_longer_enough(feet: Feet) -> bool {
feet.0 > 3
}

Tip #38

正在编写一个函数但还没准备好最终完成?可以使用 todo!()unimplemented!() 宏来让代码保持可编译状态。但要记住,如果你的程序运行时遇到这些点,它将会panic!这对于开发阶段非常理想。🚧

和第四条重复了

Tip #39

30秒速成指南:构建 Rust #模块

创建你的模块结构:

1
2
3
4
my_module/
│ ├── mod.rs
│ ├── component_1.rs
│ └── component_2.rs

在 mod.rs 中添加:

1
2
mod component_1;
mod component_2;

或者新的方式:

1
2
3
4
my_module/
├── component_1.rs
└── component_2.rs
my_module.rs

🌟 小贴士:使用pub 关键字来定义公有访问权限。

Tip #40

实际上,Rust 中有两种类型的声明式宏(declarative)和更高级的 过程式宏(procedural)。下面是一个使用 macro_rules! 宏来生成 println 功能的声明式宏示例:

1
2
3
4
5
6
7
8
9
macro_rules! my_println {
($($arg:tt)*) => {
println!($($arg)*);
};
}
fn main() {
my_println!("Hello, Rust!");
}

这段代码定义了一个名为 my_println! 的宏,它接收任意数量的参数并简单地将它们传递给标准库的 println! 宏,从而达到打印输出的目的。这是声明式宏的一个基本应用,它们基于规则匹配并在编译时展开。