C#依赖注入是将对象创建控制权交由外部的设计实践,核心是类不自行new依赖而通过构造函数接收;需完成声明抽象、注册实现、接收依赖三步;生命周期选择错误易致并发异常或内存泄漏。

它不是语法糖,也不是框架黑魔法——C# 中的依赖注入(DI)是一种把“谁来创建对象”的控制权交出去的设计实践。核心就一句话:类不自己 new 依赖,而是让外部把依赖塞进来。
为什么非得用 DI?不写 new 就行了?
不用 DI 的典型写法是这样的:
public class OrderService
{
private readonly SqlOrderRepository _repo = new SqlOrderRepository(); // ❌ 自己 new,硬编码
public void Process(Order order) => _repo.Save(order);
}问题立刻浮现:
-
SqlOrderRepository一换(比如改成FileOrderRepository),你得改OrderService源码 —— 违反开闭原则 - 单元测试时没法塞个
MockOrderRepository进去,测试会真连数据库 —— 测试慢、不稳定、难隔离 - 所有地方都
new同一个类,连接字符串、重试策略等配置散落在各处 —— 难统一管理
DI 怎么做?三步走,缺一不可
不是加个 NuGet 包就叫用了 DI,必须完成这三件事:
-
声明抽象:定义接口,如
IOrderRepository,只管“能做什么”,不管“怎么做” -
注册实现:在
Program.cs里告诉容器:“当有人要IOrderRepository,就给一个SqlOrderRepository实例”,例如:builder.Services.AddScoped(); -
接收依赖:在构造函数里写参数,让容器自动填值,例如:
public OrderService(IOrderRepository repo) { _repo = repo; }
漏掉任意一步,运行时就会抛出 InvalidOperationException: No service for type 'X' has been registered。
生命周期选错,bug 会半夜找你
注册时选的生命周期不是“随便点一个”,它直接决定对象是否共享、线程安全、内存泄漏风险:
-
AddTransient():每次请求都新建 —— 适合无状态工具类(如IMapper),但别用它注册 DbContext -
AddScoped():一次 HTTP 请求内复用(ASP.NET Core 默认作用域)——DbContext必须用这个,否则并发写入会崩 -
AddSingleton():整个应用生命周期只一个实例 —— 适合配置类、缓存管理器,但若内部持有非线程安全资源(如StreamWriter),多线程下大概率出错
最常踩的坑是:把本该 Scoped 的仓储注册成 Singleton,结果数据库上下文跨请求复用,报错 A second operation started on this context。
DI 真正难的不是写那几行注册代码,而是想清楚哪些该抽象、哪些该共享、哪些该隔离 —— 这些判断一旦定错,后期重构成本远高于初期多花十分钟设计接口。










