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

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


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

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

这个模块里面,我想讲的,大多数是在实际项目中常用的东西,例如一些NuGet 包,一个语法,或者某种设计模式

不过不过多描述概念了,不讲官方那些罗里吧嗦的概念,只需要理解他是什么鬼东西,干什么的,怎么用即可

大抵就是学院派和江湖派的区别吧

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

然后,为什么要进行这么古老的学习方式,废话,这年头AI快把初级员工的路堵死了,

不来点古法编程,抽象能力提升很慢的,用了AI几个月,发现初级迈向中级,你不古法编程就等死吧

而且上班摸鱼时间一大把,系统性的学习学习怎么了,打发时间也挺好的,然后深入学习一下IoC的思想

废话少说,进入正题


一.DI依赖注入 --- 概念

!TIP

如果你不想看文字,或者觉得我这一块讲的不是特别明白的,想看视频教学的话

推荐一位up做的关于依赖注入的教学视频,大概30分钟左右的教学,

只不过后面几个视频初学者容易看不懂

【.Net-依赖注入】从依赖说起_哔哩哔哩_bilibili

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


1.什么是依赖(Dependency)?

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

c# 复制代码
/// 因为产品需要零件A,所以产品依赖于零件A
/// 即:零件A就是产品的依赖
public class 零件A
{
    public int GetID() => 100;
}

public class 产品
{
    private readonly 零件A _a;

    public 产品(零件A a) => _a = a;
}

2.什么是注入(Injection)?

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

    c# 复制代码
    // 通过对象product使用了对象a
    
    var a = new 零件A();
    var product = new 产品(a);

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

  • 依赖注入 = 依赖 + 注入

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

c# 复制代码
// 一个用于示例的空类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中,特指:创建什么对象,什么时候创建,对象如何创建对象的决定权
      • 看不懂就看下面的例子,一眼秒懂
c# 复制代码
// 产品控制着零件A的创建
// 即:产品拥有创建零件A的控制权

// 缺点,产品和零件A已经绑死了,高度耦合,扩展等死,后人挠头,直骂屎山
public class 零件A {	}

public class 产品
{
    private readonly 零件A _a;

    public 产品()
    {
        _a = new 零件A();
    }
}

2.什么是反转(Inversion)?

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

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

c# 复制代码
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最常见的方式之一
shell 复制代码
# 原来:产品内部控制零件A
产品
 ↓	# 控制
零件A
#==================================================
# 现在:外部同时控制零件和产品,然后把零件A交给产品使用
外部
 ↓  # 控制
零件A

外部
 ↓  # 控制
产品

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

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

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

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

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

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

    c# 复制代码
    /// 这里我们使用 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个基本步骤

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

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

    c# 复制代码
        /// 不使用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)

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

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

c# 复制代码
    /* 一.创建容器生成类(服务集合) */
    ServiceCollection containerBuilder = new ServiceCollection();

2.注册服务(AddXXX)

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

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

c# 复制代码
    /* 二.注册容器中的服务信息(注册实例) ------ 服务类型,实现类型,生命周期 */
    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.服务类型 = 注册类型

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

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

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

    复制代码
    ```c#
    // 接口注册
    services.AddTransient<ILogger, FileLogger>();
    // 基类注册
    services.AddSingleton<零件Base, 零件A>();
    ```

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

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

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

    shell 复制代码
    # 类型注册:使用容器注册产品
    容器
     ↓
    new 产品()
    
    #========================================
    # 实例注册:将产品实例交给容器
    new 产品()
     ↓
    容器

  • 代码示例:

c# 复制代码
SerialPort port = new SerialPort("COM1", 115200);

services.AddSingleton(port);

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

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

    • !CAUTION

      • 注册工厂时并不会立即执行 Lambda 表达式
        • Lambda 只是被容器保存起来,
        • 当获取服务(调用服务)时,容器才会执行该 Lambda 来创建对象
c# 复制代码
containerBuilder.AddSingleton<零件A>();

/// 这里是sp指IServiceProvider(当前容器)
containerBuilder.AddSingleton<产品>(sp =>
{
    // 现在这个大括号里面的内容就是你自定义的对象创建过程
    
    var part = sp.GetRequiredService<零件A>();
    return new 产品(part);
});
shell 复制代码
获取产品
 ↓
