C#摸鱼实录——IoC与DI案例详解

IoC(控制反转)与DI(依赖注入)


开一个新的模块哈,在这个模块里面,我们主要讲一个东西如何使用,尽量不纠结概念,简单过过

之前老是被人说,是不是过于偏向于学院派了,所以从现在开始,我们将只关注能不能用

这个模块里面,我想讲的,大多数是在实际项目中常用的东西,例如一些NuGet 包,一个语法,或者某种设计模式
不过不过多描述概念了,不讲官方那些罗里吧嗦的概念,只需要理解他是什么鬼东西,干什么的,怎么用即可
大抵就是学院派和江湖派的区别吧

顺便后面我要是忘记这个东西怎么用了,还可以回来看看文档,顺便,这就是我未来AI的蒸馏对象 我蒸馏我自己

然后,为什么要进行这么古老的学习方式,废话,这年头AI快把初级员工的路堵死了,
不来点古法编程,抽象能力提升很慢的,用了AI几个月,发现初级迈向中级,你不古法编程就等死吧
而且上班摸鱼时间一大把,系统性的学习学习怎么了,打发时间也挺好的,然后深入学习一下IoC的思想

废话少说,进入正题


一.DI依赖注入 — 概念

[!TIP]

如果你不想看文字,或者觉得我这一块讲的不是特别明白的,想看视频教学的话
推荐一位up做的关于依赖注入的教学视频,大概30分钟左右的教学,
只不过后面几个视频初学者容易看不懂
【.Net-依赖注入】从依赖说起_哔哩哔哩_bilibili

很多人可能经常使用依赖注入,但是不知道他叫什么,DI是什么鬼东西,其实看一眼代码就了解了 不懂你就再看一眼


1.什么是依赖(Dependency)?

  • 一个对象要工作,需要另一个对象的帮助,没有另一个对象就完成不了

  • /// 因为产品需要零件A,所以产品依赖于零件A
    /// 即:零件A就是产品的依赖
    public class 零件A
    {
        public int GetID() => 100;
    }
    
    public class 产品
    {
        private readonly 零件A _a;
    
        public 产品(零件A a) => _a = a;
    }
    

2.什么是注入(Injection)?

  • 把对象交给另一个对象使用

    • // 通过对象product使用了对象a
      
      var a = new 零件A();
      var product = new 产品(a);
      
      

3.什么是依赖注入(Dependency Injection)?

  • 依赖注入 = 依赖 + 注入

    • 即:对象所需要的依赖由外部提供,而不是自己创建
  • 下面是依赖注入的一点基本概念,结合上面的内容,已经写的非常清楚了,就不再过多阐述

  • // 一个用于示例的空类DbService
    public interface IDbService
    {
        void Insert();
    }
    
    public class DbService : IDbService
    {
        public DbService() { }
        public void Insert() => Console.WriteLine("=====================================");
    }
    
    
    /// <summary>
    /// 传统写法 - 不使用依赖注入(模块之间强依赖,耦合度高)
    /// </summary>
    public class NO_DI
    {
        // 🌱钱没给够,你自己new吧
        private DbService _db = new DbService();    
    
        public void Save() => _db.Insert();
    }
    
    /// <summary>
    /// 使用依赖注入(松散解耦)
    /// </summary>
    public class Yes_DI
    {
        private readonly DbService _db;
    
        // 🌱钱给够了,直接从外部“注入”
        public Yes_DI(DbService db) => _db = db;
    
        public void Save() => _db.Insert();
    }
    
    
    /// <summary>
    /// 依赖注入常用三种方式(但是基本上还是以构造注入为主)
    /// </summary>
    
    /// <summary>
    /// 1.1构造注入
    /// </summary>
    public class 构造注入
    {
        private readonly DbService _db;
    
        public 构造注入(DbService db) => _db = db;
    
        // var a = new A();
        // var demo = new 构造注入(a);
    }
    
    /// <summary>
    ///  1.2.属性注入
    /// </summary> 
    public class 属性注入
    {
        public 属性注入() {   }
    
        public DbService DB { get; set; } = null!;
    
        // 属性注入 demo = new 属性注入();
        // demo.DB = new DbService();
    }
    
    /// <summary>
    ///  1.3.方法注入
    /// </summary> 
    public class 方法注入
    {
        public 方法注入() { }
        public void Execute(DbService db)   {   }
    
        // 方法注入.Execute(new DbService());
    }
    
    

二.IoC(控制反转)— 概念

1.什么是控制(Control)?

  • 谁决定对象如何产生和使用

    • 控制权:决定某件事情如何进行的权力
      • 在IoC中,特指:创建什么对象,什么时候创建,对象如何创建对象的决定权
      • 看不懂就看下面的例子,一眼秒懂
  • // 产品控制着零件A的创建
    // 即:产品拥有创建零件A的控制权
    
    // 缺点,产品和零件A已经绑死了,高度耦合,扩展等死,后人挠头,直骂屎山
    public class 零件A {	}
    
    public class 产品
    {
        private readonly 零件A _a;
    
        public 产品()
        {
            _a = new 零件A();
        }
    }
    

