.NET学习-依赖注入、配置系统、日志系统

依赖注入

首先理解"控制反转"的思想

它是把 "创建 / 管理依赖对象的控制权",从 "使用依赖的类内部" 转移到 "外部容器 / 框架"。

生活例子:你要实现吃饭的功能,自己来做饭吃,就相当于控制权在自己手里,要多辣自己都可以控制;而点外卖的话,控制权不在自己手里,称为"控制反转",具体的口味都是由商家决定的。

"依赖注入"是实现"控制反转"的方式

可以这样理解,依赖就是需要的东西,比如美食,将需要的东西注入进来,而不是自己去创建。需要什么直接去要,自己不用实现。

依赖注入代码示例:

先定义一个简单的日志服务:

cs 复制代码
// 1. 服务接口(被依赖的对象)
public interface ILogService
{
    void Log(string message); // 日志方法
}

// 2. 服务实现类
public class ConsoleLogService : ILogService
{
    // 实现:控制台输出日志
    public void Log(string message)
    {
        Console.WriteLine($"【控制台日志】{message}");
    }
}

依赖注入实现:

cs 复制代码
using System;
using Microsoft.Extensions.DependencyInjection; // .NET DI核心命名空间

// 业务类:依赖ILogService(被动接收注入)
public class OrderService
{
    // 1. 声明依赖:通过构造函数告诉容器"我需要ILogService"
    private readonly ILogService _logService;
    public OrderService(ILogService logService)
    {
        _logService = logService; // 容器注入的实例
    }

    // 业务方法:使用依赖的服务
    public void CreateOrder(int orderId)
    {
        _logService.Log($"创建订单:{orderId}"); // 用注入的日志服务
    }
}

class Program
{
    static void Main()
    {
        // 步骤1:注册服务(把服务映射关系添加到容器)
        var services = new ServiceCollection();
        // 注册:ILogService接口 → ConsoleLogService实现类(瞬时生命周期)
        services.AddTransient<ILogService, ConsoleLogService>();
        // 注册业务类OrderService
        services.AddTransient<OrderService>();

        // 步骤2:构建DI容器(IServiceProvider)
        using var serviceProvider = services.BuildServiceProvider();

        // 步骤3:从容器解析OrderService(容器自动注入ILogService)
        var orderService = serviceProvider.GetRequiredService<OrderService>();
        
        // 步骤4:调用业务方法(使用注入的日志服务)
        orderService.CreateOrder(1001);
    }
}

输出结果:

cs 复制代码
【控制台日志】创建订单:1001

💡服务定位器

"服务定位器"是实现"控制反转"思想的另一种方式

核心逻辑是:通过一个「全局可访问的 "定位器"(通常是.NETIServiceProvider)」,让类主动从容器中获取所需的服务实例,而非被动接收注入("依赖注入" 是被动)。

简单说:

  • 依赖注入(DI)是 "容器把服务送上门"(被动接收);
  • 服务定位器是 "类自己去容器里拿服务"(主动获取)。

优点是:灵活(可动态获取服务)

缺点:依赖关系 "隐藏"(代码中看不到类依赖哪些服务)

代码示例:

cs 复制代码
using System;
using Microsoft.Extensions.DependencyInjection;

// 业务类:主动从定位器(IServiceProvider)获取ILogService
public class OrderService
{
    // 1. 持有定位器(IServiceProvider)引用
    private readonly IServiceProvider _serviceLocator;
    public OrderService(IServiceProvider serviceLocator)
    {
        _serviceLocator = serviceLocator; // 注入定位器(仅这一步是DI)
    }

    public void CreateOrder(int orderId)
    {
        // 2. 主动从定位器中获取ILogService(服务定位器核心逻辑)
        var logService = _serviceLocator.GetRequiredService<ILogService>();
        logService.Log($"创建订单:{orderId}");
    }
}

