在 C# 中理解与实践依赖注入(DI)
什么是 DI?
依赖注入(Dependency Injection,简称 DI),是一种通过将对象所依赖的组件(或服务)从外部传入,而不是在内部直接创建,从而实现控制反转(IoC)的设计模式。
- 控制反转(IoC):将对象的创建、配置、生命周期管理等责任,从类内部“反转”到外部容器或调用者。
- 依赖注入(DI):IoC 的具体实现方式之一,通过构造函数、属性或方法将依赖传入。
为什么需要 DI?
-
降低耦合
- 传统写法里,类内部直接
new
出依赖对象,两个类强绑定在一起。 - DI 让类只依赖接口或抽象,不关心具体实现,方便后期扩展或替换。
- 传统写法里,类内部直接
-
提高可测试性
- 通过接口编程,可以在单元测试时注入“假实现”(Mock),无需依赖真实数据库、网络等。
-
集中管理依赖
- 使用容器在启动时统一注册所有服务,后续只需从容器获取,无需手动
new
。
- 使用容器在启动时统一注册所有服务,后续只需从容器获取,无需手动
常见的注入方式
-
构造函数注入(Constructor Injection)
最常用:依赖通过构造函数参数传入,确保依赖在对象创建时就可用。 -
属性注入(Property Injection)
通过 public 属性设置依赖,适合可选依赖或循环依赖场景,但不如构造注入直观。 -
方法注入(Method Injection)
在调用某个方法时,将依赖作为参数传入,使用场景较少。
手动实现构造函数注入示例
// 定义消息服务接口
public interface IMessageService
{
void Send(string to, string message);
}
// 邮件发送服务
public class EmailService : IMessageService
{
public void Send(string to, string message)
{
// 模拟发送邮件
Console.WriteLine($"[Email] 发给 {to}:{message}");
}
}
// 短信发送服务
public class SmsService : IMessageService
{
public void Send(string to, string message)
{
// 模拟发送短信
Console.WriteLine($"[SMS] 发给 {to}:{message}");
}
}
// 通知管理器,依赖 IMessageService
public class NotificationManager
{
private readonly IMessageService _messageService;
// 构造函数注入
public NotificationManager(IMessageService messageService)
{
_messageService = messageService;
}
public void Notify(string user, string content)
{
// 调用接口,无需关心具体实现
_messageService.Send(user, content);
}
}
// 程序入口:手动组装依赖
class Program
{
static void Main(string[] args)
{
// 选择具体实现:更换为 SmsService 时,只需改这一行
IMessageService service = new EmailService();
var manager = new NotificationManager(service);
manager.Notify("alice@example.com", "你好,Alice!");
}
}
使用 .NET 内置容器
从 .NET Core / .NET 6 开始,微软提供了轻量级的 DI 容器,位于 Microsoft.Extensions.DependencyInjection
包中:
using Microsoft.Extensions.DependencyInjection;
class Program
{
static void Main(string[] args)
{
// 1. 创建 ServiceCollection,用于注册服务
var services = new ServiceCollection();
// 2. 注册服务:接口 -> 实现
services.AddTransient<IMessageService, EmailService>(); // 每次请求创建新实例
services.AddTransient<NotificationManager>(); // 注册 NotificationManager,自身依赖会被自动注入
// 3. 构建 ServiceProvider
var provider = services.BuildServiceProvider();
// 4. 从容器中获取 NotificationManager 并使用
var manager = provider.GetService<NotificationManager>();
manager.Notify("bob@example.com", "你好,Bob!");
}
}
AddTransient<TInterface, TImplementation>()
:每次请求都创建新的实例AddSingleton<TInterface, TImplementation>()
:全局单例,第一次创建后复用同一实例AddScoped<TInterface, TImplementation>()
:每个作用域(如 ASP.NET Core 的一次 HTTP 请求)内单例
在 WPF 中使用 DI
在 WPF 应用中,一般在 App.xaml.cs 中配置容器,然后通过构造函数注入到窗口或 ViewModel:
// App.xaml.cs
public partial class App : Application
{
private readonly ServiceProvider _serviceProvider;
public App()
{
// 1. 创建 ServiceCollection
var services = new ServiceCollection();
// 2. 注册服务与窗口
services.AddSingleton<IMessageService, EmailService>(); // 单例消息服务
services.AddTransient<MainWindow>(); // 主窗口注册为瞬时服务
// 3. 构建 ServiceProvider
_serviceProvider = services.BuildServiceProvider();
}
protected override void OnStartup(StartupEventArgs e)
{
// 从容器中获取 MainWindow 并显示
var mainWindow = _serviceProvider.GetService<MainWindow>();
mainWindow.Show();
base.OnStartup(e);
}
}
// MainWindow.xaml.cs
public partial class MainWindow : Window
{
private readonly IMessageService _messageService;
// 构造函数注入 IMessageService
public MainWindow(IMessageService messageService)
{
InitializeComponent();
_messageService = messageService;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// 使用注入的服务发送消息
_messageService.Send("user@example.com", "来自 WPF 的消息");
}
}
小结
- 依赖注入(DI):将组件的创建和管理交给外部容器,实现控制反转(IoC)。
- 主要优点:
- 降低模块间耦合
- 提高单元测试可 mock 性
- 集中管理依赖、统一生命周期
- 常见注入方式:
- 构造函数注入(Constructor Injection)
- 属性注入(Property Injection)
- 方法注入(Method Injection)
- .NET 内置容器:
- 使用
ServiceCollection
注册服务(Transient / Scoped / Singleton) - 调用
BuildServiceProvider()
构建容器并自动注入
- 使用
- WPF 集成要点:
- 在
App.xaml.cs
中初始化容器 - 将窗口和 ViewModel 注册到容器
- 在窗口或 ViewModel 构造函数中直接接收依赖
- 在
在 Prism 中使用依赖注入
Prism 是一个经典的 WPF/Xamarin MVVM 框架,自带对 Unity、DryIoc、Autofac 等容器的支持。以下示例基于 Prism.DryIoc:
// App.xaml.cs
public partial class App : PrismApplication
{
protected override Window CreateShell()
{
// 从容器中解析 MainWindow 并返回
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// 注册服务:接口 -> 实现
containerRegistry.Register<IMessageService, EmailService>();
// 注册视图与 ViewModel 自动关联
containerRegistry.RegisterForNavigation<MainView, MainViewModel>();
}
}
// MainViewModel.cs
public class MainViewModel : BindableBase
{
private readonly IMessageService _messageService;
public DelegateCommand SendCommand { get; }
// 构造函数注入 IMessageService
public MainViewModel(IMessageService messageService)
{
_messageService = messageService;
SendCommand = new DelegateCommand(OnSend);
}
private void OnSend()
{
_messageService.Send("prism@example.com", "来自 Prism 的消息");
}
}
PrismApplication
会在启动时自动构建容器RegisterTypes
方法中使用IContainerRegistry
注册所有依赖Container.Resolve<T>()
或构造函数自动注入
小贴士
- 选择容器:Prism 推荐使用 DryIoc、Unity 等成熟容器;简单项目可直接用 .NET 内置容器。
- 模块化:Prism 支持模块化加载,IModule 接口中也可注册服务。
- 性能考虑:大型项目下,DryIoc 性能优;Autofac 功能丰富但相对较重。
在 MVVM Toolkit 中使用 DI
// App.xaml.cs
public partial class App : Application
{
private IServiceProvider _provider;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// 配置 ServiceCollection
var services = new ServiceCollection();
services.AddSingleton<IMessageService, EmailService>(); // 单例服务
services.AddTransient<MainViewModel>(); // 注册 ViewModel
services.AddTransient<MainWindow>(sp => // 注册窗口并在构造时注入 VM
{
return new MainWindow
{
DataContext = sp.GetRequiredService<MainViewModel>()
};
});
_provider = services.BuildServiceProvider();
// 从容器中获取窗口并显示
var window = _provider.GetRequiredService<MainWindow>();
window.Show();
}
}
// MainViewModel.cs
public partial class MainViewModel : ObservableObject
{
private readonly IMessageService _messageService;
// 构造函数注入
public MainViewModel(IMessageService messageService)
{
_messageService = messageService;
}
[ICommand] // CommunityToolkit.Mvvm 源码生成命令
private void Send()
{
_messageService.Send("toolkit@example.com", "来自 MVVM Toolkit 的消息");
}
}
-
使用 Microsoft.Extensions.DependencyInjection 管理生命周期
-
ObservableObject + [ICommand] 简化属性和命令的定义
-
在窗口创建时,将 MainViewModel 注入到 DataContext