2.什么是反转(Inversion)?

  • 反转 = 原来的方向反过来了

    • 原本由A负责的事情,改由B负责
  • 在IoC中通常指:控制权发生变化,由内部控制变成外部控制

  • public class 零件A {	}
    
    //=======================================
    // 内部控制
    public class 产品
    {
        private readonly 零件A _a;
    
        public 产品()
        {
            _a = new 零件A();
        }
    }
    
    //=======================================
    // 外部控制
    public class 产品
    {
        private readonly 零件A _a;
    
        public 产品(零件A a)
        {
            _a = a;
        }
    }
    

3.什么是控制反转(Inversion of Control)?

  • 控制方向被反过来了,所以叫控制反转

  • 控制反转:原本由对象自己掌握的控制权,转移给了外部对象或容器

    • 但是需要注意的是,IoC是一种思想,它并不是某种具体的实现
    • 换句话说,DI是IoC的一种实现方式,依赖注入就是使用控制反转的思想
      • 即:依赖注入(DI)是实现IoC最常见的方式之一
  • # 原来:产品内部控制零件A
    产品
     ↓	# 控制
    零件A
    #==================================================
    # 现在:外部同时控制零件和产品,然后把零件A交给产品使用
    外部
     ↓  # 控制
    零件A
    
    外部
     ↓  # 控制
    产品
    
    

三.DI容器 —— 具体使用示例

  • 在此之前我们先添加个控制反转(IoC)的NuGet包,这个是官方给的实现控制反转的容器

    • 你可以选择在NuGet包管理器 → 管理解决方案的NuGet程序包中搜索Microsoft.Extensions.DependencyInjection下载

    • 只不过我还是喜欢命令行操作下载,这很酷,并且非常快捷,难道不是吗

    • # 扩展 -> NuGet包管理器 -> 程序包管理控制台 -> 输入下面的命令行 
      dotnet add package Microsoft.Extensions.DependencyInjection
      

  • 然后,再添加几个一会示例中要使用的类,为了更好的理解,这里我使用中文来更好的演示

    • /// 这里我们使用 Nuget顶级包Microsoft.Extensions.DependencyInjection(官方DI容器) 来实现IOC操作
      
      /// <summary>
      /// 零件接口
      /// </summary>
      public interface 零件接口
      {
          int GetID();
      }
      
      /// <summary>
      /// 零件A
      /// </summary>
      public class 零件A : 零件接口
      {
          public int ID { get; set; } = 1001;
      
          public int GetID() => ID;
      }
      
      /// <summary>
      /// 零件B
      /// </summary>
      public class 零件B : 零件接口
      {
          public int ID { get; set; } = 2002;
      
          public int GetID() => ID;
      }
      
      /// <summary>
      /// 产品
      /// </summary>
      public class 产品
      {
          private readonly 零件接口 _part;
      
          public 产品(零件接口 part)
          {
              _part = part;
          }
      
          public void ShowInfo()
          {
              Console.WriteLine($"产品使用的零件ID:{_part.GetID()}");
          }
      }
      
      
      

DI容器使用的4个基本步骤

  • 使用容器的四个步骤基本上就是:创建 -> 注册 -> 构建 -> 调用

    • # DI容器使用的4个步骤
      1. 创建容器生成器	# ServiceCollection
      2. 注册服务		# AddXXX
      3. 构建容器		# BuildServiceProvider
      4. 获取服务		# GetService
      
      
  • 这里直接上代码,后面依次解说

    •     /// 不使用IoC容器
          零件A part = new 零件A();
          产品 产品1 = new 产品(part);
          产品1.ShowInfo();
      
      //=================================================================
      
          /// 使用IoC容器:创建 -> 注册 -> 构建 -> 调用
          /* 一.创建容器生成类(服务集合) */
          ServiceCollection containerBuilder = new ServiceCollection();
      
          /* 二.注册容器中的服务信息(注册实例) —— 服务类型,实现类型,生命周期 */
          containerBuilder.AddSingleton<产品>();
          containerBuilder.AddTransient<零件接口, 零件A>();
      
          /* 三.生成容器 */
          IServiceProvider container = containerBuilder.BuildServiceProvider();
          containerBuilder.MakeReadOnly();    // 将生成类集合声明为只读,后续再添加(Add)服务会进行报错
      
          // containerBuilder.AddSingleton<产品>();	// => 使用MakeReadOnly后报错
      
          /* 四.服务调用(从容器获取对象) */
          产品? product = container.GetService<产品>();
          产品 product2 = container.GetRequiredService<产品>();
      
      

