控制反转(IoC)与依赖注入(DI)核心知识点整理

6 阅读9分钟

控制反转(IoC)与依赖注入(DI)核心知识点整理

本文档围绕.NET环境下的控制反转(IoC)依赖注入(DI) 展开,从核心概念、实现方式、服务生命周期、代码实操到项目实战、批量注册进行了全面讲解,以下是结构化整理内容。

一、核心概念:IoC与DI

1. 控制反转(IoC)

  • 本质:一种设计思想/理论,而非具体实现,核心是对象控制权的转移
  • 控制的内容:对象的创建、属性赋值、对象之间的关系管理。
  • 反转的含义:将开发人员手动管理、创建对象的权限,转移给外部容器,由容器替代开发人员完成对象管理。
  • 解决的问题
    1. 减少开发人员对对象创建、组装细节的关注,专注业务逻辑开发;
    2. 解决代码与具体类的强耦合问题,降低需求变更时的代码修改成本。
  • 传统开发问题:开发人员需手动创建对象(如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. 生命周期选择原则

  1. 无状态的类→单例(Singleton)
  2. ASP.NET Core中,与请求相关的类→范围(Scoped)
  3. 轻量、使用频率低的无状态类→瞬态(Transient)(谨慎使用)。

四、.NET中服务的注册与获取

1. 基础准备

  1. 通过NuGet安装包:Install-Package Microsoft.Extensions.DependencyInjection
  2. 引用命名空间: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. 核心规则

  1. .NET中DI默认采用构造函数注入,为推荐方式;
  2. 依赖的对象建议用readonly修饰,防止后续被重新赋值;
  3. 开发人员仅需声明依赖,无需关心依赖对象的创建过程。

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. 批量注册的核心规则

  1. 过滤规则:仅注册非接口、非抽象类的具体实现类;
  2. 命名规则:按类名后缀匹配(如DAL层以DAO结尾,BLL层以BLL结尾);
  3. 关联规则:将具体实现类与它所实现的接口自动关联注册。

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);
}

八、整体总结

  1. IoC是思想,DI是实现:控制反转的核心是对象控制权转移,依赖注入是最常用的实现方式,服务定位器为补充;
  2. 面向接口编程是核心:服务注册优先使用“接口+实现类”,降低模块耦合,便于替换实现;
  3. 生命周期按需选择:单例(无状态全局)、范围(ASP.NET Core请求)、瞬态(轻量低频率),避免滥用瞬态;
  4. 构造函数注入为推荐:依赖关系明确,可通过readonly保证对象不可变;
  5. 批量注册简化开发:通过反射+扩展方法封装批量注册逻辑,解决服务数量多导致的注册代码繁杂问题;
  6. DI的核心价值解耦,让开发人员专注业务逻辑,需求变更时仅需修改服务注册,无需修改业务代码。