执行工厂函数
 ↓
从容器获取零件A
 ↓
创建并且返回产品

3.构建容器(BuildServiceProvider)

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

  • !IMPORTANT

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

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

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

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

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

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

4.获取服务(GetService)

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

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

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

4.1 不带键的服务

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

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

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

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

4.2 带键的服务

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

1)GetKeyedService
c# 复制代码
/// 注册
containerBuilder.AddKeyedSingleton<产品>("产品");

// .......

/// 获取服务
// 获取产品
产品 product = container.GetRequiredKeyedService<产品>("产品");

2)GetRequiredKeyedService
c# 复制代码
/// 注册
containerBuilder.AddKeyedSingleton<零件接口, 零件A>("零件A");

// .......

/// 获取服务
// 获取零件
零件接口 part = container.GetRequiredKeyedService<零件接口>("零件A");

四.流程图

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

shell 复制代码
IoC(思想)
 ↓
DI(实现方式)
 ↓
DI容器

# ================================

1.创建容器
 ↓
ServiceCollection

2.注册服务
 ↓
AddSingleton
AddTransient
AddScoped

3.构建容器
 ↓
BuildServiceProvider

4.获取服务
 ↓
GetRequiredService
GetService

五.实际案例

1.回顾

但是在此之前,我们先回顾一下内容,不然------

你不会以为就这么完了吗,我有说过结束了吗,那和我以前的博客有什么区别?

c# 复制代码
注册(AddXXX)
 	↓
向容器登记对象创建规则

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

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

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

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

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

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

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

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

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

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

  • 你可能以为的:

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

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

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

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

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

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

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

      • 一个实际案例

      c# 复制代码
      /// <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容器只是负责解析并创建这些依赖对象

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

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

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

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

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

    c# 复制代码
    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>();
    shell 复制代码
    我要 XXXService
     ↓
    发现需要 ConfigService
     ↓
    获取 ConfigService
    ###########################
    发现需要 LogService
     ↓
    获取 LogService
    ###########################
    创建 XXXService
     ↓
    返回 XXXService

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

  • 拜托,这是C#,不是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:注册时,若服务对象 ≠ 实现对象,获取服务时返回对象总认为是服务对象

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

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

c# 复制代码
// 让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是设计模式

  • 原代码:

    c# 复制代码
    /// 这里我们使用 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容器

c# 复制代码
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:工厂模式

c# 复制代码
/// <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容器,使用键

c# 复制代码
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,你又会怎么设计

对于不同行业,不同领域的东西,你可以思考出怎么来做这个产品的能力

这就是我理解的业务能力

只要业务能力足够,很多人哪怕转行,也非常轻松

而不是像我现在这样,上班天天盯着电脑,你说做一个个人项目

自己都不知道自己的需求是什么,架构设计怎么做合适,我好像除了写点业务一无所有

但是当自己接到一个项目时,虽然架构不是特别熟悉,但是却好像明白我需要干什么

这依旧是值得思考的一个点,但是这些也只能后面慢慢来了

相关推荐
咕白m6253 小时前
使用 C# 在 Excel 中应用多种字体样式
后端·c#
Artech9 小时前
[MAF预定义的AIContextProvider-02]AgentSkillsProvider——将Agent Skills引入MAF
ai·c#·agent·agent skills·maf
2601_962072551 天前
李梦娇常识4600问|题库|打印版
sql·华为od·华为·c#·华为云·.net·harmonyos
m0_547486661 天前
《C#语言程序设计与实践》 全套PPT课件
c语言·c#·c语言程序设计
叶帆1 天前
【YFIOs】用C#开发硬件之设备上云
开发语言·unity·c#
IT方大同1 天前
(嵌入式操作系统)信号量
嵌入式硬件·c#
z落落1 天前
C# FileStream文件流读取文件
开发语言·c#
yngsqq1 天前
排版优化 异形排版
c#
苦学的罐头1 天前
C# 协变与逆变深度解析:为什么 IEnumerable<T> 能转换,而 List<T> 不行?
开发语言·c#·list