1.创建容器生成器(ServiceCollection)

  • 创建一个服务集合,用于保存所有服务注册信息

这没什么好讲的,你完全不需要关心这个是什么,反正是创建服务集合就对了

    /* 一.创建容器生成类(服务集合) */
    ServiceCollection containerBuilder = new ServiceCollection();

2.注册服务(AddXXX)

  • 本质上是告诉容器,使用什么服务类型,什么实现类型,生命周期是什么
    • 需要什么服务
    • 创建什么对象
    • 使用什么生命周期

2.1 三种注册服务:单例,瞬时,作用域

    /* 二.注册容器中的服务信息(注册实例) —— 服务类型,实现类型,生命周期 */
    containerBuilder.AddSingleton<产品>();
    containerBuilder.AddTransient<零件接口, 零件A>();

	// 三种注册服务
    // 1.AddSingleton(单例):只创建一个单例
    // 2.AddTransient(瞬时):每次获取都创建一个新对象(每次调用都是一个新的对象)
    // 3.AddScoped(作用域):多用于Web服务(同一个会话,是同一个服务),WPF中很少使用

    // 1)单例
    // 服务对象 = 实现对象
    containerBuilder.AddSingleton<产品>();

    // 2)瞬时
    // (1)AddTransient<服务类型>()  等价于  AddTransient<服务类型, 实现类型>(),但是服务类型 = 实现类型
    // (2)AddTransient<服务类型, 实现类型>()  =>  服务类型通常为接口或者基类
    containerBuilder.AddTransient<零件A>();
    containerBuilder.AddTransient<零件接口, 零件A>();

    // 3)作用域(不会Web我就偷懒不讲了哈)
    // AddScoped<接口或父类, 实现类>()

    // 尝试注册 - 用法和上面一样,效果 -> 如果没注册就注册,注册了就通过
    // services.AddSingleton();
    // services.AddScoped();
    // services.AddTransient();

2.2 三种常用注册方式

  • 这里只讲述三种最常见的注册方式:类型注册,实例注册和工厂注册

    • 注册方式 示例 容器负责创建? 常用程度
      类型注册 AddXXX<服务类型>()
      (服务类型 = 实现类型)
      ⭐⭐⭐⭐⭐
      AddXXX<服务类型, 实现类型>() ⭐⭐⭐⭐
      实例注册 AddXXX(obj) ⭐⭐⭐
      工厂注册 AddXXX(sp => ...) 部分负责 ⭐⭐


0)三种注册方式详细总结
对比项 类型注册 实例注册 工厂注册
典型语法 AddXXX<T>()
AddXXX<TService, TImplementation>()
AddXXX(obj) AddXXX(sp => ...)
注册内容 类型信息 已存在的对象实例 对象创建方法(Lambda)
对象何时创建 获取服务时由容器创建 注册前已创建 获取服务时执行工厂函数创建
谁决定如何创建对象 容器 屏幕前苦逼的你 屏幕前苦逼的你
容器是否负责创建 部分负责
容器是否管理生命周期
是否支持依赖注入 ❌(对象已创建)
是否可以获取容器中的其它服务 自动注入 ✅(通过 sp.GetRequiredService()
适用场景 日常开发最常用 已存在对象、第三方对象 复杂初始化、特殊参数、条件创建
常用程度 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐
WPF使用频率 极高 偶尔 偶尔

1)类型注册:容器负责创建
  • 类型注册有两种:

    • 1.服务类型 = 注册类型

    • // 服务类型 = 实现类型
      AddXXX<服务类型>()
      
      • 这是WPF中最常用的注册方式

      • services.AddSingleton<产品>();
        services.AddTransient<零件A>();
        
        // 等价于
        services.AddSingleton<产品, 产品>();
        services.AddTransient<零件A, 零件A>();
        
        
    • 2.服务类型 ≠ 注册类型

    • AddXXX<服务类型, 实现类型>()
      
      • 有博客也叫这种方式为接口 / 基类注册

      • // 接口注册
        services.AddTransient<ILogger, FileLogger>();
        // 基类注册
        services.AddSingleton<零件Base, 零件A>();
        

2)实例注册:你先创建,再交给容器
  • 实例注册主要针对于已经存在的对象,直接使用存在的的实例

    • 容器不能使用已经存在的实例

    • 容器不会创建已经存在的实例(实例已经创建完成,因此容器不再负责创建)

    • # 类型注册:使用容器注册产品
      容器
       ↓
      new 产品()
      
      #========================================
      # 实例注册:将产品实例交给容器
      new 产品()
       ↓
      容器
      

  • 代码示例:

  • SerialPort port = new SerialPort("COM1", 115200);
    
    services.AddSingleton(port);
    

