本文示例代码,均采用 .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
類有多個構造函數,容器會根據實際情況,選擇最合適的那一個。
本文的代碼流程如下:
- 創建 HostBuilder 時,註冊後台服務
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:如果同時存在多個滿足前面規則的構造函數,則會拋出異常。
在複雜程序中,容器會分析服務的依賴項。從依賴關係樹的最深處開始,依次構建,重複注入,以一種遞歸的方式,將最終需要的服務構建出來。