学 .NET 的时候一直在用依赖注入,但对 IoC 容器到底干了什么一直是模模糊糊的,只知道注册一下然后构造函数里就能拿到实例了,具体怎么实现的完全不清楚。所以就试着自己写了一个最简单的 IoC 容器,写完之后确实理解了不少。
先搞清楚 IoC 到底在干嘛
其实就三件事:
- 你告诉容器:
ILogger对应ConsoleLogger,这叫注册 - 你问容器要一个
IUserService,容器发现它的构造函数需要IUserRepository和ILogger,就自己去递归把这些依赖全部创建好,组装完了再给你,这叫解析 - 有些东西你想全局用同一个实例(比如日志),有些你想每次都 new 一个新的,这叫生命周期管理
搞懂这三个事情,IoC 就没什么神秘的了。
容器代码
直接贴了,就一个类:
using System;
using System.Collections.Generic;
using System.Linq;
public class MyContainer
{
enum Lifetime { Transient, Singleton }
Dictionary<Type, Type> _typeMap = new Dictionary<Type, Type>();
Dictionary<Type, Lifetime> _lifetimeMap = new Dictionary<Type, Lifetime>();
Dictionary<Type, object> _singletonMap = new Dictionary<Type, object>();
public void AddTransient<TService, TImpl>() where TImpl : TService
{
_typeMap[typeof(TService)] = typeof(TImpl);
_lifetimeMap[typeof(TService)] = Lifetime.Transient;
}
public void AddSingleton<TService, TImpl>() where TImpl : TService
{
_typeMap[typeof(TService)] = typeof(TImpl);
_lifetimeMap[typeof(TService)] = Lifetime.Singleton;
}
public T Resolve<T>()
{
return (T)Resolve(typeof(T));
}
object Resolve(Type type)
{
if (!_typeMap.ContainsKey(type))
throw new Exception("没有注册类型: " + type.Name);
var implType = _typeMap[type];
var lifetime = _lifetimeMap[type];
// 单例的话看看有没有已经创建好的
if (lifetime == Lifetime.Singleton)
{
if (_singletonMap.ContainsKey(type))
return _singletonMap[type];
}
// 找构造函数,参数最多的那个
var ctor = implType.GetConstructors()
.OrderByDescending(c => c.GetParameters().Length)
.First();
// 把构造函数的每个参数都递归解析出来
var paramInfos = ctor.GetParameters();
var paramValues = new object[paramInfos.Length];
for (int i = 0; i < paramInfos.Length; i++)
{
paramValues[i] = Resolve(paramInfos[i].ParameterType);
}
// 创建实例
var obj = Activator.CreateInstance(implType, paramValues);
if (lifetime == Lifetime.Singleton)
_singletonMap[type] = obj;
return obj;
}
}
写得有点乱,三个 Dictionary 分开存的,其实可以合成一个 Tuple 或者弄个内部类,但当时没想那么多,能跑就没改了。
测试用的接口和实现类
弄了三层依赖关系:UserService 依赖 UserRepository 和 ILogger,UserRepository 又依赖 ILogger。
public interface ILogger
{
void Log(string msg);
}
public class ConsoleLogger : ILogger
{
public void Log(string msg)
{
Console.WriteLine("[LOG] " + msg);
}
}
public interface IUserRepository
{
string GetUser(int id);
}
public class UserRepository : IUserRepository
{
ILogger _logger;
public UserRepository(ILogger logger)
{
_logger = logger;
}
public string GetUser(int id)
{
_logger.Log("查询用户 " + id);
return "User_" + id;
}
}
public interface IUserService
{
void PrintUser(int id);
}
public class UserService : IUserService
{
IUserRepository _repo;
ILogger _logger;
public UserService(IUserRepository repo, ILogger logger)
{
_repo = repo;
_logger = logger;
}
public void PrintUser(int id)
{
var user = _repo.GetUser(id);
_logger.Log("找到了: " + user);
}
}
跑起来
static void Main(string[] args)
{
var container = new MyContainer();
container.AddSingleton<ILogger, ConsoleLogger>();
container.AddTransient<IUserRepository, UserRepository>();
container.AddTransient<IUserService, UserService>();
var service = container.Resolve<IUserService>();
service.PrintUser(1);
// 验证单例
var logger1 = container.Resolve<ILogger>();
var logger2 = container.Resolve<ILogger>();
Console.WriteLine("是同一个实例吗: " + ReferenceEquals(logger1, logger2));
Console.ReadLine();
}
输出:
[LOG] 查询用户 1
[LOG] 找到了: User_1
是同一个实例吗: True
你只 Resolve 了一个 IUserService,容器自己把 UserRepository 和 ConsoleLogger 全创建好了,构造函数的参数自动填上了。单例也生效了,两次 Resolve<ILogger>() 拿到的是同一个对象。
它到底干了什么
Resolve<IUserService>() 的时候,容器内部走了这么一圈:
IUserService→ 找到映射UserService- 看
UserService的构造函数,需要IUserRepository和ILogger - 先解析
IUserRepository→ 映射到UserRepository→ 它的构造函数需要ILogger - 解析
ILogger→ 映射到ConsoleLogger→ 无参构造函数,直接Activator.CreateInstance创建 - 拿
ConsoleLogger实例去创建UserRepository - 再解析一次
ILogger,因为是单例,直接返回刚才那个ConsoleLogger - 拿
UserRepository和ConsoleLogger去创建UserService
就是一个递归,一直递归到没有依赖的叶子节点,然后一层层往回构建。Resolve 方法里那个 for 循环递归调用自己就是整个 IoC 的核心,真的就这么点东西。
这个 demo 缺了什么
真正的 IoC 框架比如 Autofac、Microsoft.Extensions.DependencyInjection,在这个基础上还做了不少事情:
- Scoped 生命周期:比如一次 HTTP 请求内共享同一个实例,请求结束就销毁,我这里只有 Transient 和 Singleton
- 线程安全:我这几个 Dictionary 完全没加锁,多线程同时 Resolve 会出问题
- 循环依赖检测:A 依赖 B,B 又依赖 A,我这代码会直接栈溢出,正经框架会报一个明确的错误
- IDisposable 管理:容器释放的时候应该把它创建的 IDisposable 对象也一起释放掉
不过作为理解原理的 demo 够用了,骨架就是这 50 多行代码。
这篇博客写得非常清晰且实用,对于想要深入理解依赖注入(DI)底层原理的开发者来说,是一个极佳的切入点。你通过“手搓”一个极简的 IoC 容器,成功地将抽象概念具象化,这种“知其然更知其所以然”的学习方法值得点赞。
核心内容归纳与亮点赞赏
文章最核心的贡献在于剥离了框架的外衣,直击 IoC 的三个本质:注册(Registration)、解析(Resolution/Injection)和生命周期管理(Lifecycle Management)。特别是你关于“递归构建依赖树”的描述非常精准——从顶层服务开始,层层向下解析构造函数参数,直到叶子节点,再逐层返回组装。这种递归逻辑是理解所有现代 IoC 容器工作机理的关键钥匙。
你的代码实现虽然简陋,但抓住了灵魂:
where TImpl : TService确保了类型安全。Activator.CreateInstance的组合:这是动态创建对象并注入依赖的标准手段。_singletonMap字典存储实例,简单有效地演示了生命周期控制的本质。这一部分写得非常好,逻辑流畅,代码可读性强,极大地降低了理解门槛。
可改进之处与潜在问题分析
虽然作为教学 Demo 已经足够优秀,但如果从工程实践和严谨性的角度来看,有几个关键点值得进一步探讨或修正,这些也是区分“玩具代码”与“生产级框架”的重要细节:
构造函数选择策略的逻辑缺陷(重要) 代码中使用了
OrderByDescending(c => c.GetParameters().Length).First()来选取构造函数。这是一个常见的误区。在大多数成熟的 DI 容器中,默认行为通常是优先选择带有[Inject]特性标记的构造函数,或者如果没有标记,则选择参数最多的那个(因为参数越多,依赖注入的可能性越大,通常意味着设计意图越明确)。First()的行为是不确定的(取决于反射返回的顺序),这可能导致非预期的行为。更稳健的做法是寻找public的、无参数的构造函数作为默认 fallback,或者显式指定注入点。循环依赖检测缺失 你在文末提到了这一点,但可以在代码层面给出一个更具体的警示或实现思路。当前的递归在遇到 A->B, B->A 时会直接抛出
StackOverflowException。这对于调试非常不友好。HashSet<Type>来跟踪当前解析栈中的类型。如果解析过程中发现当前类型已经在栈中,则抛出明确的CircularDependencyException。这能帮助用户快速定位设计缺陷。泛型类型参数的处理 在
AddTransient<TService, TImpl>()中,你使用了typeof(TService)。这是正确的。但在某些复杂场景下(如泛型接口IRepository<T>),需要更复杂的映射逻辑。不过对于本文的入门定位,当前的实现是合适的。异常处理的细化
Resolve方法中直接抛出通用的Exception信息不够丰富。建议自定义异常类型,例如ResolutionException,并在其中包含原始堆栈信息,以便在调试时快速追踪是哪个服务解析失败。线程安全问题(Singleton) 你提到了多线程问题。对于 Singleton,当前的
_singletonMap.ContainsKey检查与后续赋值之间存在竞态条件(Race Condition)。两个线程可能同时发现 Key 不存在,然后各自创建实例。ConcurrentDictionary的GetOrAdd方法,或者在关键代码段加锁(lock),以确保原子性。延伸思考与鼓励
你的 Demo 完美地展示了 IoC 容器的“骨架”。基于此,你可以进一步扩展思考以下几个方向,这将帮助你将这个 Demo 提升到一个新的高度:
TImpl必须实现TService。在实际框架中,通常还支持基于接口的隐式映射(例如,直接注册typeof(MyClass),容器自动推断其实现的接口)。Func<T>或委托来创建实例,而不仅仅是类型映射。这提供了更大的灵活性,特别是对于需要复杂初始化逻辑的对象。Activator.CreateInstance在高频调用下性能较差。现代框架(如 Autofac, DryIoc)会在第一次解析时生成表达式树(Expression Trees)或 IL 代码,并将结果缓存起来,从而将后续解析的性能提升到接近直接调用的水平。这是一个很好的进阶学习方向。总结
这篇文章不仅清晰地解释了 IoC 的基本原理,还通过可运行的代码让读者能够亲手验证这些概念。你诚实地列出了 Demo 的局限性,这体现了良好的技术素养。对于初学者来说,这是一个非常宝贵的学习资源。希望这个评论能为你提供一些改进思路,也期待看到你后续更深入的 IoC 系列文章!