3)工厂注册:你告诉容器以后怎么创建
  • 工厂注册,实际上就是告诉容器,不要使用默认的方法,使用我提供给你的方法

    • 所以有其他博客说,工厂注册的使用时机:需要 复杂初始化,需要配置参数,根据条件创建对象时

    • [!CAUTION]

      • 注册工厂时并不会立即执行 Lambda 表达式
        • Lambda 只是被容器保存起来,
        • 当获取服务(调用服务)时,容器才会执行该 Lambda 来创建对象
  • containerBuilder.AddSingleton<零件A>();
    
    /// 这里是sp指IServiceProvider(当前容器)
    containerBuilder.AddSingleton<产品>(sp =>
    {
        // 现在这个大括号里面的内容就是你自定义的对象创建过程
        
        var part = sp.GetRequiredService<零件A>();
        return new 产品(part);
    });
    
    
  • 获取产品
     ↓
    执行工厂函数
     ↓
    从容器获取零件A
     ↓
    创建并且返回产品
    

3.构建容器(BuildServiceProvider)

  • 这个也不需要多讲,但是有一个特别需要注意的地方

  • [!IMPORTANT]

    • BuildServiceProvider() 会根据当前注册信息生成真正的 DI 容器

    • 换句话说,前面的创建集合,还并没有生成一个真正的DI容器

      • 在调用之前:只有注册信息

      • 调用之后:才能获取服务

    /* 三.生成容器 */
    IServiceProvider container = containerBuilder.BuildServiceProvider();
    containerBuilder.MakeReadOnly();    // 将生成类集合声明为只读,后续再添加服务会进行报错

    // containerBuilder.AddSingleton<产品>();	//=> 使用MakeReadOnly后报错

4.获取服务(GetService)

  • 一般情况下,我们会获取不带键的服务(如下),但是现在官方给了我们一种比较新的写法(带键)
/* 四.服务调用(从容器获取对象) */

// 不带键
产品? product = container.GetService<产品>();
产品 product2 = container.GetRequiredService<产品>();
  • 为什么需要带键?同一个服务类型,可能对应多个实现对象,我想指定实现对象,怎么办?

    • services.AddSingleton<IProtocol, ModbusProtocol>();
      services.AddSingleton<IProtocol, OpcUaProtocol>();
      
      // ......
      
      // 两个实现对象,容器该怎么选择呢?会选择最后一个注册的实现
      // 如果我想要选择中间的或者第一个怎么办呢?
      // 所以出现了带键的服务,注册时给它一个标签,获取时,指定对应标签,那就完事
      var protocol = provider.GetRequiredService<IProtocol>();
      
      

4.1 不带键的服务

1)GetService
  • Get-获取,Service-服务,GetService-获取服务,但是你没说是不是必须的

    • 有就给,没有就算了(返回 null)
  • # 如果产品存在 => 返回对象
    # 如果产品不存在 => 返回 null
    产品? product = container.GetService<产品>();
    

2)GetRequiredService
  • Required-必须的,今天你要是不把服务交出来,这个程序就别想跑了!

    • 必须要有,没有就直接报错
  • # 如果产品存在 => 返回对象
    # 如果产品不存在 => 返回 直接抛异常
    产品 product = container.GetRequiredService<产品>();
    
    

4.2 带键的服务

说白了,和不带键的服务用法基本上一致,就是在注册时给它加了个标签

1)GetKeyedService
  • /// 注册
    containerBuilder.AddKeyedSingleton<产品>("产品");
    
    // .......
    
    /// 获取服务
    // 获取产品
    产品 product = container.GetRequiredKeyedService<产品>("产品");
    

2)GetRequiredKeyedService
  • /// 注册
    containerBuilder.AddKeyedSingleton<零件接口, 零件A>("零件A");
    
    // .......
    
    /// 获取服务
    // 获取零件
    零件接口 part = container.GetRequiredKeyedService<零件接口>("零件A");
    
    

四.流程图

  • 感谢ChatGPT为我们生成了一张总的流程图(其实是我懒,不想画)

  • IoC(思想)
     ↓
    DI(实现方式)
     ↓
    DI容器
    
    # ================================
    
    1.创建容器
     ↓
    ServiceCollection
    
    2.注册服务
     ↓
    AddSingleton
    AddTransient
    AddScoped
    
    3.构建容器
     ↓
    BuildServiceProvider
    
    4.获取服务
     ↓
    GetRequiredService
    GetService
    
    

五.实际案例

1.回顾

但是在此之前,我们先回顾一下内容,不然——
你不会以为就这么完了吗,我有说过结束了吗,那和我以前的博客有什么区别?

注册(AddXXX)
 	↓
向容器登记对象创建规则

调用(GetXXX)
 	↓
让容器按照规则创建并返回对象

//========================================================
    
