Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust Idioms 学习笔记

来源:Rust Unofficial Patterns

原文版本: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 推荐)
  • 参数多 → 使用 bon crate 的 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)✅ 自动解引用
StringDeref<Target=str>&str✅ 自动解引用
HashMap<K,V>❌ 仅普通借用
Box<T>Deref<Target=T>&T✅ 智能指针

智能指针集合Vec, String 实现 Deref,自动解引用到借用视图;非智能指针集合HashMap, BTreeMap 等没有 DerefAPI 设计:方法通常实现在借用视图上


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,留下 Nonereplace(&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()
};
}

详见 2.4 The Default Trait

..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)  // 返回原字符串
}
}

错误时返回原始输入,调用者可重试或记录,而非直接丢弃


本部分学习总结

核心收获

  1. 借用优先 - 参数用 &T,避免不必要的所有权转移
  2. format! 宏 - 字符串拼接首选
  3. Default trait - 无参构造的最佳实践
  4. Move 语义 - 集合类型赋值会转移所有权
  5. RAII - 同步资源用 Drop,async 资源用显式 close
  6. mem 工具 - replace/take 用于所有权交换
  7. 栈上分发 - 临时批量处理避免堆分配

最佳实践

场景推荐做法
函数参数&TAsRef<T>
字符串format! / indoc
构造new() / Default / bon
资源清理RAII / 显式 close()
集合传参&Vec / &HashMap

常见陷阱

  • ❌ 忘记集合是 Move 语义
  • ❌ async 资源在 Drop 中无法清理
  • ❌ 返回栈上的 trait object 引用