Rust Idioms 学习笔记
原文版本:
main@f279f35(2026-03-29 查验)
概述
Idioms 是 Rust 社区的编码规范和最佳实践,代表“地道的 Rust 写法“。除非有充分理由,否则应遵循这些习惯。
共 15 个小节
2.1 Use borrowed types for arguments
核心原则
函数只读取数据时用借用 (&T),需要存储/消耗时用所有权。
代码对比
#![allow(unused)]
fn main() {
// ❌ 获取所有权 - 调用者无法继续使用
fn print_name(name: String) {
println!("Name: {}", name);
}
let my_name = String::from("Alice");
print_name(my_name);
// println!("{}", my_name); // ❌ 编译错误
// ✅ 借用 - 调用者可继续使用
fn print_name(name: &str) {
println!("Name: {}", name);
}
let my_name = String::from("Alice");
print_name(&my_name);
println!("{}", my_name); // ✅ 可以继续使用
}
进阶:使用 AsRef 提高通用性
#![allow(unused)]
fn main() {
fn process_path<P: AsRef<std::path::Path>>(path: P) {
let path_ref = path.as_ref();
}
}
推荐:只读参数用 &T/&str;需要存储用所有权 T;通用 API 用 AsRef<T>
2.2 Concatenating Strings with format!
核心原则
优先使用 format! 宏进行字符串拼接和格式化。
代码对比
#![allow(unused)]
fn main() {
// ❌ 使用 + 操作符 - 需要 clone,不优雅
let s = "Hello".to_string() + ", " + &name;
// ✅ 使用 format! - 清晰简洁
let s = format!("Hello, {}!", name);
// ✅ 多行字符串 - indoc crate
use indoc::indoc;
let sql = indoc! {r#"
SELECT * FROM users
WHERE id = $1
"#};
}
推荐:字符串插值用 format!;多行文本用 indoc!
2.3 Constructor
核心原则
构造函数命名遵循约定,无参构造优先用 Default。
构造函数模式
| 模式 | 用法 | 适用场景 |
|---|---|---|
new() | T::new(args) | 标准构造函数 |
default() | T::default() | 使用 Default trait |
from() | T::from(other) | 类型转换(Into/From trait) |
with_xxx() | T::with_name(x) | 变体构造函数 |
builder() | T::builder().x().y().build() | 复杂对象构建 |
实践讨论
- 无参数 + 非 async → 不定义
new(),只实现Default::default()(Clippy 推荐) - 参数多 → 使用
boncrate 的 Builder 宏
#![allow(unused)]
fn main() {
// ✅ 无参构造 - 只用 Default
#[derive(Default)]
struct Config {
value: u32,
}
// ✅ 多参数 - bon crate
use bon::Builder;
#[derive(Builder)]
struct User {
name: String,
#[builder(default = 18)]
age: u32,
email: Option<String>,
}
}
2.4 The Default Trait
核心原则
优先 derive Default,需要自定义时手动实现。
实现方式
#![allow(unused)]
fn main() {
// ✅ 自动派生(字段都有 Default)
#[derive(Default)]
struct Config {
host: String, // ""
port: u16, // 0
}
// ✅ 手动实现(自定义默认值)
impl Default for Config {
fn default() -> Self {
Self {
host: String::from("localhost"),
port: 8080,
}
}
}
// ✅ 使用 ..Default::default()
let config = Config {
host: String::from("example.com"),
..Default::default()
};
}
推荐:字段都有 Default 用 #[derive(Default)];自定义默认值手动实现 Default
2.5 Collections Are Smart Pointers
核心原则
部分集合类型(Vec, String)实现了 Deref trait,像智能指针一样自动解引用到借用视图。
关键理解
#![allow(unused)]
fn main() {
// ✅ Vec 实现了 Deref<Target=[T]>,自动解引用到 slice
let v: Vec<i32> = vec![1, 2, 3];
let s: &[i32] = &v; // 自动 Deref: &Vec → &[T]
v.len(); // 实际调用的是 <[T]>::len()
// ✅ String 实现了 Deref<Target=str>,自动解引用到 &str
let s: String = String::from("hello");
let slice: &str = &s; // 自动 Deref: &String → &str
// ❌ HashMap 没有实现 Deref,不能自动解引用
use std::collections::HashMap;
let mut map: HashMap<String, i32> = HashMap::new();
// &map 仍然是 &HashMap,没有借用视图类型
}
类型对比
| 类型 | Deref 实现 | 借用视图 | 智能指针行为 |
|---|---|---|---|
Vec<T> | ✅ Deref<Target=[T]> | &[T] (slice) | ✅ 自动解引用 |
String | ✅ Deref<Target=str> | &str | ✅ 自动解引用 |
HashMap<K,V> | ❌ | 无 | ❌ 仅普通借用 |
Box<T> | ✅ Deref<Target=T> | &T | ✅ 智能指针 |
智能指针集合:Vec, String 实现 Deref,自动解引用到借用视图;非智能指针集合:HashMap, BTreeMap 等没有 Deref;API 设计:方法通常实现在借用视图上
2.6 Finalisation in Destructors
核心原则
利用 Drop trait 实现 RAII,自动清理资源。
RAII 模式
// ✅ 经典例子:文件自动关闭
struct FileHandle {
fd: i32,
}
impl Drop for FileHandle {
fn drop(&mut self) {
println!("Closing file descriptor {}", self.fd);
}
}
fn main() {
let file = FileHandle { fd: 42 };
} // ← drop() 自动调用
async 资源清理
#![allow(unused)]
fn main() {
// ✅ async 资源 - 显式 close 方法
struct AsyncResource {
conn: Connection,
}
impl AsyncResource {
async fn close(self) -> Result<()> {
self.conn.close().await
}
}
// 使用
resource.close().await?;
// 保护机制:测试 + Code Review
}
推荐:同步资源用 RAII + Drop;async 清理用显式 close() 方法;Drop 中不要 panic
2.7 mem::{take, replace} - 所有权交换
核心原则
不拷贝地替换值,实现所有权交换。
💡 比喻:像印第安纳琼斯用沙袋替换神器——拿走原物,留下替代品。
函数对比
| 函数 | 作用 | 等价于 |
|---|---|---|
mem::replace(&mut a, b) | 用 b 替换 a,返回旧 a | - |
mem::take(&mut a) | 拿走 a,留下 default() | replace(&mut a, Default::default()) |
Option::take() | 拿走 Option,留下 None | replace(&mut opt, None) |
常见场景
#![allow(unused)]
fn main() {
// 1. 状态机中拿走状态
let data = mem::take(&mut self.data);
// 2. Option 取值
let value = opt.take().unwrap_or_default();
// 3. 重新配置时替换
let old = mem::replace(&mut self.config, new_config);
}
进阶场景:Enum 变体切换
原文核心用例:在 enum 变体之间切换时,不 clone 地提取字段。
#![allow(unused)]
fn main() {
use std::mem;
enum MyEnum {
A { name: String, x: u8 },
B { name: String },
}
// A → B 变体切换,不 clone name
fn a_to_b(e: &mut MyEnum) {
if let MyEnum::A { name, x: 0 } = e {
*e = MyEnum::B {
name: mem::take(name), // 拿走 name,留下空 String
}
}
}
}
为什么需要:借用检查器不允许直接从 &mut 中拿走值(必须有东西占位),mem::take 用默认值占位,返回所有权。
关系图
mem::replace ← 最底层
↑
│
┌───┴───┐
│ │
mem::take Option::take()
所有权交换,避免 clone;常见场景:状态机、Option 取值、配置替换;enum 变体切换
2.8 On-Stack Dynamic Dispatch
核心原则
临时批量处理时用栈上借用,避免堆分配。
代码对比
#![allow(unused)]
fn main() {
// ❌ 堆上动态分发
let traits: Vec<Box<dyn Trait>> = vec![
Box::new(A),
Box::new(B),
];
// ✅ 栈上动态分发
let a = A;
let b = B;
let traits: [&dyn Trait; 2] = [&a, &b];
}
推荐:临时批量用 &[&dyn Trait] 栈上分发;需要所有权用 Box<dyn Trait>
2.9 Foreign function interface (FFI)
核心原则
安全地与 C 等语言交互。
关键要点
#![allow(unused)]
fn main() {
// 1. 字符串转换
use std::ffi::CString;
let rust_str = "hello";
let c_str = CString::new(rust_str).unwrap();
let c_ptr = c_str.as_ptr(); // *const i8
// 2. 错误处理
// C 的 errno → Rust 的 Result
}
字符串转换用 CString/CStr;错误处理包装为 Result
2.10 Iterating over an Option
核心原则
Option 可以当迭代器用。
代码示例
#![allow(unused)]
fn main() {
// ✅ Some 时执行一次,None 时不执行
for x in Some(5) {
println!("{}", x);
}
// ✅ 链式调用
let result: Vec<i32> = Some(5)
.into_iter()
.map(|x| x * 2)
.collect(); // [10]
}
Option<T> 实现 IntoIterator;Some 迭代一次,None 跳过;避免模式匹配嵌套
2.11 Pass Variables to Closure
核心原则
理解闭包捕获变量的方式。
捕获方式
#![allow(unused)]
fn main() {
// 1. 借用捕获(默认)
let x = 5;
let closure = || println!("{}", x);
// 2. move 捕获
let x = vec![1, 2, 3];
let closure = move || println!("{:?}", x);
// 3. 可变借用
let mut x = 5;
let mut closure = || x += 1;
}
借用捕获:只读访问;move 捕获:转移所有权(多线程、返回闭包);可变借用:修改捕获变量
2.12 Privacy For Extensibility
核心原则
用隐私控制实现可扩展的 API。
代码示例
#![allow(unused)]
fn main() {
pub struct Config {
inner: InnerConfig, // 私有字段
}
impl Config {
pub fn new() -> Self { } // 公开构造方法
}
}
私有字段 + 公共方法,保留未来修改内部实现的权利,避免锁定实现细节
2.13 Easy doc Initialization
核心原则
使用 ..Default::default() 简化初始化。
代码示例
#![allow(unused)]
fn main() {
let config = Config {
name: String::from("test"),
port: 8080,
..Default::default()
};
}
..Default::default() 填充未指定字段,只关注需要自定义的部分
2.14 Temporary mutability
核心原则
让可变性尽可能局部。
代码示例
#![allow(unused)]
fn main() {
let mut buffer = String::new();
buffer.push_str("hello");
buffer.push_str(" world");
let result = buffer; // 之后不再需要 mut
}
可变性限制在最小作用域,构建完成后转为不可变,减少借用冲突
2.15 Return consumed arg on error
核心原则
错误时返回原始输入,让调用者决定如何处理。
代码示例
#![allow(unused)]
fn main() {
fn parse(s: String) -> Result<i32, String> {
s.parse().map_err(|_| s) // 返回原字符串
}
}
错误时返回原始输入,调用者可重试或记录,而非直接丢弃
本部分学习总结
核心收获
- 借用优先 - 参数用
&T,避免不必要的所有权转移 - format! 宏 - 字符串拼接首选
- Default trait - 无参构造的最佳实践
- Move 语义 - 集合类型赋值会转移所有权
- RAII - 同步资源用 Drop,async 资源用显式 close
- mem 工具 -
replace/take用于所有权交换 - 栈上分发 - 临时批量处理避免堆分配
最佳实践
| 场景 | 推荐做法 |
|---|---|
| 函数参数 | &T 或 AsRef<T> |
| 字符串 | format! / indoc |
| 构造 | new() / Default / bon |
| 资源清理 | RAII / 显式 close() |
| 集合传参 | &Vec / &HashMap |
常见陷阱
- ❌ 忘记集合是 Move 语义
- ❌ async 资源在 Drop 中无法清理
- ❌ 返回栈上的 trait object 引用