本文示例代码,均采用 .NET 6,具体的代码可以在这个仓库 Articles.DI 中获取。
前面的文章中,我们提及了依赖注入的基本使用。我们使用了简单的案例,注册了IMessageWriter
接口,以及编写了两个实现类MessageWriter
和LoggingMessageWriter
,但是它们二者都只有一个构造函数。如果我们注册服务时,实现类有多个构造函数时,容器该如何选择呢?
如何选择构造函数#
我们可以直接写代码,来模拟这个场景。有一个服务 ExampleService,它有多个构造函数,这些构造函数所需参数的数量有多有少,同时需要的类型也各有不同,具体看下面的代码:
// https://github.com/alva-lin/Articles.DI/tree/master/WorkerService3
public class ExampleService
{
public ExampleService() => Console.WriteLine("空的构造函数");
public ExampleService(AService aService) =>
Console.WriteLine("单参数构造函数:AService");
public ExampleService(AService aService, BService bService) =>
Console.WriteLine("双参数构造函数:AService, BService");
public ExampleService(AService aService, CService cService) =>
Console.WriteLine("双参数构造函数:AService, CService");
}
public class AService
{
public AService() => Console.WriteLine("AService 实例化");
}
public class BService
{
public BService() => Console.WriteLine("BService 实例化");
}
public class CService
{
public CService() => Console.WriteLine("CService 实例化");
}
ExampleService
类有四个构造函数,分别依赖三个服务,我们在注册服务时,只注册AService
和BService
。
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
services.AddSingleton<ExampleService>();
// 尝试注释(or 取消注释)下面的代码,形成不同组合,运行以查看输出结果
services.AddSingleton<AService>();
services.AddSingleton<BService>();
// services.AddSingleton<CService>();
})
.Build();
await host.RunAsync();
public class Worker : BackgroundService
{
private readonly ExampleService _exampleService;
// 注入了 ExampleService 的实例,但是调用了它的哪个构造函数?
public Worker(ExampleService exampleService)
{
_exampleService = exampleService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// 不执行任何操作
}
}
上述代码的执行结果为
AService 实例化
BService 实例化
双参数构造函数:AService, BService
从结果可以看出,容器在实例化ExampleService
类时,使用了第三个构造函数。对比所有的构造函数,第三和第四个构造函数所需的参数数量最多,而第四个构造函数需要CService
类,我们并未在容器中注册这个服务,所以容器不会选择第四个构造函数。那么我们可以明白,容器选择构造函数的一部分规则:
规则 1:构造函数所需的参数类型必须是在容器中注册过的;
规则 2:尽可能选择参数最多的构造函数;
如果我们在注册服务时,将CService
一起注册,再运行一遍,这时程序就会报错:
Unhandled exception. System.AggregateException:
Some services are not able to be constructed
(Error while validating the service descriptor
'ServiceType: Microsoft.Extensions.Hosting.IHostedService
Lifetime: Singleton
ImplementationType: WorkerService3.Worker':
Unable to activate type 'WorkerService3.ExampleService'.
The following constructors are ambiguous:
Void .ctor(WorkerService3.AService, WorkerService3.BService)
Void .ctor(WorkerService3.AService, WorkerService3.CService))
...
错误信息指出无法构建ExampleService
类型,两个构造函数有歧义,无法选择。那么我们可以知道第三个规则:
规则 3:如果同时存在多个满足前面规则的构造函数,则会抛出异常。
依赖关系图#
上述代码中,Worker
类依赖ExampleService
类,而ExampleService
类又依赖其他类,形成一个链式的依赖,那么容器在实例化Worker
类时,会根据找到它的构造函数,Worker
类只有一个构造函数,声明了需要一个ExampleService
类型的示例,那么容器就继续实例化ExampleService
类,找到它的构造函数,而ExampleService
类有多个构造函数,容器会根据实际情况,选择最合适的一个。
本文的代码流程如下:
- 创建 HostBuilder 时,注册后台服务
Worker
,以及其他服务(services.add...); - 启动后台服务,即
Worker
类(await host.RunAsync ();) - 容器实例化
Worker
类,找到其构造函数,解析所需的参数。找到了ExampleService
类; - 容器实例化
ExampleService
类,找到它有多个构造函数; - 从参数数量最多的构造函数开始,对比是否能满足其条件,筛选出最满足需求的一个;
- 选择第三个构造函数,实例化
AService
和BService
,因为二者构造函数简单,直接生成即可; - 将
AService
和BService
实例,注入到ExampleService
类,完成实例化; - 将
ExampleService
实例,注入到Worker
类,完成实例化;
从Worker
类到ExampleService
类,再到AService
和BService
,这是一个树形依赖关系。而容器实例化Worker
类时,根据这个依赖关系,依次深入,生成一个个依赖项,将其递归式的注入。
总结#
容器在构建实例时,选择构造函数的规则如下:
规则 1:构造函数所需的参数类型必须是在容器中注册过的;
规则 2:尽可能选择参数最多的构造函数;
规则 3:如果同时存在多个满足前面规则的构造函数,则会抛出异常。
在复杂程序中,容器会分析服务的依赖项。从依赖关系树的最深处开始,依次构建,重复注入,以一种递归的方式,将最终需要的服务构建出来。