Skip to Content
Week 05Day 2 - 依赖注入

Day 2 - 依赖注入

建议用时:260-320 分钟

你将学会什么

  • 什么是依赖,什么是注入
  • 为什么业务类不要自己到处 new 具体实现
  • 构造函数注入为什么是最常见写法
  • 接口如何让实现可替换
  • 什么是组合根,也就是统一创建对象的地方
  • Singleton、Scoped、Transient 生命周期分别是什么意思
  • 如何使用 Microsoft.Extensions.DependencyInjection 注册和解析服务

依赖注入的核心不是容器,而是“使用对象的类,不负责创建它依赖的具体对象”。先把构造函数注入理解清楚,再看容器就不会乱。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“解决方案结构”,今天继续学习“依赖注入”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

第一次学习不要求背完整页。完成下面 3 项,就可以继续:

  • 能用自己的话说明“依赖注入”解决什么问题。
  • 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
  • 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。

第一部分:先学原理和最小知识

这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。依赖注入的名字听起来复杂,本质很简单:谁需要什么,从外面传给它。

1. 什么是依赖

一个类需要另一个对象帮它做事,这个对象就是它的依赖。

例如:

ProductService 需要保存商品 保存商品需要 IProductRepository 所以 IProductRepository 是 ProductService 的依赖

2. 什么是注入

注入就是从外面传进来。

最常见写法是构造函数注入:

public ProductService(IProductRepository repository)

意思是:

ProductService 要工作,需要外部给我一个 IProductRepository。

3. 不注入会有什么问题

如果业务类自己创建依赖:

ProductService 里面直接 new FileProductRepository()

问题是:

  1. 想换成数据库保存,要改 ProductService
  2. 想测试 ProductService,必须真的写文件。
  3. 业务规则和保存细节绑在一起。
  4. 代码越来越难替换。

4. 注入之后有什么好处

注入之后:

ProductService 只依赖 IProductRepository

好处:

  1. 文件保存可以换成内存保存。
  2. 内存保存可以换成数据库保存。
  3. 测试时可以用假的实现。
  4. 业务服务更专注业务规则。

5. 为什么要依赖接口

如果构造函数写:

public ProductService(FileProductRepository repository)

它还是绑死了文件实现。

更好的写法是:

public ProductService(IProductRepository repository)

接口表示能力:

我不关心你怎么保存,只要你能 Save。

6. 什么是组合根

组合根就是统一创建对象、组装依赖的地方。

在控制台程序里,通常就是 Program.cs 开头。

例如:

IProductRepository repository = new FileProductRepository(...) ProductService service = new ProductService(repository)

业务类不要到处 new 依赖,入口层统一组装。

7. 什么是 DI 容器

DI 容器是帮你创建对象、管理依赖的工具。

不用容器时,你手动写:

new ProductService(new FileProductRepository(...))

用容器时,你先注册:

IProductRepository -> FileProductRepository ProductService

然后让容器解析:

GetRequiredService<ProductService>()

容器会自动看构造函数需要什么,再帮你创建。

8. 生命周期是什么

生命周期决定对象什么时候创建、是否复用。

生命周期含义适合什么
Singleton整个程序只创建一个配置、无状态工具、全局缓存
Scoped一个作用域内共用一个Web 请求内的数据库上下文
Transient每次需要都创建新的轻量服务、无状态服务

先记住:

Singleton 复用最久。 Transient 每次新建。 Scoped 在一个范围内复用。

控制台练习里最常见的是 Singleton 和 Transient。

9. 生命周期选错会怎样

错误后果
把带状态对象注册成 Singleton多处共享状态,数据可能串
把昂贵对象注册成 Transient重复创建,浪费资源
Scoped 对象被 Singleton 长期持有生命周期混乱

本阶段先记住:有状态、和某次操作强相关的对象,不要随便做 Singleton。

依赖注入常用 API 速查