class Program
{
    static void Main()
    {
        // 步骤1:注册服务(和DI示例完全一样)
        var services = new ServiceCollection();
        services.AddTransient<ILogService, ConsoleLogService>();
        services.AddTransient<OrderService>();

        // 步骤2:构建容器(定位器本身)
        using var serviceProvider = services.BuildServiceProvider();

        // 步骤3:解析OrderService(容器注入IServiceProvider)
        var orderService = serviceProvider.GetRequiredService<OrderService>();
        
        // 步骤4:调用业务方法(主动获取日志服务)
        orderService.CreateOrder(1002);
    }
}

相关概念

服务 :即对象(提供特定功能的对象)

在 DI 中通常以「接口 + 实现类」的形式存在(也可直接是具体类)

如上例中,ConsoleLogService 和ILogService 就是服务,具体的说前者是服务实现类,后者是服务接口。

注册服务:将服务映射关系告知容器的过程

核心定义:把「服务接口与实现类的对应关系」、「服务的生命周期」这两类信息,提交给服务容器的操作,相当于给容器 "下达指令"。

核心目的:让服务容器知道 "当需要某个服务接口的实例时,应该创建哪个实现类的对象,以及这个对象的生命周期如何"。

cs 复制代码
// 注册:ILogService(接口)→ ConsoleLogService(实现类),生命周期为瞬态
services.AddTransient<ILogService, ConsoleLogService>();

服务容器:负责管理注册的服务(DI 的 "核心管家")

核心定义:.NET 中对应IServiceProvider(构建后)/IServiceCollection(注册时),是存储所有注册服务信息、管理服务实例创建 / 销毁 / 依赖解析的核心容器。

如上例:运行阶段容器:IServiceProvider

注册阶段容器:ServiceCollection

核心管理职责(不止是 "存储",更核心是 "管理"):

存储:保存所有服务的注册信息(接口 - 实现类映射、生命周期);

创建:根据注册信息,在需要时创建服务实例;

解析:自动解析服务的依赖链(如 A 依赖 B,B 依赖 C,容器会自动创建 C→B→A);

销毁:根据服务生命周期,在合适时机销毁服务实例,释放资源。

查询服务:创建对象及关联对象(也叫 "服务解析")

核心定义:从服务容器中获取已注册服务实例的过程,容器在这个过程中会自动完成「当前服务对象创建」+「关联依赖对象创建与注入」,最终返回可用的服务实例。

核心细节: 不是简单的 "取对象",而是 "按需创建 + 依赖组装":如果服务 A 依赖服务 B,容器会先创建服务 B,再将 B 注入到 A 中,最终返回完整可用的 A 实例;

核心方法:.NET 中常用GetRequiredService<T>()(强制获取,无实例抛异常)、GetService<T>()(可选获取,无实例返回 null)。

cs 复制代码
// 这行代码就是「查询服务」的核心实现
//从IServiceProvider(运行阶段容器)中,查询并获取OrderService类型的服务实例;
var orderService = serviceProvider.GetRequiredService<OrderService>();

对象生命周期:Transient (瞬态);Scoped(范围);Singleton(单例)

  • 核心定义:服务实例的 "存活规则",决定了服务实例的「创建时机」和「销毁时机」,.NET DI 内置 3 种核心生命周期,适配不同场景需求。

  • 三种生命周期核心拆解(通俗化):

    生命周期类型 核心特点(创建 / 销毁) 通俗类比 适用场景
    Transient(瞬态) 每次查询 / 注入都创建新实例,无主动销毁(由 GC 回收) 餐厅的一次性餐具,每次使用都换新的 无状态工具类(如日志、仓储、辅助计算)
    Scoped(范围) 同一 "作用域" 内(如ASP.NET Core 单个请求)创建1 个实例,作用域结束时销毁 餐厅的一桌餐具,同一桌顾客共用一套,用餐结束后回收 ASP.NET Core 请求相关服务(如用户上下文、请求仓储)
    Singleton(单例) 整个应用程序生命周期内只创建1 个实例,应用停止时销毁 餐厅的冰箱,所有厨师共用一个,餐厅关门后才停用 全局共享无状态服务(如配置、缓存、日志工厂)

根据类型获取 / 注册服务:

有两种类型:服务类型 vs 实现类型"

两者的关系:可以 "不同",也可以 "相同" ,推荐不同

