夜雨聆风学习资料网

ARTICLE · 1030362

C# 手写 IOC 容器源码实战|从零实现依赖注入,看完落地到.NET 项目

C# 手写 IOC 容器源码实战|从零实现依赖注入,看完落地到.NET 项目

做.NET 开发的同学,几乎每天都在写 AddScopedAddSingletonAddTransient,靠依赖注入撑起整个项目的架构。但如果面试官问你:能不能手写一个 IOC 容器?DI 的底层原理是什么? 很多人就卡壳了。

很多人用了多年 DI,却始终把它当黑盒:只知道注册服务、获取服务,除了循环依赖、作用域异常就只能瞎试。今天我们就从零开始,用 C# 手写一个完整的轻量 IOC 容器,覆盖三大生命周期、构造函数注入、作用域创建、循环依赖检测等核心能力。看完不仅能懂原理,还能直接把思路落地到日常项目里。

一、先搞透:IOC 和 DI 到底是什么?

在动手写代码之前,先把核心概念掰扯清楚,这是手写容器的前提。

1. 核心概念

  • IOC(控制反转)
    :是一种设计思想,把「对象创建、依赖管理」的控制权,从业务代码手中反转交给容器。以前我们是 new 对象,现在由容器统一创建、注入。
  • DI(依赖注入)
    :是 IOC 思想的具体实现方式,最常用的就是构造函数注入:容器自动把依赖的对象通过构造函数传进去,不用自己手动实例化。

2. IOC 容器的三大核心能力

任何一个合格的 IOC 容器,都离不开这三件事:

  1. 服务注册
    :告诉容器「哪个接口对应哪个实现类」,以及它的生命周期
  2. 服务解析
    :根据服务类型,自动创建实例并递归解析所有依赖
  3. 生命周期管理
    :控制实例是全局唯一、每次新建,还是每个作用域一个
3. 为什么要手写 IOC?
  • 面试硬通货:.NET 中高级面试高频考点,手写 IOC 是区分「会用」和「懂原理」的分水岭
  • 排错能力翻倍:遇到循环依赖、作用域对象释放、服务注册冲突等问题,一眼定位根因
  • 夯实底层功底:顺带吃透反射、构造函数选择、生命周期设计、递归解析这些基本功

二、动手前:整体架构与功能规划

我们不搞玩具代码,要做就做一个可运行、覆盖核心场景的 IOC 容器,规划如下:

支持的核心功能

✅ 三种标准生命周期:瞬时(Transient)、单例(Singleton)、作用域(Scoped) ✅ 构造函数注入,自动递归解析依赖链 ✅ 支持泛型 + Type 两种服务注册方式 ✅ 支持创建作用域,作用域内实例共享 ✅ 循环依赖自动检测并抛出异常 ✅ 服务未注册、无匹配构造函数等边界处理

核心类设计

我们对标官方 Microsoft.Extensions.DependencyInjection 的核心设计,精简出 6 个核心类型:

三、第一步:定义基础枚举与服务元数据

万丈高楼平地起,先把最基础的元数据定义好。

1. 生命周期枚举

2. 服务描述类 ServiceDescriptor

这是容器的「数据底座」,每注册一个服务,就对应一个 ServiceDescriptor,记录服务的所有元信息。

四、第二步:实现服务注册层 IServiceCollection

服务注册层负责收集所有服务的描述信息,相当于容器的「注册表」。

1. 接口与默认实现

2. 封装常用注册扩展方法

为了和官方用法保持一致,我们用扩展方法封装 AddTransientAddSingletonAddScoped,支持泛型用法。

五、第三步:核心实现!服务解析与 ServiceProvider

ServiceProvider 是整个 IOC 容器的心脏,负责创建实例、解析依赖、管理生命周期。这部分是重中之重,我们一步步拆解。

1. 核心字段与构造函数

2. 核心入口:GetService 方法

这是容器对外的核心方法,根据服务类型返回实例,内部处理生命周期和依赖解析。

3. 生命周期实例管理

三种生命周期的核心区别,就是实例缓存的位置不同

  • 单例:存在根容器的 _singletonCache 里,全局唯一
  • 作用域:存在当前容器的 _scopedCache 里,作用域内唯一
  • 瞬时:不缓存,每次都新建

4. 实例创建与依赖解析(核心中的核心)

CreateInstance 方法负责通过反射创建实例,并自动解析构造函数的所有参数依赖,支持递归解析。

我们采用最优构造函数选择策略:优先选择参数最多、且所有参数都能被容器解析的构造函数。

六、第四步:实现作用域 Scoped 功能

Scoped 生命周期在ASP.NET Core 里非常常用(每个请求一个作用域),核心是:作用域内实例共享,作用域释放时自动回收。

1. 作用域接口与实现

2. 在 ServiceProvider 中添加创建作用域的方法
注意:单例缓存是根容器共享的,而作用域缓存是每个子容器独立的,这就是「单例全局唯一、作用域内唯一」的实现原理。

3. 资源释放

最后实现 IDisposable,容器释放时自动销毁所有实现了 IDisposable 的服务实例。

七、实战验证:控制台项目跑通我们的 IOC

写了这么多代码,到底能不能用?我们写一个完整的控制台 Demo 来验证所有功能。

1. 定义测试接口与实现

模拟三层依赖,包含不同生命周期:

2. 主程序测试

运行之后,你会看到单例 ID 全局一致、作用域内实例相同、多层依赖自动解析,完美符合预期。

八、落地进阶:从手写容器到官方 DI 的进阶

我们手写的这个容器,已经覆盖了 DI 的核心本质,但和官方的 Microsoft.Extensions.DependencyInjection 比,还有不少高级功能可以拓展:

1. 可以继续拓展的高级特性

  • 属性注入
    :通过反射给标记了特性的属性赋值
  • 泛型开放类型注册
    :比如 Add(typeof(IRepository<>), typeof(BaseRepository<>))
  • 装饰器模式支持
    :动态给服务包装装饰器
  • 表达式树优化
    :用表达式树编译替代反射,大幅提升解析性能
  • 服务键控注册
    :同一个接口多个实现,按 key 获取

2. 对日常开发的落地价值

很多同学会说:「我直接用官方 DI 就行,手写有啥用?」

  • 遇到循环依赖报错,你知道是递归解析时的栈检测,能快速定位依赖链
  • 遇到Scoped 服务在单例中失效,你懂是因为实例缓存层级不同,能立刻想到用作用域工厂
  • 看第三方框架的 DI 扩展(比如 ABP、Autofac),能快速看懂底层思路
  • 面试遇到「手写 IOC」,直接默写核心逻辑,当场拉开差距

九、写在最后:手写 IOC,你收获的不只是代码

从零手写一个 IOC 容器,本质是把黑盒的依赖注入拆开揉碎,理解每一步的设计思想。这不是造轮子,而是练内功:

  • 彻底搞懂「控制反转」到底反转了什么
  • 掌握反射、生命周期、递归解析的底层玩法
  • 建立「接口 - 注册 - 解析 - 管理」的架构思维

相关学习资料

返回首页浏览学习资料