前面介绍过 什么是控制反转(IoC)?什么是依赖注入(DI)?以及实现原理
在 C# 中,依赖注入(DI)的实现方式主要分为手动注入 和通过 IoC 容器注入 (如 .NET 自带的 Microsoft.Extensions.DependencyInjection)。以下是具体代码示例,涵盖常用场景和最佳实践。
一、手动依赖注入(基础示例)
手动注入不依赖第三方容器,直接通过代码传递依赖,适合简单场景,核心是构造函数注入(最推荐的方式)。
1. 构造函数注入(推荐)
csharp
cs
using System;
// 1. 定义抽象依赖(接口)
public interface IMessageSender
{
void Send(string message);
}
// 2. 实现具体依赖(邮件发送)
public class EmailSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine($"[邮件发送] {message}");
}
}
// 3. 实现具体依赖(短信发送)
public class SmsSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine($"[短信发送] {message}");
}
}
// 4. 依赖方(通知服务):通过构造函数接收依赖
public class NotificationService
{
private readonly IMessageSender _messageSender;
// 构造函数注入:依赖由外部传入,且用 readonly 确保不可变
public NotificationService(IMessageSender messageSender)
{
_messageSender = messageSender ?? throw new ArgumentNullException(
nameof(messageSender), "依赖不能为空"); // 校验依赖,避免空引用
}
public void NotifyUser(string username)
{
_messageSender.Send($"用户 {username} 已收到通知");
}
}
// 5. 调用:手动注入依赖
class Program
{
static void Main()
{
// 手动创建依赖实例
IMessageSender emailSender = new EmailSender();
// IMessageSender smsSender = new SmsSender(); // 可切换为短信发送
// 注入到依赖方
NotificationService notificationService = new NotificationService(emailSender);
notificationService.NotifyUser("张三");
// 输出:[邮件发送] 用户 张三 已收到通知
}
}
优势:
- 依赖在对象创建时就必须传入,确保对象初始化后即可正常工作(避免空引用)。
- 依赖不可变(
readonly),避免运行时被篡改。
2. 属性注入(不推荐,仅特殊场景使用)
属性注入通过公共属性传递依赖,适合 "可选依赖"(非必须的功能),但可能导致对象创建后依赖未初始化的问题。
csharp
cs
public class NotificationService
{
// 属性注入:依赖通过属性设置(通常有默认值或允许为null)
public IMessageSender MessageSender { get; set; } = new EmailSender(); // 默认值
public void NotifyUser(string username)
{
if (MessageSender == null)
throw new InvalidOperationException("未设置消息发送器");
MessageSender.Send($"用户 {username} 已收到通知");
}
}
// 调用
class Program
{
static void Main()
{
var service = new NotificationService();
service.MessageSender = new SmsSender(); // 通过属性注入依赖
service.NotifyUser("李四");
// 输出:[短信发送] 用户 李四 已收到通知
}
}
注意:属性注入可能导致 "对象已创建但依赖未设置" 的风险,除非有明确理由(如框架限制),否则优先用构造函数注入。
二、使用 .NET 自带的 DI 容器(Microsoft.Extensions.DependencyInjection)
在 .NET Core/.NET 5+ 中,官方提供了 Microsoft.Extensions.DependencyInjection 容器,是企业级开发的首选。需先通过 NuGet 安装包(一般项目默认已引用):Install-Package Microsoft.Extensions.DependencyInjection
1. 基本用法(注册 + 解析)
csharp
cs
using Microsoft.Extensions.DependencyInjection;
using System;
// 复用上面的 IMessageSender、EmailSender、SmsSender、NotificationService
class Program
{
static void Main()
{
// 1. 创建服务容器
var serviceCollection = new ServiceCollection();
// 2. 注册服务(关键步骤:告诉容器"抽象 -> 具体实现"的映射)
// 注册 IMessageSender,指定实现为 EmailSender
serviceCollection.AddSingleton<IMessageSender, EmailSender>();
// 注册 NotificationService(容器会自动注入其依赖 IMessageSender)
serviceCollection.AddSingleton<NotificationService>();
// 3. 构建服务提供器(容器的具体实现)
var serviceProvider = serviceCollection.BuildServiceProvider();
// 4. 从容器解析服务(自动处理依赖链)
var notificationService = serviceProvider.GetRequiredService<NotificationService>();
// 5. 使用服务
notificationService.NotifyUser("王五");
// 输出:[邮件发送] 用户 王五 已收到通知
}
}
2. 服务生命周期(3 种核心类型)
容器通过 "生命周期" 管理服务实例的创建和销毁,核心有 3 种:
| 生命周期 | 说明 | 适用场景 |
|---|---|---|
| Transient | 每次请求(GetService)创建新实例 |
轻量级、无状态服务(如工具类) |
| Scoped | 每个 "作用域" 内创建一个实例(如 Web 请求) | 数据库上下文(DbContext) |
| Singleton | 整个应用生命周期内只创建一个实例 | 全局配置、缓存服务 |
示例:验证生命周期差异
csharp
cs
using Microsoft.Extensions.DependencyInjection;
using System;
// 测试服务:记录实例ID,观察是否为同一实例
public class TestService
{
public Guid Id { get; } = Guid.NewGuid(); // 实例化时生成唯一ID
}
class Program
{
static void Main()
{
var services = new ServiceCollection();
// 注册3种生命周期的服务
services.AddTransient<TestService>(); // Transient
services.AddScoped<TestService>(); // Scoped(需在作用域内解析)
services.AddSingleton<TestService>(); // Singleton
var provider = services.BuildServiceProvider();
// 1. 测试 Transient:每次获取都是新实例
Console.WriteLine("Transient:");
var t1 = provider.GetRequiredService<TestService>();
var t2 = provider.GetRequiredService<TestService>();
Console.WriteLine($"t1.Id == t2.Id? {t1.Id == t2.Id}"); // 输出:False
// 2. 测试 Scoped:同一作用域内是同一实例,不同作用域不同
Console.WriteLine("\nScoped:");
using (var scope1 = provider.CreateScope()) // 创建作用域1
{
var s1 = scope1.ServiceProvider.GetRequiredService<TestService>();
var s2 = scope1.ServiceProvider.GetRequiredService<TestService>();
Console.WriteLine($"scope1内 s1.Id == s2.Id? {s1.Id == s2.Id}"); // True
}
using (var scope2 = provider.CreateScope()) // 创建作用域2
{
var s3 = scope2.ServiceProvider.GetRequiredService<TestService>();
Console.WriteLine($"scope2内 s3.Id 与 scope1的s1不同? {true}"); // 必然不同
}
// 3. 测试 Singleton:全局唯一实例
Console.WriteLine("\nSingleton:");
var s1 = provider.GetRequiredService<TestService>();
var s2 = provider.GetRequiredService<TestService>();
Console.WriteLine($"s1.Id == s2.Id? {s1.Id == s2.Id}"); // 输出:True
}
}
3. 依赖链解析(多层依赖)
容器会自动解析 "依赖的依赖"(递归解析),无需手动处理多层依赖关系。
csharp
cs
using Microsoft.Extensions.DependencyInjection;
using System;
// 第一层依赖:日志服务
public interface ILogger { void Log(string msg); }
public class ConsoleLogger : ILogger { public void Log(string msg) => Console.WriteLine($"[日志] {msg}"); }
// 第二层依赖:用户仓储(依赖日志)
public interface IUserRepository
{
void Add(string username);
}
public class UserRepository : IUserRepository
{
private readonly ILogger _logger;
// 依赖 ILogger
public UserRepository(ILogger logger)
{
_logger = logger;
}
public void Add(string username)
{
_logger.Log($"用户 {username} 已添加到数据库");
}
}
// 第三层依赖:用户服务(依赖用户仓储)
public class UserService
{
private readonly IUserRepository _userRepository;
// 依赖 IUserRepository(其内部又依赖 ILogger)
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public void RegisterUser(string username)
{
_userRepository.Add(username);
}
}
// 容器自动解析依赖链
class Program
{
static void Main()
{
var services = new ServiceCollection();
// 注册所有依赖(只需注册抽象与实现的映射,容器自动处理依赖链)
services.AddSingleton<ILogger, ConsoleLogger>();
services.AddSingleton<IUserRepository, UserRepository>();
services.AddSingleton<UserService>();
var provider = services.BuildServiceProvider();
var userService = provider.GetRequiredService<UserService>();
userService.RegisterUser("赵六");
// 输出:[日志] 用户 赵六 已添加到数据库(依赖链:UserService -> UserRepository -> ConsoleLogger)
}
}
三、ASP.NET Core 中的依赖注入(实际应用)
在 ASP.NET Core 中,DI 容器已内置,通常在 Program.cs 中注册服务,在控制器 / 服务中通过构造函数注入使用。
1. 注册服务(Program.cs)
csharp
cs
var builder = WebApplication.CreateBuilder(args);
// 注册控制器(默认已包含)
builder.Services.AddControllers();
// 注册自定义服务
builder.Services.AddScoped<IMessageSender, EmailSender>(); // 作用域生命周期(适合Web请求)
builder.Services.AddScoped<NotificationService>();
var app = builder.Build();
// 中间件配置...
app.MapControllers();
app.Run();
2. 在控制器中注入服务
csharp
cs
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class NotificationController : ControllerBase
{
private readonly NotificationService _notificationService;
// 控制器构造函数注入:容器自动传入 NotificationService(及其依赖)
public NotificationController(NotificationService notificationService)
{
_notificationService = notificationService;
}
[HttpGet("{username}")]
public IActionResult Notify(string username)
{
_notificationService.NotifyUser(username);
return Ok("通知已发送");
}
}
总结
- 核心方式:构造函数注入是首选,确保依赖不可变且初始化完整。
- 容器使用 :.NET 自带的
Microsoft.Extensions.DependencyInjection是主流选择,通过 "注册 - 解析" 管理服务,自动处理依赖链。 - 生命周期:根据服务特性选择 Transient/Scoped/Singleton,避免因生命周期错误导致的问题(如在 Singleton 中注入 Scoped 服务)。
以上示例覆盖了从基础手动注入到框架级容器使用的场景,可根据项目规模选择合适的方式。