如果一个服务是用接口+实现类 实现的,那么服务类型和实现类型分别是接口和实现类

如果一个服务就是 用一个类实现的,那么服务类型和实现类型就均是这个类。

比如之前的日志服务:

ILogService(接口,约定 "要做日志功能");

实现类型:ConsoleLogService(类,实际写日志到控制台)。

代码里的注册就是 "把招牌和干活的人绑定": 运行 // 服务类型(ILogService) → 实现类型(ConsoleLogService) services.AddTransient<ILogService, ConsoleLogService>();

cs 复制代码
// 服务类型(ILogService) → 实现类型(ConsoleLogService)
services.AddTransient<ILogService, ConsoleLogService>();

两者相同(直接注册类,不用接口):如果不写接口,直接注册一个类(比如OrderService): 服务类型 = 实现类型,服务类型和实现类型都是都是OrderService;

cs 复制代码
OrderService services.AddTransient<OrderService>(); 

为什么 "建议面向接口编程(服务类型用接口)"?

用餐厅类比:如果招牌是 "川菜馆"(接口,只说 "做川菜"),实际干活的可以是张师傅、李师傅(不同实现类)------想换厨师,直接换干活的人,不用换门口的招牌(业务代码不用改)。

但如果招牌直接写 "张师傅川菜馆"(服务类型用具体类),想换李师傅就得把招牌也换成 "李师傅川菜馆"(业务代码必须改)。 所以用接口当服务类型,换实现更灵活,业务代码不用动(这就是 "面向接口编程" 的好处)。

".NET 的 DependencyInjection 组件,包含 ServiceLocator 的功能"

.NET 官方的这个 "DependencyInjection 组件"(就是我们一直用的ServiceCollection+IServiceProvider),不止能做 DI(被动注入),还能做服务定位器(主动拿服务)

比如:

cs 复制代码
// 这行就是"服务定位器"的功能(主动从容器拿服务)
var orderService = serviceProvider.GetRequiredService<OrderService>();

所以这个组件是 "一专多能"------ 名字叫 "依赖注入",但把 "服务定位器" 的功能也包进去了,既支持被动注入,也支持主动取服务。

初步使用

.NET 的 "依赖注入" 功能不是所有项目都默认自带的(比如控制台项目),这个命令是从官方 "软件商店"(NuGet)下载并安装 DI 所需的所有工具类(比如ServiceCollectionIServiceProvider),让你的项目能用上 DI。

cs 复制代码
Install-Package Microsoft.Extensions.DependencyInjection

"using + 命名空间" 是告诉 C# 代码:"我要用到刚才安装的 DI 工具包里的类了",这样后续写ServiceCollectionIServiceProvider时,代码才认识这些类型(不会报错)。

cs 复制代码
using Microsoft.Extensions.DependencyInjection

ServiceCollectionIServiceProvider的关系

