.Net 中使用 DI
依赖注入(Dependency Injection,DI)是控制反转(Inversion of Control,IOC)思想的实现方式。
依赖注入简化模块的组装过程,降低模块之间的耦合度
为什么使用DI?
假设有这样的接口和实现类:
public interface ISayHello
{
public void SayHello();
}
public class Chinese : ISayHello, IDisposable
{
public void SayHello()
{
Console.WriteLine("你好!");
}
public void Dispose()
{
Console.WriteLine("资源释放了");
}
}
调用方式:
ISayHello sayHello = new Chinese();
sayHello.SayHello();
基于接口编程自有其好处,但是如上面的方式还是有缺陷的。调用者必须知道是谁实现了ISayHello
接口的SayHello
方法。这里就需要知道一个接口,一个实现类,加上本身调用的地方,一个简单的调用就有三者耦合。
DI 原理
下面让我们通过 DI 的方式实现,首先是 nuget 安装:
Install-Package Microsoft.Extensions.DependencyInjection -Version 6.0.0
上面包是微软实现的实现了依赖注入的包,看看怎么使用(最麻烦的使用方式):
ServiceCollection serviceContainer = new ServiceCollection(); // 构建一个服务容器
serviceContainer.AddTransient<Chinese>(); // 把 Chinese 作为瞬态服务(对象),添加到服务器容器
// 通过容器构建一个 ServiceProvider(服务提供器),ServiceProvider 可以提供容器中管理的服务(已添加到容器的服务)
using (ServiceProvider serviceProvider = serviceContainer.BuildServiceProvider())
{
ISayHello sayHello = serviceProvider.GetService<Chinese>(); // 从 ServiceProvider 中获取需要的服务(对象)
sayHello.SayHello();
}
- 首先创建一个服务(对象)容器
- 把需要服务容器管理的服务(对象)添加到服务容器
- 通过服务容器构建一个 ServiceProvider(服务提供器),ServiceProvider 可以提供容器管理的服务(已添加到容器的服务)
- 通过 ServiceProvider(服务提供器)获取需要的服务(对象)
DI 的生命周期
Transient(瞬态)
使用一次即销毁,下次使用将是一个新的对象
Singleton(单例)
顾名思义,参考单例模式
Scoped(范围)
在一个范围内使用同一个对象
可以通过ServiceProvider
的CreateScope
方法创建新的范围:
ServiceCollection serviceContainer = new ServiceCollection();
serviceContainer.AddScoped<Chinese>();
using (ServiceProvider serviceProvider = serviceContainer.BuildServiceProvider())
{
ISayHello sayHello1 = serviceProvider.GetService<Chinese>();
ISayHello sayHello2 = serviceProvider.GetService<Chinese>();
ISayHello sayHello3 = null; // 应该在 Scope 范围内声明变量
ISayHello sayHello4 = null; // 应该在 Scope 范围内声明变量
ISayHello sayHello5 = null; // 应该在 Scope 范围内声明变量
ISayHello sayHello6 = null; // 应该在 Scope 范围内声明变量
using (IServiceScope serviceScope = serviceProvider.CreateScope())
{
sayHello3 = serviceScope.ServiceProvider.GetService<Chinese>();
sayHello4 = serviceScope.ServiceProvider.GetService<Chinese>();
}
using (IServiceScope serviceScope = serviceProvider.CreateScope())
{
sayHello5 = serviceScope.ServiceProvider.GetService<Chinese>();
sayHello6 = serviceScope.ServiceProvider.GetService<Chinese>();
}
Console.WriteLine(Object.ReferenceEquals(sayHello1, sayHello2)); // True, 都是通过 serviceProvider 创建
Console.WriteLine(Object.ReferenceEquals(sayHello3, sayHello4)); // True, 都是通过同一个 serviceScope 创建,在单独的一个范围中
Console.WriteLine(Object.ReferenceEquals(sayHello1, sayHello4)); // False,在单独的一个范围中,所以和 serviceProvider 创建的不一致
Console.WriteLine(Object.ReferenceEquals(sayHello5, sayHello6)); // True, 同 sayHello3, sayHello4
Console.WriteLine(Object.ReferenceEquals(sayHello1, sayHello6)); // False,同 sayHello1, sayHello4
Console.WriteLine(Object.ReferenceEquals(sayHello4, sayHello6)); // False,在两个 serviceScope 中,所以 False
}
为了确保对象正确的生命周期,应该在 Scope 范围内声明变量
由上图可以知道,如果实现类实现了 IDisposable
接口,则在离开作用域之后,容器会自动调用对象的Dispose
方法。
也印证了需要在范围内声明并使用对象,在外面可能资源被释放了
如何选择生命周期?
如果一个类是无状态的,这时可以选择 Singleton。因为无状态的类无所谓并发问题(不会修改状态)。
如果一个类有状态,并且有 Scope 控制,可以注册为 Scoped。因为 Scope 控制的代码都是运行在同一个线程中的,没有并发修改的问题。
最后 Transient 在使用时,需要谨慎,因为每次都会创建一个新的对象。