控制反转(IoC)与依赖注入(DI)核心知识点整理
本文档围绕.NET环境下的控制反转(IoC) 和依赖注入(DI) 展开,从核心概念、实现方式、服务生命周期、代码实操到项目实战、批量注册进行了全面讲解,以下是结构化整理内容。
一、核心概念:IoC与DI
1. 控制反转(IoC)
- 本质:一种设计思想/理论,而非具体实现,核心是对象控制权的转移。
- 控制的内容:对象的创建、属性赋值、对象之间的关系管理。
- 反转的含义:将开发人员手动管理、创建对象的权限,转移给外部容器,由容器替代开发人员完成对象管理。
- 解决的问题:
- 减少开发人员对对象创建、组装细节的关注,专注业务逻辑开发;
- 解决代码与具体类的强耦合问题,降低需求变更时的代码修改成本。
- 传统开发问题:开发人员需手动创建对象(如
new SqlConnection()),需掌握类的构造、依赖细节,代码耦合度高。
2. 依赖注入(DI)
- 本质:控制反转思想的核心实现方式(另一种是服务定位器),也是实际开发中最常用的方式。
- 核心逻辑:容器根据类的依赖关系,自动为类注入所需的依赖对象,开发人员仅需声明“需要什么对象”,无需关心对象创建过程。
- 与服务定位器的对比:依赖注入由容器主动赋值,服务定位器需开发人员手动调用方法获取对象,前者更简洁,为首选方案。
3. 关键术语
- 容器:负责服务的注册、创建、管理、注入的框架,.NET中核心接口为
IServiceCollection,默认实现ServiceCollection。 - 服务:注册到容器中的对象/类型,分为服务接口和具体实现类,推荐面向接口注册服务。
二、IoC的两种实现方式
1. 服务定位器
- 原理:框架提供
ServiceLocator类,通过其GetService方法获取所需对象,对象的创建逻辑由框架封装。 - 伪代码示例:
IDbConnection conn = ServiceLocator.GetService<IDbConnection>(); conn.Open(); // 面向接口编程,无需关心conn的具体实现 - 特点:需手动调用方法获取服务,适用于依赖注入无法满足需求的场景。
2. 依赖注入
- 原理:通过属性/构造函数声明依赖,容器在创建类对象时,自动为依赖的属性/构造函数参数赋值。
- 属性注入示例:
class Demo { // 容器自动为该属性注入IDbConnection实现类对象 public IDbConnection Conn { get; set; } public void InsertDB() { IDbCommand cmd = Conn.CreateCommand(); } } - .NET默认方式:构造函数注入(推荐,依赖关系更明确,可通过
readonly修饰依赖对象防止篡改)。
三、服务的生命周期
容器中注册的服务有3种生命周期,核心区别是获取服务时创建新对象,还是复用已有对象,.NET中通过AddTransient/AddScoped/AddSingleton分别注册。
1. 瞬态(Transient)
- 规则:每次请求服务时,容器都会创建一个新对象。
- 缺点:频繁创建对象会占用更多内存,需谨慎使用。
- 适用场景:无状态、轻量且使用频率低的对象。
2. 范围(Scoped)
- 规则:同一范围内多次请求,共享同一个对象;不同范围则创建新对象。
- 默认范围:在ASP.NET Core MVC/WebAPI中,一次HTTP请求即为一个范围。
- 控制台程序:需开发人员通过
sp.CreateScope()手动创建范围。 - 适用场景:同一请求/范围内需要共享数据的场景(如ASP.NET Core的请求上下文)。
3. 单例(Singleton)
- 规则:全局范围内仅创建一个对象,所有请求共享。
- 优点:减少对象创建次数,节省系统资源。
- 注意事项:单例对象必须是无状态的(无属性/成员变量),避免多线程并发修改导致的问题。
- 适用场景:无状态、全局通用的工具类/服务类。
4. 生命周期选择原则
- 无状态的类→单例(Singleton);
- ASP.NET Core中,与请求相关的类→范围(Scoped);
- 轻量、使用频率低的无状态类→瞬态(Transient)(谨慎使用)。
四、.NET中服务的注册与获取
1. 基础准备
- 通过NuGet安装包:
Install-Package Microsoft.Extensions.DependencyInjection; - 引用命名空间:
using Microsoft.Extensions.DependencyInjection。
2. 服务注册
核心是通过IServiceCollection的扩展方法注册,支持具体类注册和接口+实现类注册(推荐),同时指定生命周期。
(1)基础注册方式
ServiceCollection services = new ServiceCollection();
// 注册具体类,瞬态生命周期
services.AddTransient<TestServiceImpl>();
// 注册接口+实现类,单例生命周期(推荐,面向接口编程)
services.AddSingleton<ITestService, TestServiceImpl>();
// 手动创建对象注册(适用于需要传递构造参数的场景)
services.AddSingleton(typeof(ITestService), new TestServiceImpl());
(2)注册核心规则
- 注册的类型需与后续获取服务的类型一致,否则无法获取;
- 推荐接口+实现类注册,降低耦合,便于替换实现。
3. 服务获取
通过IServiceCollection.BuildServiceProvider()创建服务定位器(ServiceProvider),再通过其方法获取服务,ServiceProvider实现IDisposable,需用using释放资源。
| 方法 | 特点 | 适用场景 |
|---|---|---|
GetService<T>() | 泛型方法,找不到服务时返回null | 不确定服务是否注册的场景 |
GetRequiredService<T>() | 泛型方法,找不到服务时抛出异常 | 确定服务已注册的场景(推荐) |
GetServices<T>() | 返回实现该接口的所有服务,类型为IEnumerable<T> | 一个接口有多个实现类的场景 |
非泛型GetService(Type) | 需手动类型转换,灵活性低 | 动态获取服务类型的场景 |
核心示例
using (ServiceProvider sp = services.BuildServiceProvider())
{
// 推荐:获取接口对应的服务,找不到则抛异常
ITestService service = sp.GetRequiredService<ITestService>();
service.Name = "test";
service.SayHi();
// 获取一个接口的所有实现类
IEnumerable<ITestService> allServices = sp.GetServices<ITestService>();
foreach (var s in allServices) s.SayHi();
}
五、依赖注入的核心实现:构造函数注入
1. 核心定义
- 依赖:类A的构造函数包含类B的参数,说明A依赖B(
class A{ public A(B b){} }); - 注入:实例化A时,先实例化B并传入A的构造函数的过程;
- 依赖注入:容器通过反射解析类的依赖关系,自动创建并注入依赖对象的过程。
2. 核心规则
- .NET中DI默认采用构造函数注入,为推荐方式;
- 依赖的对象建议用
readonly修饰,防止后续被重新赋值; - 开发人员仅需声明依赖,无需关心依赖对象的创建过程。
3. 代码示例(多层依赖注入)
// 配置接口与实现
interface IConfig { string GetValue(string name); }
class ConfigImpl : IConfig { public string GetValue(string name) => $"获取了{name}的配置信息"; }
// 日志接口与实现(依赖IConfig)
interface ILog { void Log(string message); }
class LogImpl : ILog
{
private readonly IConfig _config;
// 构造函数注入IConfig,容器自动传入实现类对象
public LogImpl(IConfig config) => _config = config;
public void Log(string message)
{
Console.WriteLine(_config.GetValue("日志"));
Console.WriteLine($"日志信息:{message}");
}
}
// 业务类(依赖ILog)
class Controller
{
private readonly ILog _log;
public Controller(ILog log) => _log = log;
public void Test()
{
Console.WriteLine("开始业务");
_log.Log("业务执行中");
Console.WriteLine("业务结束");
}
}
// 注册并使用
ServiceCollection services = new ServiceCollection();
services.AddScoped<Controller>();
services.AddScoped<ILog, LogImpl>();
services.AddScoped<IConfig, ConfigImpl>();
using (ServiceProvider sp = services.BuildServiceProvider())
{
var c = sp.GetRequiredService<Controller>();
c.Test(); // 容器自动注入所有依赖,无需手动创建LogImpl/ConfigImpl
}
4. 依赖注入的核心优势:解耦
当需要替换依赖的实现时,仅需修改服务注册代码,无需修改业务类代码。 示例:将配置从本地读取改为数据库读取,仅需新增实现类并修改注册:
// 新增数据库配置实现类
class DBConfigImpl : IConfig { public string GetValue(string name) => $"从数据库获取{name}的配置信息"; }
// 仅修改注册代码,业务类(LogImpl/Controller)无需任何修改
services.AddScoped<IConfig, DBConfigImpl>();
六、项目实战:分层架构中的DI应用
以UI层→BLL层→DAL层→数据库的传统分层项目为例,实现DI的全流程应用,核心是每层通过构造函数注入下层接口,面向接口编程。
1. 项目结构
- Model层:实体类(如
UserInfo); - DAL层:数据访问层,操作数据库,依赖
IDbConnection; - BLL层:业务逻辑层,处理业务,依赖DAL层接口;
- UI层:表现层,调用BLL层,负责服务注册与初始化。
2. 核心代码实现
(1)Model层:实体类
public class UserInfo { public int Id { get; set; } public string? Name { get; set; } public string? Password { get; set; } }
(2)DAL层:数据访问(依赖IDbConnection)
// 接口
public interface IUserDAO { UserInfo GetUserInfo(string userName); }
// 实现类(构造函数注入IDbConnection)
public class UserDAO : IUserDAO
{
private readonly IDbConnection _conn;
public UserDAO(IDbConnection conn) => _conn = conn;
public UserInfo GetUserInfo(string userName)
{
// 借助SqlHelper操作数据库,返回UserInfo
using (var dt = SqlHelper.ExecuteQuery(_conn, $"select * from userInfo where Name='{userName}'"))
{
if (dt.Rows.Count <= 0) return null;
var row = dt.Rows[0];
return new UserInfo { Id = (int)row["Id"], Name = (string)row["Name"], Password = (string)row["Password"] };
}
}
}
(3)BLL层:业务逻辑(依赖IUserDAO)
// 接口
public interface IUserBLL { bool CheckLogin(string userName, string password); }
// 实现类(构造函数注入IUserDAO)
public class UserBLL : IUserBLL
{
private readonly IUserDAO _userDAO;
public UserBLL(IUserDAO userDAO) => _userDAO = userDAO;
public bool CheckLogin(string userName, string password)
{
var user = _userDAO.GetUserInfo(userName);
return user != null && user.Password == password;
}
}
(4)UI层:服务注册与调用
ServiceCollection services = new ServiceCollection();
// 注册数据库连接(动态创建,注入连接字符串并打开)
services.AddScoped<IDbConnection>(sp =>
{
string connStr = "Data Source=.;Initial Catalog=TestDB;uid=sa;pwd=123456";
var conn = new SqlConnection(connStr);
conn.Open();
return conn;
});
// 注册DAL和BLL层服务
services.AddScoped<IUserDAO, UserDAO>();
services.AddScoped<IUserBLL, UserBLL>();
// 调用业务方法
using (ServiceProvider sp = services.BuildServiceProvider())
{
var userBll = sp.GetRequiredService<IUserBLL>();
bool isLogin = userBll.CheckLogin("admin", "123456");
Console.WriteLine(isLogin ? "登录成功" : "登录失败");
}
3. 实战优势
当需要更换数据库(如SqlServer→MySQL)时,仅需修改IDbConnection的注册代码,DAL/BLL/UI层的业务代码无需任何修改,彻底解耦。
七、高级技巧:服务的批量注册
当项目服务数量较多时,手动逐个注册会导致代码繁杂、难以维护,需实现批量注册,核心思路是通过反射遍历程序集,按规则自动注册服务。
1. 批量注册的核心规则
- 过滤规则:仅注册非接口、非抽象类的具体实现类;
- 命名规则:按类名后缀匹配(如DAL层以
DAO结尾,BLL层以BLL结尾); - 关联规则:将具体实现类与它所实现的接口自动关联注册。
2. 最优实现:扩展方法封装
将批量注册逻辑封装为IServiceCollection的扩展方法,实现代码复用、统一管理,简化UI层的注册代码。
(1)编写扩展方法
public static class RegisterServiceExtension
{
/// <summary>
/// 批量注册服务
/// </summary>
/// <param name="services">服务容器</param>
/// <param name="assembly">要扫描的程序集</param>
/// <param name="endWith">类名后缀(如DAO/BLL)</param>
/// <param name="serviceLifetime">生命周期</param>
/// <returns>服务容器</returns>
public static IServiceCollection BatchRegisterService(this IServiceCollection services, Assembly assembly, string endWith, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)
{
// 按规则过滤具体实现类
var typeList = assembly.GetTypes().Where(t => !t.IsInterface && !t.IsAbstract && t.Name.EndsWith(endWith));
var dict = new Dictionary<Type, Type[]>();
// 遍历类,获取其实现的所有接口
foreach (var type in typeList)
{
var interfaces = type.GetInterfaces();
dict.Add(type, interfaces);
}
// 按生命周期注册类与接口的关联
if (dict.Keys.Count > 0)
{
foreach (var instanceType in dict.Keys)
{
foreach (var interfaceType in dict[instanceType])
{
switch (serviceLifetime)
{
case ServiceLifetime.Scoped: services.AddScoped(interfaceType, instanceType); break;
case ServiceLifetime.Singleton: services.AddSingleton(interfaceType, instanceType); break;
case ServiceLifetime.Transient: services.AddTransient(interfaceType, instanceType); break;
}
}
}
}
return services;
}
}
(2)UI层调用扩展方法(简化注册)
ServiceCollection services = new ServiceCollection();
// 注册数据库连接
services.AddScoped<IDbConnection>(sp =>
{
string connStr = "Data Source=.;Initial Catalog=TestDB;uid=sa;pwd=123456";
var conn = new SqlConnection(connStr);
conn.Open();
return conn;
});
// 批量注册DAL层(DAO后缀)和BLL层(BLL后缀)服务,范围生命周期
services.BatchRegisterService(Assembly.Load("DAL"), "DAO");
services.BatchRegisterService(Assembly.Load("BLL"), "BLL");
// 调用业务方法
using (ServiceProvider sp = services.BuildServiceProvider())
{
var userBll = sp.GetRequiredService<IUserBLL>();
bool isLogin = userBll.CheckLogin("admin", "123456");
Console.WriteLine(isLogin);
}
八、整体总结
- IoC是思想,DI是实现:控制反转的核心是对象控制权转移,依赖注入是最常用的实现方式,服务定位器为补充;
- 面向接口编程是核心:服务注册优先使用“接口+实现类”,降低模块耦合,便于替换实现;
- 生命周期按需选择:单例(无状态全局)、范围(ASP.NET Core请求)、瞬态(轻量低频率),避免滥用瞬态;
- 构造函数注入为推荐:依赖关系明确,可通过
readonly保证对象不可变; - 批量注册简化开发:通过反射+扩展方法封装批量注册逻辑,解决服务数量多导致的注册代码繁杂问题;
- DI的核心价值:解耦,让开发人员专注业务逻辑,需求变更时仅需修改服务注册,无需修改业务代码。