// 参考前面章节使用的代码(三.DI容器中的几个类)
// 去掉中间的步骤,单独使用这两组代码(注册+调用),会出现什么问题?
/* 第一组 */
services.AddSingleton<产品>();
产品 product = container.GetRequiredService<产品>();

/* 第二组 */
services.AddTransient<零件接口, 零件A>();
零件接口? part = container.GetService<零件接口>();

// 问:为什么这里的服务调用是 获取"零件接口"的对象 ,为什么不可以是使用"零件"的对象?
// 别瞎思考了,这里你看起来像对象的东西,实际上只是一个服务标识,即 注册为零件接口服务 的标识

1)注册到底注册了个什么东西出来?

  • 注册阶段并不会创建对象,而是在告诉容器执行什么样的规则(以这里的两行代码为例):

    • 以后如果有人需要产品,就创建产品对象

    • 以后如果有人需要零件接口,就创建零件A对象

      • # 注册阶段
        服务类型:	产品				零件接口
        		    ↓ 				   ↓
        实现类型:  产品类			   零件A类
        ########################################
        # 类似于容器表
        产品				零件接口
         ↓ 					↓
        产品类				零件A类
        
        # 相当于(DI容器)说明书
        我要什么服务
         ↓
        应该创建什么对象
        
        # 即:需要(服务类型)时,创建(实现类型)对象
        
        

2)当我们调用服务时,又干了什么事情?

  • 你可能以为的:

    • # 产品 product = container.GetRequiredService<产品>();
      我要产品 => 查找登记表 => 找到产品类 => 查看产品构造函数
       ↓
      发现产品需要零件接口 => 查找登记表 => 零件接口 -> 零件A
       ↓
      创建零件A
       ↓
      创建产品
       ↓
      返回产品
      
      # 零件接口? part = container.GetService<零件接口>();
      我要零件接口 => 查找登记表 => 零件接口 -> 零件A
       ↓
      创建零件A
       ↓
      返回零件A
       ↓
      调用GetID()
      
  • 但实际上我在上面埋了个雷

    • services.AddSingleton<产品>();
      产品 product = container.GetRequiredService<产品>();
      
      # 注意,这里只有一个单例注册,并没有services.AddTransient<零件接口, 零件A>();
      这个时候你获取服务会发生下面事件:
      
      我要产品 -> 查找登记表 -> 找到产品类 -> 查看产品构造函数
       ↓
      发现产品需要零件接口 -> 查找登记表
       ↓
      未找到:零件接口 -> 某个实现
       ↓
      无法创建零件接口对应对象
       ↓
      无法创建产品
       ↓
      报错
      
      # 恭喜你,现在喜提编译器警告一次
      System.InvalidOperationException:“Unable to resolve service for type 'DI_Demo.零件接口' while attempting to activate 'DI_Demo.产品'.”
      
  • 为什么会报错?初学者可能会产生一个误解:

    • 产品已经依赖了零件接口,那容器应该会自动创建零件接口吧?
      • 问题是接口不是对象,他不能实例化
      • 接口有实现吗,他有对象吗,它没对象啊,纯单身狗一条啊
      • 接口只是一种约定,或者说草案,不是一个对象啊
      • 所以会出现:产品需要零件接口,但是接口不能创建对象,然后就崩了
    # 换句话说,你要是可以帮我实现下面这行代码算你厉害
    ❌零件接口 part = new 零件接口();
    
    # 容器真正要知道的是下面内容:
    零件接口
     ↓
    到底应该创建谁?
     ↓
    零件A,B,C中的哪一个?
    
    # 所以必须注册:明确知道是注册的谁
    services.AddTransient<零件接口, 零件A>();
    

  • 但是,这个时候你可能还会非常疑惑:

  • 那单例注册到底有什么用啊?为什么只放一个单例注册+一个单例注册的服务调用基本上无法使用?

    • 单例注册往往不是用来解决依赖的,仅说明,该对象只创建一次

    • 因为一个服务很少是孤立存在的,也因此单例注册几乎很少单独使用

      • 一个实际案例

      • /// <summary>
        /// 配置服务(整个程序只有一份,所以需要被注册为单例服务)
        /// </summary>
        public class ConfigService
        {
            public string ComPort { get; } = "COM3";
            public int BaudRate { get; } = 115200;
        }
        
        /// <summary>
        /// XXX通讯模块
        /// </summary>
        public class XXXService
        {
            private readonly ConfigService _config;
        
            public XXXService(ConfigService config)
            {
                _config = config;
            }
        
            public void Connect()
            {
                Console.WriteLine($"连接XXX:{_config.ComPort},波特率:{_config.BaudRate}");
            }
        }
        
        // 1.创建服务集合
        ServiceCollection services = new();
        
        // 2.注册服务
        services.AddSingleton<ConfigService>();
        services.AddTransient<XXXService>();
        
        // 3.构建容器
        IServiceProvider provider = services.BuildServiceProvider();
        
        // 4.获取服务
        XXXService xxx = provider.GetRequiredService<XXXService>();
        
        // 5.使用服务
        xxx.Connect();
        
        

