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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM