目录

依赖注入的生命周期(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 容器内部核心步骤:

  1. 扫描注册表(你 Register 的所有类型)
  2. 选择合适的构造函数
  3. 通过反射创建实例
  4. 递归解决依赖
  5. 应用生命周期管理(缓存单例、scope 等)

1. DI 容器的“注册表”

当你写:

container.Register<IUserService, UserService>();

容器会做两件事:

  1. 记录:接口 → 实现类
  2. 记录生命周期(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>();

容器流程如下:

  1. 查看注册表:IUserService 对应 UserService
  2. 找出构造函数参数
  3. 对每个参数递归 Resolve(寻找对应实现类)
  4. 根据生命周期决定是否从缓存取
  5. 如果不是缓存,则通过反射创建实例
  6. 返回最终实例

像武林总盟这样处理:

A 想学武 → 查档案 → 派师傅 → 派师傅的师傅 → 配好一整套班底 → 返回完整的训练团队。