2.理解误区

1)误区1:依赖是在注册时就创建的

  • 有一点必须记住:

    • 解决依赖的从来都不是什么Transient、Singleton、Scoped
    • 而是DI容器根据注册表找到对应实现并创建对象
    • 那三个注册方式只能决定创建出来的对象能活多久
  • 实际上,依赖这种东西在类中构造函数定义和实现的时候就已经定义好了(依赖关系已定义,这里以构造依赖为例)

    • 依赖关系在编写构造函数时就已经确定好了

    • DI容器只是负责解析并创建这些依赖对象

    • # 很多很多教程都喜欢这样写:
      services.AddSingleton<产品>();
      services.AddTransient<零件接口, 零件A>();
      
      # 会让很多人误解:
      # Singleton负责产品,Transient负责依赖
      
      # 但是假设我这样写,阁下如何应对呢
      services.AddSingleton<产品>();
      services.AddSingleton<零件接口, 零件A>();
      
      

2)误区2:认为单例注册是无法获取服务的

  • 很多实际业务服务不会单独存在,但单例注册本身完全可以单独获取

  • 而且当一堆单例注册出现的时候就不一样了 果然,还是人多力量大

    • 多个服务注册在一起后,容器可以在获取服务时自动解析依赖关系(不仅局限于单例注册)

    • public class X_Service
      {
          public X_Service(ConfigService config, LogService log) {	}
      }
      
      services.AddSingleton<ConfigService>();
      services.AddSingleton<LogService>();
      services.AddSingleton<X_Service>();
      
      X_Service x = provider.GetRequiredService<X_Service>();
      
    • 我要 XXXService
       ↓
      发现需要 ConfigService
       ↓
      获取 ConfigService
      ###########################
      发现需要 LogService
       ↓
      获取 LogService
      ###########################
      创建 XXXService
       ↓
      返回 XXXService
      
      

3)误区3:认为使用注册必须一一对应

  • 拜托,这是C#,不是C++,初学者甚至都可以看明白很多官方库代码,不要想得过于复杂

  • 如果不知道怎么注册,你就将注册看做一个列表,你只需要将你可以想到的依赖关系和单例情况全部写上去就完事了

  • 容器可以在获取服务时自动解析依赖关系,你要使用对应的服务对象,他自己找到怎么自动查找和解析

    • 注册阶段可以把已知的服务和依赖关系全部登记到容器中

    • 而且只要不是实例注册,绝大多数服务都不会立即创建,

    • 即使暂时没有使用,也基本不会产生明显开销

      • public class DatabaseService
        {
            public void Save(string msg)
            {
                Console.WriteLine($"保存数据:{msg}");
            }
        }
        
        public class LogService
        {
            public void Write(string msg)
            {
                Console.WriteLine($"记录日志:{msg}");
            }
        }
        
        public class OrderService
        {
            private readonly DatabaseService _db;
            private readonly LogService _log;
        
            public OrderService(DatabaseService db, LogService log)
            {
                _db = db;
                _log = log;
            }
        
            public void CreateOrder()
            {
                _db.Save("订单");
                _log.Write("创建订单成功");
            }
        }
        
        public class OrderController
        {
            private readonly OrderService _orderService;
        
            public OrderController(OrderService orderService)
            {
                _orderService = orderService;
            }
        
            public void Create() => _orderService.CreateOrder();
        }
        
        // 1.创建
        ServiceCollection services = new();
        
        // 2.注册
        services.AddSingleton<DatabaseService>();
        services.AddSingleton<LogService>();
        services.AddSingleton<OrderService>();
        services.AddSingleton<OrderController>();
        
        // 4.构建
        IServiceProvider provider = services.BuildServiceProvider();
        
        // 5.获取服务
        OrderController controller = provider.GetRequiredService<OrderController>();
        
        // 6.使用
        controller.Create();
        
        

