Day 2 - 依赖注入
建议用时:260-320 分钟
你将学会什么
- 什么是依赖,什么是注入
- 为什么业务类不要自己到处
new具体实现 - 构造函数注入为什么是最常见写法
- 接口如何让实现可替换
- 什么是组合根,也就是统一创建对象的地方
- Singleton、Scoped、Transient 生命周期分别是什么意思
- 如何使用
Microsoft.Extensions.DependencyInjection注册和解析服务
依赖注入的核心不是容器,而是“使用对象的类,不负责创建它依赖的具体对象”。先把构造函数注入理解清楚,再看容器就不会乱。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“解决方案结构”,今天继续学习“依赖注入”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“依赖注入”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。依赖注入的名字听起来复杂,本质很简单:谁需要什么,从外面传给它。
1. 什么是依赖
一个类需要另一个对象帮它做事,这个对象就是它的依赖。
例如:
ProductService 需要保存商品
保存商品需要 IProductRepository
所以 IProductRepository 是 ProductService 的依赖2. 什么是注入
注入就是从外面传进来。
最常见写法是构造函数注入:
public ProductService(IProductRepository repository)意思是:
ProductService 要工作,需要外部给我一个 IProductRepository。3. 不注入会有什么问题
如果业务类自己创建依赖:
ProductService 里面直接 new FileProductRepository()问题是:
- 想换成数据库保存,要改
ProductService。 - 想测试
ProductService,必须真的写文件。 - 业务规则和保存细节绑在一起。
- 代码越来越难替换。
4. 注入之后有什么好处
注入之后:
ProductService 只依赖 IProductRepository好处:
- 文件保存可以换成内存保存。
- 内存保存可以换成数据库保存。
- 测试时可以用假的实现。
- 业务服务更专注业务规则。
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:错误写法,业务类自己 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这个包提供 ServiceCollection、AddSingleton、AddTransient、BuildServiceProvider 等常用 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 所有依赖 | 依赖关系固定,难替换测试 | 通过构造函数注入依赖 |
| 忘记注册服务 | 运行时容器找不到实现 | 在启动处 AddSingleton、AddTransient 等注册 |
| 生命周期乱用 | 对象状态可能共享错 | 无状态服务用 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:手动依赖注入
要求:
- 定义
IMessageWriter。 - 写
ConsoleMessageWriter。 ProductService通过构造函数接收IMessageWriter。- 调用
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:替换实现
要求:
- 新增
MemoryMessageWriter。 - 不修改
ProductService。 - 改入口处的具体实现即可。
完整答案
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("保存成功");
}
}作业验收
完成后检查:
- 能解释什么是依赖。
- 能解释什么是从外部注入。
- 能手动写构造函数注入。
- 能把实现从
ConsoleMessageWriter换成MemoryMessageWriter。 - 能说出 Singleton 和 Transient 的区别。
- 能用 DI 容器注册接口和实现。
本页最后要记住
- 依赖是当前类需要的对象。
- 注入是从外部把依赖传进来。
- 构造函数注入是最常见写法。
- 业务类应该依赖接口,而不是直接依赖具体实现。
- 组合根负责统一创建和组装对象。
- DI 容器是自动创建和管理依赖的工具。
- 生命周期决定对象创建和复用方式。