.NET 中的依赖注入(三):依赖关系和构造函数发现规则

本文示例代码,均采用 .NET 6,具体的代码可以在这个仓库 Articles.DI 中获取。

前面的文章中,我们提及了依赖注入的基本使用。我们使用了简单的案例,注册了 IMessageWriter 接口,以及编写了两个实现类 MessageWriterLoggingMessageWriter,但是它们二者都只有一个构造函数。如果我们注册服务时,实现类有多个构造函数时,容器该如何选择呢?

如何选择构造函数

我们可以直接写代码,来模拟这个场景。有一个服务 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 类有四个构造函数,分别依赖三个服务,我们在注册服务时,只注册 AServiceBService

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 类有多个构造函数,容器会根据实际情况,选择最合适的一个。

本文的代码流程如下:

  1. 创建 HostBuilder 时,注册后台服务 Worker,以及其他服务(services.add...);
  2. 启动后台服务,即 Worker 类(await host.RunAsync();)
  3. 容器实例化 Worker 类,找到其构造函数,解析所需的参数。找到了 ExampleService 类;
  4. 容器实例化 ExampleService 类,找到它有多个构造函数;
  5. 从参数数量最多的构造函数开始,对比是否能满足其条件,筛选出最满足需求的一个;
  6. 选择第三个构造函数,实例化 AServiceBService,因为二者构造函数简单,直接生成即可;
  7. AServiceBService 实例,注入到 ExampleService 类,完成实例化;
  8. ExampleService 实例,注入到 Worker 类,完成实例化;

Worker 类到 ExampleService 类,再到 AServiceBService,这是一个树形依赖关系。而容器实例化 Worker 类时,根据这个依赖关系,依次深入,生成一个个依赖项,将其递归式的注入。

总结

容器在构建实例时,选择构造函数的规则如下:

规则1:构造函数所需的参数类型必须是在容器中注册过的;
规则2:尽可能选择参数最多的构造函数;
规则3:如果同时存在多个满足前面规则的构造函数,则会抛出异常。

在复杂程序中,容器会分析服务的依赖项。从依赖关系树的最深处开始,依次构建,重复注入,以一种递归的方式,将最终需要的服务构建出来。

参考链接

.NET 中的依赖关系注入

上一篇:面试官:你给我说一下线程池里面的几个锁吧。


下一篇:面试官:你给我说一下线程池里面的几把锁。