4)误区4:注册时,若服务对象 ≠ 实现对象,获取服务时返回对象总认为是服务对象

  • services.AddTransient<零件接口, 零件A>();
    零件接口? part = container.GetService<零件接口>();
    
    
  • 初学时,你以为的,创建了一个零件接口服务,然后返回了一个零件接口服务

  • 不不不,这得看你实际代码,例如这里实际上返回的是零件A,只不过使用的服务是接口

  • // 让ChatGPT蒸馏了这篇博客,然后写了一个Demo
    
    using Microsoft.Extensions.DependencyInjection;
    
    #region 服务定义
    
    /// <summary>
    /// 零件接口
    /// </summary>
    public interface IPart
    {
        Guid Id { get; }
    }
    
    /// <summary>
    /// 零件A
    /// </summary>
    public class PartA : IPart
    {
        public Guid Id { get; } = Guid.NewGuid();
    }
    
    #endregion
    
    internal class Program
    {
        static void Main(string[] args)
        {
            /* 一.创建服务集合 */
            ServiceCollection services = new();
    
            /* 二.注册服务 */
            services.AddTransient<IPart, PartA>();
    
            /* 三.构建容器 */
            IServiceProvider provider =
                services.BuildServiceProvider();
    
            /* 四.获取服务 */
    
            IPart part1 =
                provider.GetRequiredService<IPart>();
    
            IPart part2 =
                provider.GetRequiredService<IPart>();
    
            IPart part3 =
                provider.GetRequiredService<IPart>();
    
            Console.WriteLine($"part1 : {part1.Id}");
            Console.WriteLine($"part2 : {part2.Id}");
            Console.WriteLine($"part3 : {part3.Id}");
    
            Console.WriteLine();
    
            Console.WriteLine(
                $"part1 == part2 : {ReferenceEquals(part1, part2)}");
    
            Console.WriteLine(
                $"part2 == part3 : {ReferenceEquals(part2, part3)}");
        }
    }
    

3.回过头来看另一个问题:产品运行期间需要动态切换实现零件A和B怎么办?

  • 这里给了3种不同的方案,详细的我就不过多解释了,主要是因为都14点了,该摸鱼听听音乐了

  • 使用DI容器的方案是修改方案3,方案1远古方案,方案2是设计模式

  • 原代码:

    • /// 这里我们使用 Nuget顶级包Microsoft.Extensions.DependencyInjection(官方DI容器) 来实现IOC操作
      
      /// <summary>
      /// 零件接口
      /// </summary>
      public interface 零件接口
      {
          int GetID();
      }
      
      /// <summary>
      /// 零件A
      /// </summary>
      public class 零件A : 零件接口
      {
          public int ID { get; set; } = 1001;
      
          public int GetID() => ID;
      }
      
      /// <summary>
      /// 零件B
      /// </summary>
      public class 零件B : 零件接口
      {
          public int ID { get; set; } = 2002;
      
          public int GetID() => ID;
      }
      
      /// <summary>
      /// 产品
      /// </summary>
      public class 产品
      {
          private readonly 零件接口 _part;
      
          public 产品(零件接口 part)
          {
              _part = part;
          }
      
          public void ShowInfo()
          {
              Console.WriteLine($"产品使用的零件ID:{_part.GetID()}");
          }
      }
      

1)修改方案1:不使用DI容器

  • public interface IPart
    {
        int GetID();
    }
    
    public class PartA : IPart
    {
        public int GetID() => 1001;
    }
    
    public class PartB : IPart
    {
        public int GetID() => 2002;
    }
    
    public class Product
    {
        /// 当前使用的零件
        private IPart _part;
    
        /// 创建产品时指定初始零件
        public Product(IPart part)
        {
            _part = part;
        }
    
        /// 动态更换零件
        public void ChangePart(IPart part)
        {
            _part = part;
        }
    
        /// 显示产品信息
        public void ShowInfo()
        {
            Console.WriteLine(
                $"当前零件ID:{_part.GetID()}");
        }
    }
    
    internal class Program
    {
        static void Main()
        {
            /* 创建产品并使用零件A */
            Product product = new Product(new PartA());
            product.ShowInfo();
    
            Console.WriteLine();
    
            /* 更换为零件B */
            product.ChangePart(new PartB());
            product.ShowInfo();
        }
    }
    

2)修改方案2:工厂模式

  • /// <summary>
    /// 零件接口
    /// </summary>
    public interface IPart
    {
        int GetID();
    }
    
    /// <summary>
    /// 零件A
    /// </summary>
    public class PartA : IPart
    {
        public int GetID() => 1001;
    }
    
    /// <summary>
    /// 零件B
    /// </summary>
    public class PartB : IPart
    {
        public int GetID() => 2002;
    }
    
    /// <summary>
    /// 零件工厂接口
    /// </summary>
    public interface IPartFactory
    {
        IPart Create(string partType);
    }
    
    /// <summary>
    /// 零件工厂
    /// </summary>
    public class PartFactory : IPartFactory
    {
        public IPart Create(string partType)
        {
            return partType switch
            {
                "A" => new PartA(),
                "B" => new PartB(),
    
                _ => throw new ArgumentException("未知零件类型")
            };
        }
    }
    
    /// <summary>
    /// 产品
    /// </summary>
    public class Product
    {
        private readonly IPartFactory _factory;
    
        private IPart? _part;
    
        /// <summary>
        /// 产品依赖工厂
        /// </summary>
        public Product(IPartFactory factory)
        {
            _factory = factory;
        }
    
        /// <summary>
        /// 根据类型切换零件
        /// </summary>
        public void ChangePart(string partType)
        {
            _part = _factory.Create(partType);
        }
    
        public void ShowInfo()
        {
            Console.WriteLine($"当前零件ID:{_part?.GetID()}");
        }
    }
    
    internal class Program
    {
        static void Main()
        {
            Product product = new Product(new PartFactory());
    
            product.ChangePart("A");
            product.ShowInfo();
    
            Console.WriteLine();
    
            product.ChangePart("B");
            product.ShowInfo();
        }
    }
    

