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,你又会怎么设计

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

这就是我理解的业务能力

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

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

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

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

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

相关推荐
鑫 源2 小时前
C#源码开源, 获取macOS 电池数据,包括iOS (xamarin开发)
c#·.net·xamarin
爱吃奥利奥_wen12 小时前
面向对象三板斧:封装、继承、多态,到底在“装“什么、“承“什么、“变“什么?
开发语言·经验分享·c#
wflynn1 天前
GitHub 今日推荐|SCSKiller:游戏着色器预编译消除卡顿
开源·c#·github·nvidia·amd·directx12
疯狂的维修2 天前
关于用C#写电机起保停控制
开发语言·c#
用户788477316342 天前
门店核销闭环:小程序扫码 + .NET 幂等接口 + WPF 看板,一天能跑通
微信小程序·c#
samble2 天前
从零搭建灌装监控系统(十三):报警系统设计,生命周期与通知
c#·wpf·mvvm·modbus·工业控制
miofly2 天前
GitHub 今日推荐|SCSKiller:游戏着色器预编译消除卡顿
开源·c#·github
周杰伦fans3 天前
8GB显存下模型量化实战指南
人工智能·后端·c#
samble3 天前
从零搭建灌装监控系统(十二):配置系统,原子写入与容错
c#·wpf·mvvm·modbus·工业控制
流形填表3 天前
题库去重实战:基于题干指纹的重复题清理
开发语言·c#