Alva

Alva

programmer
github
telegram
email

dotnet 中的依赖注入-03.依赖关系和构造函数发现规则

本文示例代码,均采用 .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 中的依赖关系注入

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。