Rust:维护代码时遇到的坑

548 阅读4分钟

维护时很常见的结构性“坑” 下面列出 8 个在 Rust crate 中经常遇到的设计失误,给出改进方法,以及在设计之初应当如何注意


1. 把具体类型写进公有 API

pub struct Client { pub socket: std::net::TcpStream }

调用方直接操作字段,库失去演进空间。

改进方式

  • 改为 pub struct Client { socket: TcpStream } + 方法。
  • 如果外部已经依赖字段 → 只能发 破坏性版本。

事前规避

  • 所有字段默认 pub(crate) / 私有。
  • 暴露 函数 / trait。
pub struct Client { socket: std::net::TcpStream }   // 私有
impl Client {
    pub fn write(&mut self, bytes: &[u8]) -> io::Result<()> { … }
}

2. “封闭”枚举导致无法扩展

公共错误类型写成

pub enum MyError {
   Io(std::io::Error),
   Utf8(std::str::Utf8Error),
}

以后新增错误必须 破坏性修改。

改进方式

  • #[non_exhaustive] 加在枚举上,提醒用户加 _ => {} 分支。
  • 或转成 enum MyError { …, Other(Box<dyn std::error::Error + Send + Sync>) }

事前规避

  • 一开始就 #[non_exhaustive].
  • 或者把错误做成 trait 对象。

3. 错误只用 String

fn run() -> Result<(), String>:调用方无法分情况处理。

改进方式

  • 引入 thiserror,把 String 解析回结构化错误(痛苦)。

事前规避

#[derive(thiserror::Error, Debug)]
pub enum RunError {
   #[error("network: {0}")]
   Net(#[from] std::io::Error),
   #[error("parse")]
   Parse,
}

4. 同步 / 异步界面绑死

只发布阻塞 API;需要 async 时只能全部重写。

改进方式

  • 出一个 *_async 模块或新 crate(破坏生态)。

事前规避

  • 早期就分层:
    • 核心 IO-free 逻辑 → 同步函数;
    • 传输层 → 提供 sync + async 两套适配器。
  • 若只能选一种 → 先选 async,同步可 block_on.

5. 构造函数参数爆炸

fn new(a: u32, b: bool, c: Option<PathBuf>, d: Timeout, …)

改进方式

  • 新增 Builder;保留旧 new() 做包装。

事前规避

  • 一开始就 Builder。
  • 字段多还可 serde + TOML/JSON 配置。
MyClient::builder()
   .timeout(Duration::from_secs(3))
   .enable_ssl(true)
   .build()?

6. 交叉模块耦合/循环依赖

a.rs → b.rs → a.rs;改动一个文件连锁爆红。

改进方式

  • 抽公共 trait / types 放到 core 模块。
  • 拆 crate。

事前规避

  • “洋葱模型”
    1. domain types
    2. service/actor
    3. adapter (CLI / HTTP ) 每一层 只 依赖外侧。

7. Feature Flag 颗粒过大

启一个 feature 把一堆巨型依赖拖进来;裁不掉。

改进方式

  • 重新切分多个细粒度 features = ["tls-openssl", "tls-rustls"].

事前规避

  • “默认最小”原则 (default-features = false 能编)。
  • 每个外部依赖至少包一层 feature。
[features]
default = ["rustls"]
openssl = ["dep:openssl"]
rustls  = ["dep:rustls"]

8 开发共有库将私有字段或者底层实现暴露给调用方

一句话:你把同步策略 (Arc+Mutex) 直接写进了公有 API,调用方必须跟着你的选择走,这就叫“暴露实现细节”。

// lib.rs —— 做法 A:暴露实现细节
use std::sync::{Arc, Mutex};

pub type Counter = Arc<Mutex<u32>>;   // 这是库对外公开的符号

pub fn new_counter() -> Counter {
    Arc::new(Mutex::new(0))
}

任何 use mylib::Counter 的代码都能写 .lock().unwrap(), 一旦你将来想把 Mutex 换成 RwLock 或把 u32 换成 AtomicU32,所有下游都要改。 这就是所谓“公共接口(public API surface)”直接依赖了你的同步工具。 对比“封装”的做法:

改进方式

// lib.rs —— 做法 B:把实现藏起来
use std::sync::{Arc, Mutex};

pub struct Counter { inner: Arc<Mutex<u32>> }

impl Counter {
    pub fn new() -> Self { Self { inner: Arc::new(Mutex::new(0)) } }
    pub fn inc(&self)        { *self.inner.lock().unwrap() += 1; }
    pub fn get(&self) -> u32 { *self.inner.lock().unwrap() }
}

外部只能看到 inc() / get() 这两个方法,不知道你用不用锁。 将来把 Mutex → RwLock 或干脆换成 Actor,编译器不会要求下游改任何代码。

事前规避

私有化一切实现细节(字段、类型别名、同步原语)。


Pomelo_刘金 ,转载请注明出处,感谢! 设计阶段的通用清单

  1. 私有化一切实现细节(字段、类型别名、同步原语)。
  2. 先列出“可能增长的维度”: • 新错误种类、后端驱动、协议版本、并发模型…… • 对外都用可扩展手段:#[non_exhaustive]、trait、特征对象、feature flag。
  3. 把“核心业务” 与 “I/O & 并发” 分开。核心逻辑不做线程假设。
  4. 构造复杂对象就用 Builder;避免位置参数长队。
  5. 如果需要多 owner——一开始就决定: • 只读 Arc/Rc • 可写 Mutex/RefCell • Actor
  6. 统一错误:thiserror + Result<T, E>。