DI 的构造函数循环依赖问题
DI 的构造函数循环依赖问题
一、什么是构造函数循环依赖?
用最简单的话说:
两个服务互相依赖对方,导致 DI 容器无法创建实例,形成死锁。
例如:
public class A
{
public A(B b) { }
}
public class B
{
public B(A a) { }
}A 的构造函数需要一个 B
B 的构造函数需要一个 A
➡ DI 容器想创建 A,于是要 B
➡ 要创建 B,又需要 A
➡ 无限循环,爆炸 ❌
武侠理解:
- A 是“峨眉掌门”,练功需要“华山真气(B)”
- B 是“华山掌门”,练功需要“峨眉心法(A)”
两派互相都依赖对方才能修炼,所以谁都练不了 → 死循环。
二、DI 容器为什么会“死”?
注意:构造函数注入是最强依赖
DI 容器在解析构造函数时必须:
- 立即
- 完整
- 无条件
提供所有依赖。
所以:
构造函数里出现循环依赖 = 容器必死
.NET 默认容器也不支持构造函数循环依赖。
三、怎么发现循环依赖?
出现这种报错:
A circular dependency was detected for the service of type ...如果你看到这个,“恭喜”,你被循环依赖攻击了。
四、如何解决循环依赖(四大武功)
武功 1:使用接口拆分职责(最正统方式)
80% 的循环依赖都是因为类职责过大、边界不清晰。
例如:
public class UserService
{
public UserService(OrderService orderService) { }
}
public class OrderService
{
public OrderService(UserService userService) { }
}UserService 负责用户
OrderService 负责订单
但它们又互相依赖。
正确做法:
🔹 拆分成更小的接口(例如 IUserQuery / IOrderQuery)
🔹 单向依赖
武侠解释:
把“掌门”和“长老”的职责分开,他们互不依赖。
武功 2:使用 Lazy(延迟依赖)
让注入不要立即执行,而是等真正使用时再创建。
public class A
{
private readonly Lazy<B> _b;
public A(Lazy<B> b)
{
_b = b;
}
}容器不会马上创建 B,所以可以解除循环。
武侠解释:
“峨眉掌门”修炼需要“华山真气”,
但他不立刻要,只是说:
“等我需要时再叫他来。”
于是不会挂。
武功 3:使用 Func (工厂注入)
这比 Lazy 更灵活。
public class A
{
private readonly Func<B> _getB;
public A(Func<B> getB)
{
_getB = getB;
}
}武侠解释:
峨眉掌门不是要“华山真气的对象 B”,
而是要“一种能够随时召唤华山真气的方法”。
武功 4:使用 Property Injection / Method Injection
(不推荐,但确实能解决)
public class A
{
public B B { get; set; }
}或者:
public void SetB(B b)
{
this._b = b;
}武侠解释:
“我练功的时候先不用你,等需要辅助时你再来。”
但副作用多,不干净、难维护。
五、为什么 Scoped 服务中注入 Singleton 会出问题?
(这也会被误认为“循环依赖”)
- Singleton 生命周期最长
- Scoped 生命周期短
如果让 Singleton 注入 Scoped,会出现逻辑错误:
Singleton 想要依赖一个随着请求变化的对象 → 危险
这不是循环依赖,但会导致:
Cannot consume scoped service from singleton六、总结
方法 推荐度 适用场景
拆分接口(解耦)⭐⭐⭐⭐⭐ 根本性解决,最优雅
Lazy
Func
属性注入 ⭐⭐ 无法修改架构时临时方法