3)修改方案3:保留DI容器,使用键

  • using Microsoft.Extensions.DependencyInjection;
    
    public interface IPart
    {
        int GetID();
    }
    
    public class PartA : IPart
    {
        public int GetID() => 1001;
    }
    
    public class PartB : IPart
    {
        public int GetID() => 2002;
    }
    
    public class Product
    {
        private IPart? _part;
    
        /// <summary>
        /// 更换零件
        /// </summary>
        public void ChangePart(IPart part) => _part = part;
    
        public void ShowInfo() => Console.WriteLine($"当前零件ID:{_part?.GetID()}");
    }
    
    internal class Program
    {
        static void Main()
        {
            /* 一.创建服务集合 */
            ServiceCollection services = new();
    
            /* 二.注册服务 */
            // 注册带键服务
            services.AddKeyedTransient<IPart, PartA>("A");
            services.AddKeyedTransient<IPart, PartB>("B");
            // 注册产品
            services.AddSingleton<Product>();
    
            /* 三.构建容器 */
            IServiceProvider provider = services.BuildServiceProvider();
    
            /* 四.获取产品 */
            Product product = provider.GetRequiredService<Product>();
    
            /* 使用零件A */
            IPart partA = provider.GetRequiredKeyedService<IPart>("A");
            product.ChangePart(partA);
            product.ShowInfo();
    
            Console.WriteLine();
    
            /* 使用零件B */
            IPart partB = provider.GetRequiredKeyedService<IPart>("B");
            product.ChangePart(partB);
            product.ShowInfo();
        }
    }
    
    

终于又到了一篇一次的吐槽时刻了,突然觉得最近上班好无聊啊,但是只要我一开始写点东西
现场电话就打过来了,只要我开始摸鱼,真的什么事情也没有,当然,也绝对绝对不是我特别喜欢摸鱼
这几个月倒是用AI写了很多东西,但是写到后面越发发现,突然让我沉下心来敲几行代码
也是愈发手生了,抽象能力也基本上没提高到多少,
就像这篇博客一样,可能你需要花费好几天,甚至一两周的时间
才可以完全消化完毕,并且写出来,但交给AI真的就只是一两分钟的事情
你一边感叹于初级员工真的一点用处都没有,只要一个企业想,随时随地可以被优化
当你想要尝试突破初级,来到中级的这个层次,你突然会发现,还是得一步一个脚印
古法编程可能会被淘汰,但是你不能不会,你可以不是那种彻彻底底的古法编程
但是不使用AI写代码时,你也至少要有大致的思路,知道怎么用
说白了就是频繁的使用已经断绝了很多初级程序员的路,抽象能力上不去,
什么架构能力,问题排查能力,兜底能力,团队协作能力,全是无稽之谈
真的必须花费大量的时间在这写你所以为低级的知识上,你才会有所进步
当然,也有可能是因为我不是搞Java的,不会有担心框架和结束高速迭代淘汰产生的一些问题,
但是归根结底,提高抽象能力最直接的办法,也就是古法编程了......
哪怕你是去copy一部分代码,也比让智能体完完全全接手你的项目要好的多

然后再来谈谈业务,从大学到毕业工作到现在,这个词听到耳朵都快起茧了
到底什么是业务,我也仔细思考了一下,
我所认为的业务,就是当你拿到一个项目,不管是你熟悉的还是不熟悉的东西
哪怕你之前是干嵌入式的,别管什么内核,下位机,现在就是要你去写一个Web程序
你会怎么思考,给你一个强大的AI,你又会怎么设计
对于不同行业,不同领域的东西,你可以思考出怎么来做这个产品的能力
这就是我理解的业务能力
只要业务能力足够,很多人哪怕转行,也非常轻松
而不是像我现在这样,上班天天盯着电脑,你说做一个个人项目
自己都不知道自己的需求是什么,架构设计怎么做合适,我好像除了写点业务一无所有
但是当自己接到一个项目时,虽然架构不是特别熟悉,但是却好像明白我需要干什么
这依旧是值得思考的一个点,但是这些也只能后面慢慢来了

posted @ 2026-06-18 16:43  假设狐狸有信箱  阅读(66)  评论(0)    收藏  举报