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 中的依賴關係注入

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。