用法指南与决策矩阵
本文档是 rust-dix 的推荐用法权威指南:如何简化导入路径、在不同场景下选择哪种注册方式与构造函数策略,以及让代码更简洁的实用技巧。
> 阅读本文前,建议先浏览 [面向接口开发清单](interface-checklist.md) 建立整体认知。
---
1. 导入约定(路径简化)
rust-dix 的宏与类型都从 `rust_dix` crate 重新导出。**推荐在每个使用 DI 的文件顶部加两行 use**,之后所有宏和类型可直接短名引用:
use rust_dix::*; // 引入所有宏与类型
use std::sync::Arc; // 共享依赖的 Arc 包装
`use rust_dix::*` 后可直接使用:
| 类别 | 可用名称 | |------|---------| | 属性宏 | `#[inject]`、`#[module]` | | 派生宏 | `#[derive(Inject)]` | | 函数式宏 | `register!()` | | 类型 | `ServiceCollection`、`ServiceProvider`、`Scope`、`IServiceResolver`、`RdiError` |
**简化对照表:**
| 旧写法(全限定) | 推荐写法(简短) | |------------------|-----------------| | `#[rust_dix::inject]` | `#[inject]` | | `#[rust_dix::inject(scoped)]` | `#[inject(scoped)]` | | `#[derive(rust_dix::Inject)]` | `#[derive(Inject)]` | | `#[rust_dix::module]` | `#[module]` | | `rust_dix::register!(...)` | `register!(...)` | | `rust_dix::ServiceCollection::new()` | `ServiceCollection::new()` |
**大型多 crate 项目**:若担心 glob 引入污染命名空间,可用别名:
use rust_dix as di; // 之后写 #[di::inject]、di::register!()
> 约定:本文档及最佳实践章节的所有示例均采用 `use rust_dix::*` 简短风格。参考文档(如宏详解)为明确宏来源可能使用全限定路径,实际编码请用简短形式。
---
2. 注册方式决策矩阵
框架提供四种注册途径,按场景选择:
| 场景 | 推荐方式 | 一句话理由 | |------|---------|-----------| | 标准服务自动注册 | `#[inject]` 属性宏 | 零样板,注册 + 构造一步到位 | | 面向接口 trait 注册 | `#[inject]` 在 `impl Trait for T` 块 | trait 自动推断,无需 `as =` | | 多实现运行时路由 | keyed + 属性宏/闭包 | 一个接口多实现,策略模式 | | 集中声明式配置 | `#[module]` + `register!()` | 注册声明与服务模块放在一起 | | 已有实例 / 配置值 | `singleton_value(T)` / `instance(Arc<T>)` | 免写工厂闭包 | | 条件可选注册 | `try_add(...)` | 已注册则跳过,不覆盖 | | 复杂工厂 / 运行时参数 | 手动闭包 `\|p\| Arc::new(...)` | 需要计算逻辑或外部参数 |
**示例对照:**
use rust_dix::*;
use std::sync::Arc;
// 场景 1:标准服务 —— 属性宏零样板
#[inject(singleton)]
struct Config { port: u16 }
// 场景 2:接口注册 —— impl 块自动推断 trait
struct PgUserRepo;
#[inject] impl IUserRepo for PgUserRepo { /* ... */ }
// 场景 3:多实现路由 —— keyed 手动注册(keyed 不支持 #[inject] 自动注册)
let provider = ServiceCollection::new()
.keyed_singleton::<dyn IPayGateway>("wechat", |_| Arc::new(WechatPay))
.keyed_singleton::<dyn IPayGateway>("alipay", |_| Arc::new(AlipayPay))
.build()?;
// 场景 5:已有实例 —— singleton_value 自动 Arc
let provider = ServiceCollection::new()
.singleton_value(Config { port: 8080 })
.build()?;
// 场景 7:复杂工厂 —— 手动闭包
let provider = ServiceCollection::new()
.singleton(|p| Arc::new(Pool::connect(&p.get::<Config>().unwrap().url)))
.build()?;
放置位置:struct 还是 impl?(二选一,避免双重注册)
`#[inject]` 放在 struct 上 = 注册为**具体类型**;放在 `impl Trait for T` 上 = 注册为 **`dyn Trait`**。两者同时用会让同一 struct 被注册两次(具体类型 + trait),通常不是预期——消费者能绕过 trait 直接 `get::<具体类型>()`,破坏面向接口原则。
关键区分:`#[derive(Inject)]` 与 `#[inject]` 都能在 struct 侧生成构造函数,但**只有 `#[inject]` 会向容器提交注册**。因此面向接口时 struct 上应选 `#[derive(Inject)]`(仅构造、不注册)。
| 目标注册身份 | struct 上 | impl 上 | 结果 | |------------|----------|---------|------| | 仅具体类型(无 trait 契约) | `#[inject]` | *不放* | ✅ 单一注册 | | 仅 `dyn Trait`(面向接口,**推荐**) | `#[derive(Inject)]` | `#[inject]` | ✅ 单一注册 | | 两者都要(罕见,确需同时暴露) | `#[inject]` | `#[inject]` | ⚠️ 双重注册,需注释说明意图 | | 仅 impl、struct 无构造函数 | *不放* | `#[inject]` | ❌ 编译失败(无 `__rdi_construct`) |
use rust_dix::*;
use std::sync::Arc;
// ✅ Handler 面向接口:struct 用 #[derive(Inject)] 仅生成构造函数,impl 用 #[inject] 注册为 trait
#[derive(Inject)]
struct UserHandler {
#[inject]
user_svc: Arc<dyn IUserService>,
}
#[inject] // 仅注册为 dyn IUserHandler
impl IUserHandler for UserHandler {
fn handle(&self, req: Req) -> Resp { /* ... */ }
}
// ❌ 双重注册:struct 和 impl 都用 #[inject] → 容器里既有 UserHandler 又有 dyn IUserHandler
#[inject(transient)]
struct UserHandler { /* ... */ }
#[inject]
impl IUserHandler for UserHandler { /* ... */ } // 消费者可 get::<UserHandler>() 绕过接口
// ✅ 无 trait 契约的简单服务(配置、值对象):只在 struct 上用 #[inject]
#[inject(singleton)]
struct Config { port: u16 }
**何时放 struct 上**:服务作为具体类型对外暴露——无 trait,或消费者直接用具体类型(如配置、值对象)。 **何时放 impl 上**:服务有 trait 契约,面向接口便于 mock 替换——Handler / Service / Repository 均属此类,**推荐**。此时 struct 上用 `#[derive(Inject)]` 而非 `#[inject]`。 **Handler 推荐**:放 impl 上(注册为 trait),struct 上用 `#[derive(Inject)]`。
---
3. 构造函数策略:手写 vs 宏标记 vs 闭包
这是最常见的设计抉择。三种方式各有适用场景,**不互相排斥,可在同一项目混用**。
三种方式对比
| 方式 | 代码量 | 结构体干净度 | 适用场景 | |------|--------|-------------|---------| | 手写构造函数 + 闭包 | 中 | 最干净(字段零属性) | 初始化逻辑复杂、字段需计算 | | `#[inject]` 字段标记 | 最少 | 中(依赖字段有标记) | 依赖都是可直接解析的服务 | | 手动闭包工厂 | 中 | 最干净 | 已有实例、条件构造、运行时参数 |
方式 A:手写构造函数(结构体最干净)
结构体字段不带任何 DI 属性,保持纯净。在闭包中调用自定义构造函数:
use rust_dix::*;
use std::sync::Arc;
pub struct OrderService {
repo: Arc<dyn IOrderRepo>, // 字段无任何属性,结构体干净
threshold: u32, // 内部状态
}
impl OrderService {
// 自定义构造函数,完全掌控初始化逻辑
pub fn new(repo: Arc<dyn IOrderRepo>, threshold: u32) -> Self {
Self { repo, threshold }
}
}
// 注册时用闭包调用构造函数
let provider = ServiceCollection::new()
.singleton(|p| Arc::new(OrderService::new(
p.get().unwrap(),
p.get::<Config>().unwrap().threshold,
)))
.build()?;
**适用**:初始化需要计算、字段不适合直接注入、想保持结构体完全无关 DI。
方式 B:`#[inject]` 字段标记(零样板)
用 `#[derive(Inject)]` 或 `#[inject]` 属性宏自动生成构造函数,依赖字段显式标记:
use rust_dix::*;
use std::sync::Arc;
#[inject(singleton)]
pub struct OrderService {
#[inject]
repo: Arc<dyn IOrderRepo>, // 标记 → 自动注入
threshold: u32, // 未标记 → Default::default()
}
// 无需手写构造函数,无需手动注册 —— 属性宏一步到位
let provider = ServiceCollection::from_injected().build()?;
**适用**:依赖都是容器可直接解析的服务,字段可 `Default` 或可注入。这是**推荐的首选方式**——零样板、声明式、类型安全。
> 未标记字段(如 `threshold: u32`)自动走 `Default::default()`,保持结构体相对干净。若该字段需要注入而非默认值,加 `#[inject]`(`Arc<T>`)或 `#[inject(owned)]`(裸 `T`)。
方式 C:闭包工厂
手动写闭包,适合已有实例或需要运行时参数:
use rust_dix::*;
use std::sync::Arc;
let existing_db = Arc::new(DbPool::connect("..."));
let provider = ServiceCollection::new()
.instance(existing_db) // 已有 Arc 实例
.singleton_value(Config { port: 8080 }) // 自动 Arc 包装
.build()?;
**适用**:已有 `Arc<T>` 实例、配置值、需要运行时参数构造。
决策树
依赖字段能否被容器直接解析?
├─ 能(都是注册的服务)→ 用方式 B:#[inject] 字段标记(推荐)
└─ 不能(需计算 / 已有实例 / 运行时参数)
├─ 初始化逻辑复杂 → 用方式 A:手写构造函数
└─ 简单已有实例 → 用方式 C:闭包工厂 / instance / singleton_value
---
4. 生命周期选择
参见 [第八章索引 · 生命周期选择](INDEX.md#生命周期选择):
| 场景 | 生命周期 | |------|---------| | 全局配置、连接池、缓存 | Singleton | | 请求上下文、事务、用户会话 | Scoped | | 无状态轻量服务、DTO | Transient | | **禁止** Singleton 依赖 Scoped | build 阶段检测并拒绝 |
> Owned 语义:需要独占修改(`&mut self`)的服务用 `#[inject(owned)]` 注入裸 `T`,每次解析新建实例,Singleton 不可 owned。
---
5. 字段注入标记速查
仅在采用方式 B(`#[inject]` 字段标记)时适用:
| 标记 | 字段类型 | 行为 | 未注册时 | |------|---------|------|---------| | *(无标记)* | 任意(需 `Default`) | `Default::default()` | N/A | | `#[inject]` | `Arc<T>` | 共享注入 | `Err(ServiceNotFound)`(含字段 `context`) | | `#[inject]` | `Option<Arc<T>>` | 可选共享 | `None` | | `#[inject(key = "k")]` | `Arc<T>` | 键控共享 | `Err(KeyedServiceNotFound)` | | `#[inject(owned)]` | 裸 `T` | owned 注入 | `Err`(未注册或 Singleton) | | `#[inject(owned)]` | `Option<T>` | 可选 owned | `None` | | `#[inject(provider)]` | `Arc<dyn IServiceResolver>` | 注入 resolver | N/A |
> 严格类型校验:`#[inject]` 只接受 `Arc<T>`/`Option<Arc<T>>`,`#[inject(owned)]` 只接受裸 `T`/`Option<T>`,不匹配触发编译错误。可选性由 `Option<...>` 类型决定,无需额外标记。
---
6. 代码简洁性技巧
以下是让 DI 代码更简洁、可读性更高的实用技巧:
1. **`use rust_dix::*` 统一引入** —— 消除所有 `rust_dix::` 前缀,示例见 §1。 2. **`#[inject]` 属性宏零样板** —— 一步完成构造 + 注册,对比手动 `ServiceCollection` 逐个 `.singleton()` 大幅减少样板。 3. **impl 块 trait 自动推断** —— `#[inject] impl Trait for T` 自动注册为 `dyn Trait`,无需 `as = dyn Trait`。 4. **`singleton_value(T)` 自动 Arc** —— 直接传值,框架自动 `Arc::new`,比 `instance(Arc::new(T))` 更简洁。 5. **`from_injected()` 一行收集** —— 所有 `#[inject]` 标注的注册自动收集,无需逐个 `.singleton(__rdi_construct_X)`。 6. **`Option<Arc<T>>` 可选依赖** —— 类型即语义,未注册返回 `None`,无需额外标记或 `try_get` 样板。 7. **单元结构体直接支持** —— `struct Config;` 零字段服务无需任何特殊处理。 8. **`try_add` 条件注册** —— 已注册则跳过,实现可选功能模块时不覆盖默认注册。 9. **未标记字段自动 `Default`** —— 内部状态字段无需任何属性,结构体保持干净(旧的 `#[rdi(skip)]` 已移除)。 10. **keyed 服务实现策略模式** —— 一个接口多实现,运行时 `get_keyed::<dyn Trait>("key")` 路由。
---
7. 完整示例:从冗长到简洁
同一 `OrderService`,三种写法的代码量对比:
写法 1:全限定路径 + 手动闭包(最冗长,不推荐)
use std::sync::Arc;
pub struct OrderService {
repo: Arc<dyn IOrderRepo>,
logger: Arc<dyn ILogger>,
}
impl OrderService {
pub fn new(repo: Arc<dyn IOrderRepo>, logger: Arc<dyn ILogger>) -> Self {
Self { repo, logger }
}
}
fn build() -> Result<rust_dix::ServiceProvider, rust_dix::RdiError> {
rust_dix::ServiceCollection::new()
.singleton(|p| Arc::new(OrderService::new(
p.get::<dyn IOrderRepo>().unwrap(),
p.get::<dyn ILogger>().unwrap(),
)))
.singleton::<dyn IOrderRepo>(|_| Arc::new(PgOrderRepo))
.singleton::<dyn ILogger>(|_| Arc::new(ConsoleLogger))
.build()
}
写法 2:`use *` + 手动闭包(简洁,方式 A)
use rust_dix::*;
use std::sync::Arc;
pub struct OrderService { // 结构体干净,零 DI 属性
repo: Arc<dyn IOrderRepo>,
logger: Arc<dyn ILogger>,
}
impl OrderService {
pub fn new(repo: Arc<dyn IOrderRepo>, logger: Arc<dyn ILogger>) -> Self {
Self { repo, logger }
}
}
fn build() -> Result<ServiceProvider, RdiError> {
ServiceCollection::new()
.singleton(|p| Arc::new(OrderService::new(p.get().unwrap(), p.get().unwrap())))
.singleton::<dyn IOrderRepo>(|_| Arc::new(PgOrderRepo))
.singleton::<dyn ILogger>(|_| Arc::new(ConsoleLogger))
.build()
}
写法 3:`#[inject]` 属性宏(最简洁,推荐,方式 B)
use rust_dix::*;
use std::sync::Arc;
#[inject(singleton)]
pub struct OrderService {
#[inject] repo: Arc<dyn IOrderRepo>,
#[inject] logger: Arc<dyn ILogger>,
}
#[inject] impl IOrderRepo for PgOrderRepo { /* ... */ }
#[inject] impl ILogger for ConsoleLogger { /* ... */ }
fn build() -> Result<ServiceProvider, RdiError> {
ServiceCollection::from_injected().build()
}
**对比**:写法 3 比写法 1 减少约 60% 代码量,无样板、无手动闭包、无全限定路径,且类型安全由宏在编译期保证。
---
下一步
- [常见陷阱与排查](common-pitfalls.md) —— 高频错误规避 - [反模式警示](anti-patterns.md) —— 错误模式与修正 - [面向接口开发清单](interface-checklist.md) —— 核心规则速查