依赖注入的生命周期(LifeTime) 和 DI 容器的工作原理
依赖注入的生命周期(LifeTime) 和 DI 容器的工作原理(反射、构造函数选择、实例缓存等)
第一部分:依赖注入的生命周期 —— Transient / Scoped / Singleton
生命周期(LifeTime)决定 DI 容器如何创建和管理对象。
你会在不同容器看到不同写法,比如:
- ASP.NET****Core: AddTransient / AddScoped / AddSingleton
- Prism (WPF): Register / RegisterSingleton
- Autofac: InstancePerDependency / SingleInstance
- Unity: TransientLifetimeManager / ContainerControlledLifetimeManager
虽然名字不同,但概念完全一致。
1. Transient —— “每次都新来一位师傅”
定义:
每次需要时,就 new 一个新的对象。
武侠比喻:
你每次练功都找一个不同的指导师,来多少次,换多少个。
代码:
services.AddTransient<IUserService, UserService>();特点:
- 最干净、最安全
- 不共享数据
- 适合轻量、无状态的服务
适合:
- Repository
- Tools(加密、计算类)
- Helper
2. Scoped —— “每回合共享同一个师傅”
WPF没有Scoped概念,主要存在于Web(每个 HTTP 请求为 Scope)。
定义:
一个 Scope(请求)内共享同一个实例,但不同 Scope 不共享。
武侠比喻:
你参加武林大会,一场比武中同一个师傅陪你,下一场又换一个。
代码:
services.AddScoped<IUserService, UserService>();适合:
- Web 中按“请求”缓存的数据
- 每个请求有独立的状态,例如 EF DbContext(经典使用)
3. Singleton —— “全天下只有这一个师傅”
定义:
整个程序中只有一个实例,全局共享。
比喻:
整个武林只认定一个领袖,他在的时候人人都可来问他。
代码:
services.AddSingleton<ICurrentUserService, CurrentUserService>();特点:
- 全局唯一
- 只能有一个实例
- 需要线程安全
- 会一直保存在内存中(直到程序结束)
适合:
- 配置读取器(Configuration)
- 缓存服务(Cache)
- FreeSql、HttpClient(推荐单例)
- 当前用户服务(WPF)
生命周期注意事项
单例不能依赖瞬时(Transient)对象:
因为瞬时对象每次都 new,单例构造一次就固定了依赖。
单例依赖范围(Scoped)对象会报错:
ASP.NET Core 会阻止这种用法。
生命周期总结表
| 声明周期 | 创建次数 | 共享范围 | WPF | ASP.NET Core | 适合使用 |
|---|---|---|---|---|---|
| Transient | 无限制 | 无共享 | ✔ | ✔ | 工具类 |
| Scoped | 每请求一次 | 请求内部共享 | ✘ | ✔ | DbContext |
| Singleton | 1次 | 全局共享 | ✔ | ✔ | 配置,缓存,当前用户 |
第二部分:DI 容器是如何工作的?(核心原理)
现在我们看看容器内部到底做了什么。
DI 容器内部核心步骤:
- 扫描注册表(你 Register 的所有类型)
- 选择合适的构造函数
- 通过反射创建实例
- 递归解决依赖
- 应用生命周期管理(缓存单例、scope 等)
1. DI 容器的“注册表”
当你写:
container.Register<IUserService, UserService>();容器会做两件事:
- 记录:接口 → 实现类
- 记录生命周期(Transient / Singleton)
就像武林总盟记录:
IUserService → 安排 UserService
生命周期 → 每次派一个新师傅(Transient)
2. 容器如何选择构造函数?
如果一个类有多个构造函数,容器会采用:
默认策略:选择参数最多的那个构造函数
(也称为“最长构造函数策略”)
例如:
public class UserService
{
public UserService() {}
public UserService(IUserRepository repo, ILogger logger) {}
}DI 会选第二个,因为:
依赖越多,越“完整”。
3. 容器通过“反射”创建实例
容器内部相当于做:
var constructor = type.GetConstructors().OrderByDescending(c => c.GetParameters().Length).First();
var parameters = constructor.GetParameters();
foreach (var p in parameters)
{
// 递归 resolve
var dependency = container.Resolve(p.ParameterType);
}4. 递归解析依赖(链式)
结构:
UserController
-> IUserService
-> UserService
-> IUserRepository
-> UserRepository容器会从最外层开始,递归解析,层层 Resolve。
5. 生命周期管理
容器内部会有缓存字典:
Dictionary<Type, object> singletonCache如果是 Singleton:
- 第一次 Resolve → 创建并放入缓存
- 后面 Resolve → 直接返回缓存实例
如果是 Transient:
- 每次都创建新的实例(不缓存)
如果是 Scoped:
- 放在当前 Scope 的缓存中(Web 请求场景)
总结:容器的工作流程
你写:
container.Register<IUserService, UserService>();
var s = container.Resolve<IUserService>();容器流程如下:
- 查看注册表:IUserService 对应 UserService
- 找出构造函数参数
- 对每个参数递归 Resolve(寻找对应实现类)
- 根据生命周期决定是否从缓存取
- 如果不是缓存,则通过反射创建实例
- 返回最终实例
像武林总盟这样处理:
A 想学武 → 查档案 → 派师傅 → 派师傅的师傅 → 配好一整套班底 → 返回完整的训练团队。