需求写法说明
创建容器配置new ServiceCollection()注册服务入口
注册每次新建AddTransient<IService, Service>()轻量、无状态服务
注册作用域AddScoped<IService, Service>()Web 请求内常用
注册单例AddSingleton<IService, Service>()全局共享一个实例
构建容器BuildServiceProvider()得到服务提供者
取服务GetRequiredService<T>()取不到就报错
构造函数注入ProductService(IRepository repo)类不自己 new 依赖

最小模板:

var services = new ServiceCollection(); services.AddSingleton<IProductRepository, MemoryProductRepository>(); services.AddTransient<ProductService>(); ServiceProvider provider = services.BuildServiceProvider(); ProductService service = provider.GetRequiredService<ProductService>();

生命周期先这样记:

Transient:每次要都新建 Scoped:一个范围内共用 Singleton:整个程序共用一个

第二部分:把知识组合成完整例子

前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要能写出这种结构:服务依赖接口,具体实现从外面传进来。

先看效果:ProductService 不直接 new Repository

IProductRepository repository = new FileProductRepository("products.txt"); ProductService service = new ProductService(repository); Product product = service.Create("键盘", 199m); Console.WriteLine($"创建成功: {product.Name} | {product.Price}"); interface IProductRepository { void Save(Product product); } class ProductService { private readonly IProductRepository _repository; public ProductService(IProductRepository repository) { _repository = repository; } public Product Create(string name, decimal price) { Product product = new Product(name, price); _repository.Save(product); return product; } } class FileProductRepository : IProductRepository { private readonly string _path; public FileProductRepository(string path) { _path = path; } public void Save(Product product) { File.AppendAllText(_path, $"{product.Name},{product.Price}" + Environment.NewLine); } } class Product { public Product(string name, decimal price) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("商品名称不能为空"); } if (price <= 0) { throw new ArgumentException("商品价格必须大于 0"); } Name = name.Trim(); Price = price; } public string Name { get; } public decimal Price { get; } }

最重要的是:

ProductService 需要 IProductRepository,但它不自己 new FileProductRepository。

具体用文件保存、内存保存、数据库保存,由外面决定。

第三部分:跟着敲代码

从这里开始动手。每个例子都是完整代码,可以直接放进 Program.cs 运行。使用 DI 容器的例子需要先安装包。

动手前先做这 3 件事

  1. 先不用容器,把构造函数注入写明白。
  2. 能手动组装对象后,再看容器注册。
  3. 每个例子都试着换一个实现类,观察业务类是否需要改。

例子 1:错误写法,业务类自己 new 依赖

ProductService service = new ProductService(); service.Create("键盘", 199m); class ProductService { public void Create(string name, decimal price) { Product product = new Product(name, price); FileProductRepository repository = new FileProductRepository("products.txt"); repository.Save(product); } } class FileProductRepository { private readonly string _path; public FileProductRepository(string path) { _path = path; } public void Save(Product product) { File.AppendAllText(_path, $"{product.Name},{product.Price}" + Environment.NewLine); } } class Product { public Product(string name, decimal price) { Name = name; Price = price; } public string Name { get; } public decimal Price { get; } }

这段代码能运行,但结构不好:ProductService 被文件保存实现绑死了。

例子 2:改成构造函数注入

IProductRepository repository = new FileProductRepository("products.txt"); ProductService service = new ProductService(repository); service.Create("键盘", 199m); interface IProductRepository { void Save(Product product); } class ProductService { private readonly IProductRepository _repository; public ProductService(IProductRepository repository) { _repository = repository; } public void Create(string name, decimal price) { Product product = new Product(name, price); _repository.Save(product); } } class FileProductRepository : IProductRepository { private readonly string _path; public FileProductRepository(string path) { _path = path; } public void Save(Product product) { File.AppendAllText(_path, $"{product.Name},{product.Price}" + Environment.NewLine); } } class Product { public Product(string name, decimal price) { Name = name; Price = price; } public string Name { get; } public decimal Price { get; } }

现在 ProductService 只知道接口,不知道具体保存到哪里。

例子 3:替换成内存实现

