c# 依赖注入 di 是什么

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 真正难的不是写那几行注册代码,而是想清楚哪些该抽象、哪些该共享、哪些该隔离 —— 这些判断一旦定错,后期重构成本远高于初期多花十分钟设计接口。