手写依赖注入容器需实现注册、反射、递归三步协同:用接口类型作字典键注册映射,resolve时递归解析构造函数参数并处理单例缓存、循环依赖和生命周期,但缺乏作用域、泛型匹配及dispose管理等生产级能力。

不用手写也能用好依赖注入,但真想搞懂 Resolve 怎么一层层把 UserService 里嵌套的 IDbContext、ILogger 全部 new 出来,就得自己撸一个极简容器——它不为替代 Microsoft.Extensions.DependencyInjection,只为看清注册、反射、递归这三步怎么咬合。
注册接口与实现类时,字典键必须是接口类型
常见错误是把实现类当 key 存进映射表,导致 Resolve<ilogger>()</ilogger> 查不到映射。正确做法是用 typeof(TInterface) 作键,typeof(TImplementation) 作值:
- ✅
_mappings[typeof(ILogger)] = typeof(ConsoleLogger); - ❌
_mappings[typeof(ConsoleLogger)] = typeof(ILogger);(查ILogger时根本找不到) - 接口未注册时,
Resolve应抛InvalidOperationException而非返回 null,否则空引用崩溃会延迟到后续调用才暴露
Resolve 过程必须递归解析构造函数参数
Activator.CreateInstance 只能直接 new 无参类型;一旦 UserService 构造函数带 ILogger logger,就必须反射读取参数类型,再对每个参数类型调用一次 Resolve:
- 用
type.GetConstructors()[0].GetParameters()拿到所有依赖类型 - 对每个
ParameterInfo.ParameterType递归调用Resolve,形成依赖树 - 若某依赖未注册,立即抛异常,别等到构造函数执行一半才失败
- 注意循环依赖:A 依赖 B,B 又依赖 A —— 简易容器可加一个正在构建中的类型集合做检测
单例生命周期必须缓存实例,且只缓存一次
单例不是“只注册一次”,而是“首次 Resolve 后把实例存起来,后续直接返回”。缓存位置很关键:
- ✅ 存进
Dictionary<type object></type>,key 是接口类型(如typeof(ILogger)) - ❌ 存实现类类型(
typeof(ConsoleLogger)),会导致Resolve<ilogger>()</ilogger>和Resolve<iasynclogger>()</iasynclogger>拿不到同一个实例 - 多线程下需加锁或用
ConcurrentDictionary,否则可能重复创建 - 瞬时(Transient)则每次调用都走完整反射+new流程,不缓存
.NET 内置容器不等于“必须用它”,但绕过它要承担代价
手写容器跑通 Resolve<irepository>()</irepository> 很快,但真实项目里很快会撞上这些点:
- 作用域(Scope)缺失:没
CreateScope()就没法支持AddScoped<dbcontext>()</dbcontext>,控制台程序直接 Resolve Scoped 服务必报No service for type... - 泛型注册支持弱:
Register<ihandler>, Handler<t>>()</t></ihandler>需额外处理开放泛型匹配,内置容器已做好 - 性能损耗:每次
Resolve都反射构造函数,高频调用场景下比内置容器慢数倍(后者做了元数据缓存) - Dispose 管理缺失:没集成
IDisposable生命周期跟踪,DbContext类型不会被自动释放
真正容易被忽略的,是“注册顺序”和“泛型约束”的静默失效——比如 Register<ilogger filelogger>()</ilogger> 中 FileLogger 没实现 ILogger,编译器不报错,运行时 Resolve 才崩;这类问题在手写容器里得靠单元测试硬盖住,而不是指望编译器。