MemoryProductRepository repository = new MemoryProductRepository(); ProductService service = new ProductService(repository); service.Create("键盘", 199m); service.Create("鼠标", 88m); Console.WriteLine($"保存数量: {repository.Products.Count}"); interface IProductRepository { void Save(Product product); } class ProductService { private readonly IProductRepository _repository; public ProductService(IProductRepository repository) { _repository = repository; } public void Create(string name, decimal price) { Product product = new Product(name, price); _repository.Save(product); } } class MemoryProductRepository : IProductRepository { public List<Product> Products { get; } = new List<Product>(); public void Save(Product product) { Products.Add(product); } } class Product { public Product(string name, decimal price) { Name = name; Price = price; } public string Name { get; } public decimal Price { get; } }

换成内存实现后,ProductService 不需要改。这就是依赖接口的价值。

例子 4:测试时使用假的实现

FakeProductRepository repository = new FakeProductRepository(); ProductService service = new ProductService(repository); service.Create("键盘", 199m); if (repository.Saved.Count == 1) { Console.WriteLine("测试通过"); } else { Console.WriteLine("测试失败"); } interface IProductRepository { void Save(Product product); } class ProductService { private readonly IProductRepository _repository; public ProductService(IProductRepository repository) { _repository = repository; } public void Create(string name, decimal price) { Product product = new Product(name, price); _repository.Save(product); } } class FakeProductRepository : IProductRepository { public List<Product> Saved { get; } = new List<Product>(); public void Save(Product product) { Saved.Add(product); } } class Product { public Product(string name, decimal price) { Name = name; Price = price; } public string Name { get; } public decimal Price { get; } }

测试不需要真的写文件,只检查服务有没有调用保存逻辑。

例子 5:手动组合对象

IMessageWriter writer = new ConsoleMessageWriter(); NotificationService service = new NotificationService(writer); service.Send("保存成功"); interface IMessageWriter { void Write(string message); } class ConsoleMessageWriter : IMessageWriter { public void Write(string message) { Console.WriteLine(message); } } class NotificationService { private readonly IMessageWriter _writer; public NotificationService(IMessageWriter writer) { _writer = writer; } public void Send(string message) { _writer.Write($"通知: {message}"); } }

这里最上面两行就是手动组合根:创建实现,再传给服务。

例子 6:生命周期概念,Singleton 和 Transient

CounterService singleton = new CounterService(); singleton.Increase(); singleton.Increase(); Console.WriteLine($"Singleton 模拟: {singleton.Count}"); CounterService transient1 = new CounterService(); CounterService transient2 = new CounterService(); transient1.Increase(); transient2.Increase(); Console.WriteLine($"Transient 模拟 1: {transient1.Count}"); Console.WriteLine($"Transient 模拟 2: {transient2.Count}"); class CounterService { public int Count { get; private set; } public void Increase() { Count++; } }

同一个对象重复用,状态会累加。每次新建对象,状态互不影响。

例子 7:安装 DI 容器包

dotnet add package Microsoft.Extensions.DependencyInjection

这个包提供 ServiceCollectionAddSingletonAddTransientBuildServiceProvider 等常用 DI 容器能力。

例子 8:使用 DI 容器注册和解析

using Microsoft.Extensions.DependencyInjection; ServiceCollection services = new ServiceCollection(); services.AddSingleton<IMessageWriter, ConsoleMessageWriter>(); services.AddTransient<NotificationService>(); ServiceProvider provider = services.BuildServiceProvider(); NotificationService service = provider.GetRequiredService<NotificationService>(); service.Send("保存成功"); interface IMessageWriter { void Write(string message); } class ConsoleMessageWriter : IMessageWriter { public void Write(string message) { Console.WriteLine(message); } } class NotificationService { private readonly IMessageWriter _writer; public NotificationService(IMessageWriter writer) { _writer = writer; } public void Send(string message) { _writer.Write($"通知: {message}"); } }

容器会自动发现 NotificationService 构造函数需要 IMessageWriter,然后用注册好的 ConsoleMessageWriter 创建它。

例子 9:容器里的 Singleton 和 Transient

