blog2026年6月18日· 约 73 分钟

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

Twitter 微博

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

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


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

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

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

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

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

废话少说,进入正题


一.DI依赖注入 — 概念#

[!TIP]

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

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


1.什么是依赖(Dependency)?#

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

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

2.什么是注入(Injection)?#

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

    •   class=class="hljs-string">"hljs-comment">// 通过对象product使用了对象a
        
        var a = new 零件A();
        var product = new 产品(a);

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

  • 依赖注入 = 依赖 + 注入

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

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

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

1.什么是控制(Control)?#

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

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

2.什么是反转(Inversion)?#

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    class=class="hljs-string">"hljs-comment">/* 一.创建容器生成类(服务集合) */
    ServiceCollection containerBuilder = new ServiceCollection();

2.注册服务(AddXXX)#

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

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

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

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

    class=class="hljs-string">"hljs-comment">// class="hljs-number">1)单例
    class=class="hljs-string">"hljs-comment">// 服务对象 = 实现对象
    containerBuilder.AddSingleton<产品>();

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

    class=class="hljs-string">"hljs-comment">// class="hljs-number">3)作用域(不会Web我就偷懒不讲了哈)
    class=class="hljs-string">"hljs-comment">// AddScoped<接口或父类, 实现类>()

    class=class="hljs-string">"hljs-comment">// 尝试注册 - 用法和上面一样,效果 -> 如果没注册就注册,注册了就通过
    class=class="hljs-string">"hljs-comment">// services.AddSingleton();
    class=class="hljs-string">"hljs-comment">// services.AddScoped();
    class=class="hljs-string">"hljs-comment">// 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.服务类型 = 注册类型

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

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

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

      •   class=class="hljs-string">"hljs-comment">// 接口注册
          services.AddTransient<ILogger, FileLogger>();
          class=class="hljs-string">"hljs-comment">// 基类注册
          services.AddSingleton<零件Base, 零件A>();

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

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

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

    •   class=class="hljs-string">"hljs-comment"># 类型注册:使用容器注册产品
        容器
         ↓
        new 产品()
        
        class=class="hljs-string">"hljs-comment">#========================================
        class=class="hljs-string">"hljs-comment"># 实例注册:将产品实例交给容器
        new 产品()
         ↓
        容器

  • 代码示例:

  •   SerialPort port = new SerialPort(class="hljs-string">"COM1", class="hljs-number">115200);
      
      services.AddSingleton(port);

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

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

    • [!CAUTION]

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

3.构建容器(BuildServiceProvider)#

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

  • [!IMPORTANT]

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

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

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

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

    class=class="hljs-string">"hljs-comment">/* 三.生成容器 */
    IServiceProvider container = containerBuilder.BuildServiceProvider();
    containerBuilder.MakeReadOnly();    class=class="hljs-string">"hljs-comment">// 将生成类集合声明为只读,后续再添加服务会进行报错
    
    class=class="hljs-string">"hljs-comment">// containerBuilder.AddSingleton<产品>();    //=> 使用MakeReadOnly后报错

4.获取服务(GetService)#

  • 一般情况下,我们会获取不带键的服务(如下),但是现在官方给了我们一种比较新的写法(带键)

    /* 四.服务调用(从容器获取对象) */

    // 不带键 产品? product = container.GetService<产品>(); 产品 product2 = container.GetRequiredService<产品>();

  • 为什么需要带键?同一个服务类型,可能对应多个实现对象,我想指定实现对象,怎么办?

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

4.1 不带键的服务

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

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

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

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

4.2 带键的服务

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

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

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

四.流程图#

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

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

五.实际案例#

1.回顾#

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

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

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

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

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

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

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

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

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

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

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

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

  • 你可能以为的:

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

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

    • 产品已经依赖了零件接口,那容器应该会自动创建零件接口吧?

      • 问题是接口不是对象,他不能实例化
      • 接口有实现吗,他有对象吗,它没对象啊,纯单身狗一条啊
      • 接口只是一种约定,或者说草案,不是一个对象啊
      • 所以会出现:产品需要零件接口,但是接口不能创建对象,然后就崩了

      换句话说,你要是可以帮我实现下面这行代码算你厉害

      ❌零件接口 part = new 零件接口();

      容器真正要知道的是下面内容:

      零件接口 ↓ 到底应该创建谁? ↓ 零件A,B,C中的哪一个?

      所以必须注册:明确知道是注册的谁

      services.AddTransient<零件接口, 零件A>();


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

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

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

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

      • 一个实际案例

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

2.理解误区#

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

  • 有一点必须记住:

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

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

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

    •   class=class="hljs-string">"hljs-comment"># 很多很多教程都喜欢这样写:
        services.AddSingleton<产品>();
        services.AddTransient<零件接口, 零件A>();
        
        class=class="hljs-string">"hljs-comment"># 会让很多人误解:
        class=class="hljs-string">"hljs-comment"># Singleton负责产品,Transient负责依赖
        
        class=class="hljs-string">"hljs-comment"># 但是假设我这样写,阁下如何应对呢
        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
        class=class="hljs-string">"hljs-comment">###########################
        发现需要 LogService
         ↓
        获取 LogService
        class=class="hljs-string">"hljs-comment">###########################
        创建 XXXService
         ↓
        返回 XXXService

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • 原代码:

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

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

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

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

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

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

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

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

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

评论

© 2026 松岛川树