本文示例代码,均采用 .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
类有多个构造函数,容器会根据实际情况,选择最合适的一个。
本文的代码流程如下:
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:如果同时存在多个满足前面规则的构造函数,则会抛出异常。
在复杂程序中,容器会分析服务的依赖项。从依赖关系树的最深处开始,依次构建,重复注入,以一种递归的方式,将最终需要的服务构建出来。
.NET 中的依赖关系注入