依赖注入(Dependency Injection, DI) 是一种控制反转(Inversion of Control, IoC) 的具体实现方式,它是一种设计模式 ,用于解耦对象之间的依赖关系 ,使代码更可测试、可维护、可扩展。
✅ 一句话理解:
"不要自己 new 依赖,而是让别人(容器)把依赖'送'给你。"
🎯 为什么需要依赖注入?
❌ 传统写法的问题(紧耦合)
public class OrderService
{
private readonly EmailSender _emailSender = new EmailSender(); // 硬编码依赖
public void PlaceOrder()
{
// ... 处理订单
_emailSender.Send("订单已创建"); // 无法替换、无法测试
}
}
问题:
- ❌ 难以测试 :无法 mock
EmailSender(比如测试时不想真发邮件) - ❌ 难以修改 :想换成
SmsSender?必须改代码! - ❌ 违反开闭原则:对修改开放,对扩展封闭
✅ 依赖注入的核心思想
高层模块(如
OrderService)不直接创建低层模块(如IEmailSender),而是通过接口 + 外部传入的方式获得依赖。
步骤:
- 定义抽象(接口)
- 依赖抽象,而非具体实现
- 由外部(DI 容器)负责创建并注入依赖
💡 三种依赖注入方式
1️⃣ 构造函数注入(推荐!)
public interface IEmailSender
{
void Send(string message);
}
public class SmtpEmailSender : IEmailSender { ... }
public class MockEmailSender : IEmailSender { ... } // 用于测试
// OrderService 不关心谁实现 IEmailSender
public class OrderService
{
private readonly IEmailSender _emailSender;
// 依赖通过构造函数"注入"
public OrderService(IEmailSender emailSender)
{
_emailSender = emailSender;
}
public void PlaceOrder()
{
_emailSender.Send("订单已创建");
}
}
2️⃣ 属性注入(Setter 注入)
public class OrderService
{
public IEmailSender EmailSender { get; set; } // 通过属性设置
public void PlaceOrder() => EmailSender?.Send("...");
}
⚠️ 少用:依赖可能为 null,生命周期难控制。
3️⃣ 方法注入
public void PlaceOrder(IEmailSender sender)
{
sender.Send("...");
}
适用于临时依赖或策略模式。
🧩 DI 容器:自动管理依赖的"工厂"
你不需要手动 new OrderService(new SmtpEmailSender()),而是交给 DI 容器 自动完成:
在 .NET 中使用内置 DI(ASP.NET Core 默认支持)
1. 注册服务(Program.cs)
var builder = WebApplication.CreateBuilder(args);
// 注册依赖:当需要 IEmailSender 时,给 SmtpEmailSender 实例
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<OrderService>(); // 自动注入 IEmailSender
2. 使用(Controller 或其他服务)
public class OrderController : ControllerBase
{
private readonly OrderService _orderService;
// DI 容器自动创建 OrderService 并注入所需依赖
public OrderController(OrderService orderService)
{
_orderService = orderService;
}
[HttpPost]
public IActionResult CreateOrder()
{
_orderService.PlaceOrder();
return Ok();
}
}
🔁 依赖生命周期(.NET DI)
| 生命周期 | 说明 | 适用场景 |
|---|---|---|
| Transient | 每次请求都创建新实例 | 轻量、无状态服务 |
| Scoped | 每个 HTTP 请求(或作用域)一个实例 | Entity Framework DbContext |
| Singleton | 整个应用生命周期只有一个实例 | 配置、日志记录器 |
services.AddTransient<IMyService, MyService>(); // 每次 new
services.AddScoped<IMyService, MyService>(); // 每请求一个
services.AddSingleton<IMyService, MyService>(); // 全局唯一
✅ 依赖注入的好处
| 优势 | 说明 |
|---|---|
| 解耦 | 高层模块不依赖具体实现,只依赖接口 |
| 可测试性 | 轻松用 mock 对象替换真实依赖(单元测试必备) |
| 可维护性 | 修改/替换实现无需改动使用方代码 |
| 可扩展性 | 新增实现只需注册,无需修改业务逻辑 |
| 配置灵活 | 开发/测试/生产环境可注入不同实现 |
🆚 依赖注入 vs 服务定位器(Service Locator)
| 方式 | 依赖注入 | 服务定位器 |
|---|---|---|
| 代码 | public MyClass(IService s) |
var s = ServiceLocator.GetService<IService>() |
| 耦合度 | 低(显式声明依赖) | 高(隐藏依赖,运行时才知道) |
| 可测试性 | 高 | 低 |
| 推荐度 | ✅ 强烈推荐 | ❌ 反模式(隐藏依赖) |
📌 依赖注入让依赖关系"显式化",这是良好设计的关键!
⚠️ 常见误区
❌ 误区 1:DI 就是用框架
DI 是设计思想,即使不用框架,手动传参也是 DI(只是麻烦)。
❌ 误区 2:所有类都要注入
简单的 DTO、实体类、工具类(如
DateTime.Now)不需要 DI。
❌ 误区 3:循环依赖
A 依赖 B,B 又依赖 A → 设计有问题!应重构(引入中介者或拆分职责)。
✅ 最佳实践
- 优先使用构造函数注入
- 依赖接口,而非具体类
- 避免在构造函数中做复杂逻辑(保持轻量)
- 合理选择生命周期(别把 Scoped 当 Singleton 用)
- 不要在静态方法中使用 DI(破坏可测试性)
🧠 总结
| 关键点 | 说明 |
|---|---|
| 本质 | 控制反转(IoC)的一种实现 |
| 核心 | "不要自己 new,让别人给你" |
| 方式 | 构造函数注入(首选)、属性注入、方法注入 |
| 工具 | .NET 内置 DI 容器(IServiceCollection) |
| 价值 | 解耦、可测试、可维护、可扩展 |
💡 记住 :
"依赖注入不是为了用框架,而是为了写出干净、灵活、可测试的代码。"
问题:
什么是依赖注入框架?
他们是自动创建依赖项并且将他们注入到需要他们的对象中的机制。他们是可配置的
好处是什么
依赖注入将类与依赖项解耦
那总有一个地方new,那是谁new
这是个极其关键且深刻的问题!👏
你说得完全对:依赖不会凭空出现,总得有人 new 出来 。在依赖注入(DI)体系中,这个"谁"就是 DI 容器(Dependency Injection Container) ------ 也叫 IoC 容器(Inversion of Control Container)。
✅ 答案一句话:
DI 容器负责
new所有注册的服务,并自动把它们"注入"到需要的地方。
你写的代码不再手动 new 依赖 ,而是把"创建对象"的责任委托给 DI 容器。
🔍 详细解释:DI 容器是如何工作的?
步骤 1️⃣:你在启动时注册依赖关系
// Program.cs(ASP.NET Core)
var builder = WebApplication.CreateBuilder(args);
// 告诉容器:"当我需要 IEmailSender 时,请给我一个 SmtpEmailSender 实例"
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
// 告诉容器:"OrderService 是一个服务,它的依赖由你自动解决"
builder.Services.AddScoped<OrderService>();
📌 这里你没有
new任何对象 ,只是声明规则。
步骤 2️⃣:当程序运行时,DI 容器按需创建对象
假设你的控制器这样写:
public class OrderController : ControllerBase
{
public OrderController(OrderService orderService) // ← 构造函数注入
{
// ...
}
}
当用户发起 HTTP 请求时:
-
框架(如 ASP.NET Core)发现需要
OrderController -
它问 DI 容器:"请给我一个
OrderController实例" -
DI 容器检查
OrderController的构造函数,发现它需要OrderService -
容器再检查
OrderService的构造函数,发现它需要IEmailSender -
容器根据注册规则,执行:
var emailSender = new SmtpEmailSender(); // ← 这里是容器在 new! var orderService = new OrderService(emailSender); var controller = new OrderController(orderService); -
最终把完整的
OrderController实例交给框架处理请求
💡 整个过程对你透明 ------ 你只写了接口和类,容器自动完成所有
new和组装。
🧩 谁是"根对象"?------ Composition Root(组合根)
在 DI 体系中,第一个被容器创建的对象 叫做 Composition Root(组合根)。
- 在 ASP.NET Core 中:组合根是 Controller、Razor Page、Minimal API 处理函数
- 在 控制台应用 中:通常是
Main方法中手动从容器解析的第一个服务 - 在 WPF/WinForms 中:可能是
MainWindow或App类
✅ 只有组合根是由框架或你显式从容器"拉取"(Resolve)的,其他所有依赖都是容器自动"推送"(Inject)的。
💻 控制台应用示例(手动触发 DI)
// 1. 注册服务
var services = new ServiceCollection();
services.AddScoped<IEmailSender, SmtpEmailSender>();
services.AddScoped<OrderService>();
// 2. 构建容器
var serviceProvider = services.BuildServiceProvider();
// 3. 【关键】从容器"拉取"根对象(这里是你唯一接触容器的地方)
var orderService = serviceProvider.GetRequiredService<OrderService>();
// 4. 使用(orderService 内部的 SmtpEmailSender 已自动注入)
orderService.PlaceOrder();
🔸
GetRequiredService<T>()就是那个"new"的起点 ------ 但它是容器在new,不是你在new!
🌐 类比理解
| 角色 | 现实类比 |
|---|---|
| 你(开发者) | 餐厅老板,只负责写菜单(定义接口和业务逻辑) |
| DI 容器 | 厨房总管,知道每道菜怎么做(new 对象),并自动配好配料 |
| 客户端代码 | 顾客,点菜(请求服务),不关心厨房怎么做的 |
| Composition Root | 服务员,从厨房端出完整菜品(根对象) |
你不需要进厨房切菜(
new SmtpEmailSender()),厨房总管(DI 容器)全包了!
⚠️ 重要原则:不要在业务代码中调用容器!
❌ 错误做法(服务定位器反模式):
public class OrderService
{
public void PlaceOrder()
{
// 不要这样做!隐藏了依赖,破坏可测试性
var sender = serviceProvider.GetService<IEmailSender>();
sender.Send("...");
}
}
✅ 正确做法:
- 只在 Composition Root(如 Program.cs、Startup)配置容器
- 只在 Composition Root 或框架入口处解析根对象
- 业务代码永远通过构造函数/参数接收依赖,绝不主动找容器要
✅ 总结
| 问题 | 答案 |
|---|---|
总得有人 new,是谁? |
DI 容器 (如 .NET 的 ServiceProvider) |
我在哪看到 new? |
看不到 ------ 容器在幕后自动完成 |
我需要手动 new 吗? |
不需要(除了极少数 POCO 对象如 DTO) |
| 第一个对象谁创建? | 框架或你在 Composition Root 从容器解析 |
| 这有什么好处? | 解耦、可测试、生命周期自动管理 |
🧠 记住 :
"你负责设计依赖关系,容器负责实现它。"这就是依赖注入的精髓 ------ 把"创建"的控制权交出去,换来代码的灵活性和可维护性。
如果用prism框架呢?
非常好的问题!当你在使用 Prism 框架 (常用于 WPF、Uno Platform、MAUI 等 MVVM 应用)时,你已经站在一个成熟的依赖注入体系之上 。Prism 内置了对 DI 容器的支持,并帮你管理了"谁来 new"这个关键问题。
✅ Prism 中的 DI 机制概览
Prism 不强制绑定某个 DI 容器 ,而是通过抽象接口 IContainerExtension 支持多种容器,例如:
- Unity(Prism 早期默认)
- DryIoc(Prism 8+ 推荐默认)
- Autofac
- Microsoft.Extensions.DependencyInjection(.NET 原生 DI)
📌 从 Prism 8 开始,默认容器是 DryIoc(轻量、快速、功能强)。
🔧 在 Prism 中,"谁来 new"?
答案是:Prism 的 DI 容器(如 DryIoc) + Prism 框架本身。
具体流程如下:
1️⃣ 你在 App.xaml.cs 中注册服务
public partial class App : PrismApplication
{
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// 注册你的服务
containerRegistry.Register<IEmailService, SmtpEmailService>();
containerRegistry.RegisterForNavigation<MainView, MainViewModel>(); // 注册页面和 VM
}
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>(); // ← 这里是第一个 "new" 的起点!
}
}
✅
RegisterTypes就是你的 Composition Root(组合根) ------ 所有依赖关系在这里声明。
2️⃣ Prism 自动创建 ViewModel 并注入依赖
假设你的 MainViewModel 长这样:
public class MainViewModel : BindableBase
{
private readonly IEmailService _emailService;
public MainViewModel(IEmailService emailService) // ← 构造函数注入
{
_emailService = emailService;
}
public void OnSendEmail()
{
_emailService.Send("Hello from Prism!");
}
}
当你导航到 MainView 时:
-
Prism 发现
MainView关联了MainViewModel -
Prism 调用 DI 容器 :
container.Resolve<MainViewModel>() -
容器发现
MainViewModel需要IEmailService -
容器根据注册规则,执行:
var emailService = new SmtpEmailService(); // ← 容器在 new! var viewModel = new MainViewModel(emailService); // ← 容器在 new! -
Prism 把
viewModel自动赋值给MainView.DataContext
💡 你全程没有写任何
new,Prism + DI 容器全包了!
🧩 Prism 中的关键角色
| 组件 | 职责 |
|---|---|
IContainerRegistry |
你在 RegisterTypes 中使用的注册接口 |
IContainerProvider / Container |
提供 Resolve<T>() 方法(慎用!) |
ViewModelLocator(自动) |
Prism 默认启用,自动将 View 和 ViewModel 关联并注入 |
| Navigation Service | 导航时自动从容器解析目标 ViewModel |
⚠️ 注意事项 & 最佳实践
✅ 1. 不要手动 new ViewModel
❌ 错误:
// 不要这样做!依赖无法注入
DataContext = new MainViewModel();
✅ 正确:
- 使用
RegisterForNavigation<MainView, MainViewModel>() - 或让 Prism 自动按命名约定匹配(
MainView→MainViewModel)
✅ 2. 避免在 ViewModel 中调用 Container.Resolve()
❌ 反模式(服务定位器):
public void SomeMethod()
{
var service = Container.Resolve<IMyService>(); // 隐藏依赖!
}
✅ 正确:始终通过构造函数注入
public MainViewModel(IMyService myService) // 显式声明依赖
{
_myService = myService;
}
✅ 3. 理解生命周期
Prism 默认注册方式:
Register<T>→ Transient(每次解析都新建)RegisterSingleton<T>→ Singleton(全局唯一)
对于 ViewModel,Prism 每次导航都会创建新实例(除非你自定义导航逻辑)。
✅ 4. 如何注入到非 ViewModel 类?
比如一个 OrderProcessor 服务:
// 注册
containerRegistry.Register<IOrderProcessor, OrderProcessor>();
// 在 ViewModel 中使用
public class MainViewModel
{
public MainViewModel(IOrderProcessor processor) { ... }
}
只要它被容器管理的对象依赖,就会自动注入。
🛠️ 如果你想查看"谁在 new"
你可以替换为 .NET 原生 DI 容器(更透明):
// 安装 Prism.Microsoft.DependencyInjection
protected override IContainerExtension CreateContainerExtension()
{
return PrismContainerExtension.Create(new ServiceCollection());
}
然后你在 RegisterTypes 中其实就是在配置 IServiceCollection,和 ASP.NET Core 一样!
✅ 总结:Prism 中的 "new" 在哪?
| 场景 | 谁在 new |
你是否需要关心 |
|---|---|---|
| 服务(IEmailService) | DI 容器(DryIoc/.NET DI) | ❌ 不需要 |
| ViewModel | Prism + DI 容器(通过导航触发) | ❌ 不需要 |
| View(Window/Page) | WPF/MAUI 框架(但 DataContext 由 Prism 设置) | ❌ 不需要 |
| App 启动时的 Shell | 你在 CreateShell() 中调用 Container.Resolve<MainWindow>() |
✅ 这是你唯一接触"解析"的地方 |
🧠 记住 :
Prism 是你的"DI 管家" ------ 它不仅帮你注册、解析、注入,还把 MVVM、导航、模块化全串起来了。你只需专注业务逻辑,把
new的烦恼交给 Prism!