using Microsoft.Extensions.DependencyInjection; ServiceCollection services = new ServiceCollection(); services.AddSingleton<SingletonCounter>(); services.AddTransient<TransientCounter>(); ServiceProvider provider = services.BuildServiceProvider(); SingletonCounter singleton1 = provider.GetRequiredService<SingletonCounter>(); SingletonCounter singleton2 = provider.GetRequiredService<SingletonCounter>(); singleton1.Increase(); singleton2.Increase(); Console.WriteLine($"Singleton 结果: {singleton1.Count}"); TransientCounter transient1 = provider.GetRequiredService<TransientCounter>(); TransientCounter transient2 = provider.GetRequiredService<TransientCounter>(); transient1.Increase(); transient2.Increase(); Console.WriteLine($"Transient 1: {transient1.Count}"); Console.WriteLine($"Transient 2: {transient2.Count}"); class SingletonCounter { public int Count { get; private set; } public void Increase() { Count++; } } class TransientCounter { public int Count { get; private set; } public void Increase() { Count++; } }

SingletonCounter 两次解析得到的是同一个对象,所以计数会累加到 2。

TransientCounter 每次解析得到新对象,所以两个对象各自是 1。

常见错误和修法

错误为什么错修法
服务里手动 new 所有依赖依赖关系固定,难替换测试通过构造函数注入依赖
忘记注册服务运行时容器找不到实现在启动处 AddSingletonAddTransient 等注册
生命周期乱用对象状态可能共享错无状态服务用 transient,配置类可 singleton
接口和实现不匹配注入时解析失败注册 services.AddTransient<IService, Service>()
业务代码依赖容器代码变难测只在组合根使用容器,业务类接收构造函数参数

小白重复敲写训练

依赖注入先从“接口、实现、使用者”三个文件练。

训练 1:不用容器也能理解注入

IMessageService service = new ConsoleMessageService(); var app = new AppRunner(service); app.Run(); interface IMessageService { void Send(string text); } class ConsoleMessageService : IMessageService { public void Send(string text) => Console.WriteLine(text); } class AppRunner(IMessageService service) { public void Run() => service.Send("程序启动"); }

第二遍增加 SilentMessageService 并替换实现。

训练 2:注册和解析服务

using Microsoft.Extensions.DependencyInjection; var services = new ServiceCollection(); services.AddSingleton<IMessageService, ConsoleMessageService>(); using var provider = services.BuildServiceProvider(); var service = provider.GetRequiredService<IMessageService>(); service.Send("Hello DI");

改动任务:把 AddSingleton 改成 AddTransient,连续解析两次并比较对象。

训练 3:构造函数注入

class OrderService(IMessageService messages) { public void Create() { Console.WriteLine("创建订单"); messages.Send("订单创建成功"); } }

关掉答案,自己写出 OrderService 需要 IMessageService 的构造函数。

每日小测

做完本页后,用这 5 题检查是否真的掌握。

1. 判断题

本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。

答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。

2. 填空题

本页主题是:依赖注入。今天至少要掌握的 3 个点是:

1. 什么是依赖,什么是注入 2. 为什么业务类不要自己到处 `new` 具体实现 3. 构造函数注入为什么是最常见写法

答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。

3. 流程题

遇到本页相关功能时,先按什么顺序处理?

答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。

4. 找错误题

如果本页代码运行失败,第一步应该做什么?

答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。

5. 改需求题

在本页完整例子跑通后,至少改一个小需求。

可选改法:

  • 改一个字段名称。
  • 多加一个校验条件。
  • 多输出一行结果。
  • 把固定数据改成用户输入。
  • 把一次处理改成多条数据处理。

答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:什么是依赖,什么是注入。

上位机专项练习

依赖注入让业务代码依赖接口,而不是把串口、TCP 或模拟读取器写死。

下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。

专项例子 1:构造函数注入读取器

interface IReader { double Read(); } class FakeReader : IReader { public double Read() => 25.6; } class Monitor { private readonly IReader reader; public Monitor(IReader reader) => this.reader = reader; public void Show() => Console.WriteLine(reader.Read()); } class Program { static void Main() => new Monitor(new FakeReader()).Show(); }