这是把 "服务清单" 变成 "可用的容器"

  • 先理解ServiceCollection:它是 DI 的 "服务清单"------ 你先把要管理的服务(比如之前的ILogServiceOrderService)都 "登记" 到这个清单里(就像做饭前先列一个 "要准备的食材清单")。
  • 再理解BuildServiceProvider():把这个 "服务清单" 变成能实际用的 DI 容器(IServiceProvider(类比:拿着食材清单去超市,把所有食材买回来,放到厨房储物柜里 ------ 这个储物柜就是 "容器",能直接拿里面的食材做饭)。
  • 最终效果:这个容器(IServiceProvider)可以取出 "清单里登记过的服务"(比如之前代码里用serviceProvider.GetRequiredService<OrderService>()拿服务)。
cs 复制代码
// 步骤3:在"清单"里登记服务
services.AddTransient<ILogService, ConsoleLogService>();
// 步骤3:把"清单"变成可用的容器
using var serviceProvider = services.BuildServiceProvider();
// 步骤3:从容器里取服务(用之前登记的东西)
var orderService = serviceProvider.GetRequiredService<OrderService>();

初步实操

先定义一个服务:由接口和实现类组成:

cs 复制代码
  //服务接口
  public interface ITestService 
  {
      public string Name { get; set; }
      public void SayHi();
  }

  //服务实现类
  public class TestServiceImpl : ITestService
  {
      public string Name { get; set; }
      public void SayHi()
      {
          Console.WriteLine($"Hi,Im{Name}");
      }
  }

普通方法调用这个服务:

cs 复制代码
 static void Main(string[] args)
 {
     ITestService p1 = new TestServiceImpl();
     p1.Name = "Tom";
     p1.SayHi();
     Console.Read();
 }

使用服务定位器:

cs 复制代码
 static void Main(string[] args)
 {
     //初始化服务注册集合
     ServiceCollection services = new ServiceCollection();
     //注册服务(写一个购物清单)
     services.AddTransient<TestServiceImpl>();
     // 构建正式DI容器(将清单物品买来放仓库里)
     using (ServiceProvider serviceProvider = services.BuildServiceProvider())
     {
         //解析服务(从容器取实例)
         TestServiceImpl p1 = serviceProvider.GetService<TestServiceImpl>();
         p1.Name = "boy";
         p1.SayHi();
     }
 }

生命周期-Singleton

cs 复制代码
            services.AddSingleton<TestServiceImpl>();           

             using (ServiceProvider serviceProvider = services.BuildServiceProvider())
            {
                解析服务(从容器取实例)
                TestServiceImpl p1 = serviceProvider.GetService<TestServiceImpl>();
                p1.Name = "boy";
                p1.SayHi();

                TestServiceImpl p2 = serviceProvider.GetService<TestServiceImpl>();
                p2.Name = "jack";
                p2.SayHi();

                p1.SayHi();//如果他的结果为Jack,说明是同一对象
                Console.WriteLine(object.ReferenceEquals(p1, p2));//Ture
                object.ReferenceEquals这个方法用于判断2个实例是否为同一对象
            }

如果是上个生命周期,则为false

三种生命周期-Scope

cs 复制代码
services.AddScoped<TestServiceImpl>();

//Scope的写法有不同处,1、需要创建范围。2、解析的时候,不是直接用容器了,而是用scope中的容器
using (ServiceProvider serviceProvider = services.BuildServiceProvider())
{
    using (IServiceScope scope1 = serviceProvider.CreateScope())//初始化一个范围。
    {
        //TestServiceImpl p3 = serviceProvider.GetService<TestServiceImpl>();//通常是这样写,
        //但在Scope里,是用范围实例.ServiceProvider(注意这个不是实例,是类)
        TestServiceImpl p3 = scope1.ServiceProvider.GetService<TestServiceImpl>();
        p3.Name = "any";
        p3.SayHi();
    }
}

创建了2个scope,验证里面的对象是否为同一对象,结果:不是

cs 复制代码
using (ServiceProvider serviceProvider = services.BuildServiceProvider())
{
    TestServiceImpl t;
    using (IServiceScope scope1 = serviceProvider.CreateScope())//初始化一个范围。
    {
        //TestServiceImpl p3 = serviceProvider.GetService<TestServiceImpl>();//通常是这样写,
        //但在Scope里,是用范围实例.ServiceProvider(注意这个不是实例,是类)
        TestServiceImpl p3 = scope1.ServiceProvider.GetService<TestServiceImpl>();
        p3.Name = "any";
        p3.SayHi();
        t = p3;
    }

    using (IServiceScope scope2 = serviceProvider.CreateScope())//初始化一个范围。
    {
        //TestServiceImpl p3 = serviceProvider.GetService<TestServiceImpl>();//通常是这样写,
        //但在Scope里,是用范围实例.ServiceProvider(注意这个不是实例,是类)
        TestServiceImpl p3 = scope2.ServiceProvider.GetService<TestServiceImpl>();
        p3.Name = "any";
        p3.SayHi();
        Console.WriteLine(object.ReferenceEquals(t, p3));
    }
}

实现 IDisposable 的类,容器会自动调用 Dispose

通俗讲

IDisposable 是 "资源清洁工" 接口 ------ 如果你的类用了文件、数据库连接 这些 "用完必须手动关闭" 的资源,就实现这个接口,把 "关资源" 的代码写在Dispose方法里。

而.NET DI 容器是个 "贴心管家":如果这个类是Scoped/Transient生命周期,等它 "离开自己的使用范围"(比如 Scoped 的作用域结束、Transient 用完被回收),容器会自动帮你调用 Dispose 方法 ,不用你手动写using或者调用 Dispose 了。

cs 复制代码
using Microsoft.Extensions.DependencyInjection;
using System;

namespace DILifeCycle
{
    // 实现IDisposable的"文件服务"(用了文件资源,需要清理)
    public class FileService : IDisposable
    {
        public FileService()
        {
            Console.WriteLine("FileService被创建了(打开了文件资源)");
        }

        // 资源清理逻辑写在这里
        public void Dispose()
        {
            Console.WriteLine("FileService的Dispose被自动调用了(关闭了文件资源)");
        }
    }

    class Program
    {
        static void Main()
        {
            var services = new ServiceCollection();
            // 注册为Scoped(作用域内生效)
            services.AddScoped<FileService>();

            using var provider = services.BuildServiceProvider();
            using (var scope = provider.CreateScope()) // 作用域开始
            {
                var fileService = scope.ServiceProvider.GetRequiredService<FileService>();
                Console.WriteLine("正在使用FileService...");
            } // 作用域结束 → 容器自动调用Dispose

            Console.Read();
        }
    }
}
cs 复制代码
FileService被创建了(打开了文件资源)
正在使用FileService...
FileService的Dispose被自动调用了(关闭了文件资源)

别在 "长生命周期对象" 里引用 "短生命周期对象"

通俗讲

生命周期有 "长短排名":Singleton(全局唯一,跟程序同生共死) > Scoped(比如一个请求活一个) > Transient(用一次就扔)

如果 "长寿的"(比如 Singleton)引用了 "短命的"(比如 Scoped),等 "短命的" 被销毁后,"长寿的" 还拿着它的引用 ------ 再用就会拿到 "已经死了的对象",ASP.NET Core 里会直接抛异常阻止这种危险操作。

3、不要在长生命周期的对象中引用比它短的生命周期的对象。在ASP.NET Core 中,这样做默认会抛异常。

4、生命周期的选择:如果类无状态,建议为 Singleton;如果类有状态,且有 Scope 控制,建议为 Scoped,因为通常这种 Scope 控制下的代码都是运行在同一个线程中的,没有并发修改的问题;在使用 Transient 的时候要谨慎。

5、.NET 注册服务的重载方法很多,看着文档琢磨吧。

正式操作

配置系统

日志系统

相关推荐
jz_ddk5 小时前
[学习] 卫星导航的码相位与载波相位计算
学习·算法·gps·gnss·北斗
华清远见成都中心6 小时前
人工智能要学习的课程有哪些?
人工智能·学习
hssfscv6 小时前
Javaweb学习笔记——后端实战2_部门管理
java·笔记·学习
白帽子黑客罗哥6 小时前
不同就业方向(如AI、网络安全、前端开发)的具体学习路径和技能要求是什么?
人工智能·学习·web安全
于越海7 小时前
材料电子理论核心四个基本模型的python编程学习
开发语言·笔记·python·学习·学习方法
我命由我123457 小时前
开发中的英语积累 P26:Recursive、Parser、Pair、Matrix、Inset、Appropriate
经验分享·笔记·学习·职场和发展·求职招聘·职场发展·学习方法
北岛寒沫8 小时前
北京大学国家发展研究院 经济学原理课程笔记(第二十三课 货币供应与通货膨胀)
经验分享·笔记·学习
知识分享小能手8 小时前
Ubuntu入门学习教程,从入门到精通,Ubuntu 22.04中的Java与Android开发环境 (20)
java·学习·ubuntu
好奇龙猫8 小时前
【大学院-筆記試験練習:数据库(データベース問題訓練) と 软件工程(ソフトウェア)(10)】
学习
wdfk_prog8 小时前
[Linux]学习笔记系列 -- [fs][proc]
linux·笔记·学习