Tip #21
在 Rust 中,我们经常使用 Clone()
或 Copy()
。这两者之间的区别是什么?
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); 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
| trait Describable { fn describe(&self) -> String; } struct Person { name: String, age: u32, } 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}; 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); } let squares_b_it: impl Iterator<Item = u32> = (1..=100).map(|x: u32| x * x); 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"]; 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); let none_option: Option<u32> = None; let empty_string_option: Option<String> = none_option.map(|num| num.to_string()); println!("{:?}", empty_string_option); }
|
Tip #35
什么是 trait bound
? 当我们向带有泛型参数的函数中传递额外的trait名称,以便限制该泛型类型时,就是在谈论trait bound
:
1
| fn some_function<T: TraitA + TraitB>(param: T) { ... }
|
你可以使用 "+" 运算符来组合多个特质。这样一来,类型 T 就需要同时满足 TraitA
和 TraitB
这两个特质的要求。
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!
宏,从而达到打印输出的目的。这是声明式宏的一个基本应用,它们基于规则匹配并在编译时展开。