运行结果或界面效果:

25.6

改动任务: 再写一个返回 30.0 的 TestReader。

专项例子 2:注册接口和实现

var services = new ServiceCollection(); services.AddSingleton<IDeviceReader, FakeDeviceReader>(); using ServiceProvider provider = services.BuildServiceProvider(); IDeviceReader reader = provider.GetRequiredService<IDeviceReader>();

运行结果或界面效果:

容器创建 FakeDeviceReader

改动任务: 把 Singleton 改成 Transient,并查清区别。

专项例子 3:替换实现而不改业务类

IDeviceReader reader = useSimulator ? new SimulatorReader() : new ModbusReader(); var monitor = new DeviceMonitor(reader);

运行结果或界面效果:

根据配置切换模拟设备或真实设备

改动任务: 新增 SerialReader 分支。

第四部分:作业完整答案

这一部分给出当天作业的完整答案。建议先用手动注入跑通,再用容器版本跑通。

作业 1:手动依赖注入

要求:

  1. 定义 IMessageWriter
  2. ConsoleMessageWriter
  3. ProductService 通过构造函数接收 IMessageWriter
  4. 调用 Save() 输出保存成功。

完整答案

IMessageWriter writer = new ConsoleMessageWriter(); ProductService service = new ProductService(writer); service.Save(); interface IMessageWriter { void Write(string message); } class ConsoleMessageWriter : IMessageWriter { public void Write(string message) { Console.WriteLine(message); } } class ProductService { private readonly IMessageWriter _writer; public ProductService(IMessageWriter writer) { _writer = writer; } public void Save() { _writer.Write("保存成功"); } }

作业 2:替换实现

要求:

  1. 新增 MemoryMessageWriter
  2. 不修改 ProductService
  3. 改入口处的具体实现即可。

完整答案

MemoryMessageWriter writer = new MemoryMessageWriter(); ProductService service = new ProductService(writer); service.Save(); Console.WriteLine($"记录数量: {writer.Messages.Count}"); Console.WriteLine(writer.Messages[0]); interface IMessageWriter { void Write(string message); } class MemoryMessageWriter : IMessageWriter { public List<string> Messages { get; } = new List<string>(); public void Write(string message) { Messages.Add(message); } } class ProductService { private readonly IMessageWriter _writer; public ProductService(IMessageWriter writer) { _writer = writer; } public void Save() { _writer.Write("保存成功"); } }

作业 3:DI 容器完整答案

先安装包:

dotnet add package Microsoft.Extensions.DependencyInjection

完整代码:

using Microsoft.Extensions.DependencyInjection; ServiceCollection services = new ServiceCollection(); services.AddSingleton<IMessageWriter, ConsoleMessageWriter>(); services.AddTransient<ProductService>(); ServiceProvider provider = services.BuildServiceProvider(); ProductService service = provider.GetRequiredService<ProductService>(); service.Save(); interface IMessageWriter { void Write(string message); } class ConsoleMessageWriter : IMessageWriter { public void Write(string message) { Console.WriteLine(message); } } class ProductService { private readonly IMessageWriter _writer; public ProductService(IMessageWriter writer) { _writer = writer; } public void Save() { _writer.Write("保存成功"); } }

作业验收

完成后检查:

  1. 能解释什么是依赖。
  2. 能解释什么是从外部注入。
  3. 能手动写构造函数注入。
  4. 能把实现从 ConsoleMessageWriter 换成 MemoryMessageWriter
  5. 能说出 Singleton 和 Transient 的区别。
  6. 能用 DI 容器注册接口和实现。

本页最后要记住

  1. 依赖是当前类需要的对象。
  2. 注入是从外部把依赖传进来。
  3. 构造函数注入是最常见写法。
  4. 业务类应该依赖接口,而不是直接依赖具体实现。
  5. 组合根负责统一创建和组装对象。
  6. DI 容器是自动创建和管理依赖的工具。
  7. 生命周期决定对象创建和复用方式。