你有把依賴注入玩壞?

前言

自從.NET Core給咱們呈現了依賴注入,在咱們項目中處處充滿着依賴注入,雖然一切都已幫咱們封裝好,但站在巨人的肩膀上,除了憑眺遠方,咱們也應平鋪好腳下的路,使用依賴注入不只僅只是解耦,並且使代碼更具維護性,同時咱們也可垂手可得查看依賴關係,單元測試也可輕鬆完成,本文咱們來聊聊依賴注入,文中示例版本皆爲5.0。app

淺談依賴注入

在話題開始前,咱們有必要再提一下三種服務注入生命週期, 由淺及深再進行講解,基礎內容,我這裏再也不多述廢話框架

Transient(瞬時):每次對瞬時的檢索都會建立一個新的實例。ide

Singleton(單例):僅被實例化一次。此類型請求,老是返回相同的實例。單元測試

Scope(範圍):使用範圍內的註冊。將在請求類型的每一個範圍內建立一個實例。測試

 

若是已用過.NET Core一段時間,若對上述三種生命週期管理的概念沒有更深入的理解,我想有必要基礎回爐重塑下。爲何?至少咱們應該得出兩個基本結論ui

 

其一:生命週期由短到長排序,瞬時最短、範圍次之、單例最長url

 

只要作過Web項目,關於第一點就很好理解,首先咱們只對瞬時和範圍做一個基本的概述,關於單例經過實際例子來闡述,咱們理解會更深入spa

 

若爲瞬時:那麼咱們每次從容器中獲取的服務將是不一樣的實例,因此名爲瞬時或短暫命令行

 

若爲範圍:在ASP.NET Core中,針對每一個HTTP請求都會建立DI範圍,當在HTTP請求中(在中間件,控制器,服務或視圖中)請求服務,而且該服務註冊爲範圍服務時,若是在請求中屢次請求相同類型的請求,則使用相同實例。例如,若是在控制器,服務和視圖中注入了範圍服務,則將返回相同的實例。隨着另外一個HTTP請求的流,使用了不一樣的實例,請求完成後,將處理(釋放)範圍設計

 

其二:被注入的服務應與注入的服務應具備相同或更長的生命週期

 

從概念上看貌似有點拗口,經過平常生活舉個栗子則秒懂,假設有兩個桶,一個小桶和一個大桶,咱們能將小桶裝進大桶,但不能將大桶裝進小桶。

 

專業一點講,好比一個單例服務能夠被注入瞬時服務,可是一個瞬時服務不能被注入單例服務,由於單例服務比瞬時服務生命週期更長,若瞬時服務被注入單例服務,那麼勢必將延長瞬時服務生命週期,因違背大前提,將會引發異常

public interface ISingletonDemo1
{
}

public class SingletonDemo1 : ISingletonDemo1
{
    private readonly IScopeDemo1 _scopeDemo1;
    public SingletonDemo1(IScopeDemo1 scopeDemo1)
    {
        _scopeDemo1 = scopeDemo1;
    }
}

public interface IScopeDemo1
{
}
public class ScopeDemo1 : IScopeDemo1
{
}

咱們在Web中進行演示,而後在Startup中根據其接口名進行註冊,以下:

services.AddSingleton<ISingletonDemo1, SingletonDemo1>();
services.AddScoped<IScopeDemo1, ScopeDemo1>();

從理論上講確定是這樣,好像有點太絕對,抱着自我懷疑的態度,因而乎,咱們在控制檯中驗證一下看看

static void Main(string[] args)
{
    var services = new ServiceCollection();
    services.AddSingleton<ISingletonDemo1, SingletonDemo1>();
    services.AddScoped<IScopeDemo1, ScopeDemo1>();

    services.BuildServiceProvider();
}

然鵝並無拋出任何異常,注入操做都同樣,有點懵,看看各位看官可否給個合理的解釋,在控制檯中並不會拋出異常......

深談依賴注入

關於依賴注入基礎和使用準則,我建議你們去看看,仍是有不少細節須要注意

依賴注入設計準則

https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines

 

在.NET Core中使用依賴注入

https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-usage

好比其中提到一點,服務容器並不會建立服務,也就是說以下框架並無自動處理服務,須要咱們開發人員本身負責處理服務的釋放

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(new ExampleService());

    // ...
}

假設咱們有一個控制檯命令行項目,咱們經過引入依賴注入單例作一些操做

public interface ISingletonService
{
    void Execute();
}

public class SingletonService : ISingletonService
{
    public void Execute()
    {
    }
}

緊接着控制檯入口點演變成以下這般

static void Main(string[] args)
{
    var serviceProvider = new ServiceCollection()
        .AddSingleton<ISingletonService, SingletonService>()
        .BuildServiceProvider();

    var app = serviceProvider.GetService<ISingletonService>();
    app.Execute();
}

若在執行Execute方法裏面作了一些臨時操做,好比建立臨時文件,咱們想在釋放時手動作一些清理,因此咱們實現IDisposable接口,以下:

public class SingletonService : ISingletonService, IDisposable
{
    public void Execute()
    {
    }

    public void Dispose()
    {
        // do something
    }
}

而後項目上線,咱們可能會發現內存中大量充斥着該實例,從而最終致使內存泄漏,這是爲什麼呢?咱們將服務注入到容器中,容器將會自動管理注入實例的釋放,根據以下可知

 

最終咱們經過以下方式便可解決上述內存泄漏問題

using (var serviceProvider = new ServiceCollection()
                .AddSingleton<ISingletonService, SingletonService>()
                .BuildServiceProvider())
{
    var app = serviceProvider.GetService<ISingletonService>();

    app.Execute();
}

是否是有點懵,接下來咱們來深刻探討三種類型生命週期釋放問題,尤爲是單例,首先咱們經過注入自增加來標識每個注入服務,便於查看釋放時機對應標識

public interface ICountService
{
    int GetCount();
}

public class CountService : ICountService
{
    private int _n = 0;
    public int GetCount() => Interlocked.Increment(ref _n);
}

接下來則是定義瞬時、範圍、單例服務,並將其進行注入,以下:

public interface ISingletonService
{
    void Say();
}

public class SingletonService : ISingletonService, IDisposable
{
    private readonly int _n;
    public SingletonService(ICountService countService)
    {
        _n = countService.GetCount();
        Console.WriteLine($"構造單例服務-{_n}");
    }

    public void Say() => Console.WriteLine($"調用單例服務-{_n}");

    public void Dispose() => Console.WriteLine($"釋放單例服務-{_n}");

}

public interface IScopeSerivice
{
    void Say();
}

public class ScopeSerivice : IScopeSerivice, IDisposable
{
    private readonly int _n;
    public ScopeSerivice(ICountService countService)
    {
        _n = countService.GetCount();
        Console.WriteLine($"構造範圍服務-{_n}");
    }

    public void Say() => Console.WriteLine($"調用範圍服務-{_n}");

    public void Dispose() => Console.WriteLine($"釋放範圍服務-{_n}");
}

public interface ITransientService
{
    void Say();
}

public class TransientService : ITransientService, IDisposable
{
    private readonly int _n;
    public TransientService(ICountService countService)
    {
        _n = countService.GetCount();
        Console.WriteLine($"構造瞬時服務-{_n}");
    }

    public void Say() => Console.WriteLine($"調用瞬時服務-{_n}");

    public void Dispose() => Console.WriteLine($"釋放瞬時服務-{_n}");
}

最後在入口注入並調用相關服務,再加上最後打印結果,應該挺好理解的

static void Main(string[] args)
{
    var services = new ServiceCollection();

    services.AddSingleton<ICountService, CountService>();
    services.AddSingleton<ISingletonService, SingletonService>();
    services.AddScoped<IScopeSerivice, ScopeSerivice>();
    services.AddTransient<ITransientService, TransientService>();

    using (var serviceProvider = services.BuildServiceProvider())
    {
        using (var scope1 = serviceProvider.CreateScope())
        {
            var s1a1 = scope1.ServiceProvider.GetService<IScopeSerivice>();
            s1a1.Say();

            var s1a2 = scope1.ServiceProvider.GetService<IScopeSerivice>();
            s1a2.Say();

            var s1b1 = scope1.ServiceProvider.GetService<ISingletonService>();
            s1b1.Say();

            var s1c1 = scope1.ServiceProvider.GetService<ITransientService>();
            s1c1.Say();

            var s1c2 = scope1.ServiceProvider.GetService<ITransientService>();
            s1c2.Say();

            Console.WriteLine("--------------------------------釋放分界線");
        }

        Console.WriteLine("--------------------------------結束範圍1");

        Console.WriteLine();

        using (var scope2 = serviceProvider.CreateScope())
        {
            var s2a1 = scope2.ServiceProvider.GetService<IScopeSerivice>();
            s2a1.Say();

            var s2b1 = scope2.ServiceProvider.GetService<ISingletonService>();
            s2b1.Say();

            var s2c1 = scope2.ServiceProvider.GetService<ITransientService>();
            s2c1.Say();
        }

        Console.WriteLine("--------------------------------結束範圍2");
    }

    Console.ReadKey();
}

咱們描述下整個過程,經過容器建立一個scope1和scope2,並依次調用範圍、單例、瞬時服務,而後在scope和scope2結束時,釋放瞬時、範圍服務。最終在容器結束時,才釋放單例服務,從獲取、釋放以及打印結果來看,咱們能夠得出兩個結論

 

其一:每個scope被釋放時,瞬時和範圍服務都會被釋放,且釋放順序爲倒置

 

其二:單例服務在根容器釋放時纔會被釋放

 

有了上述結論2不難解釋咱們首先給出的假設控制檯命令行項目爲什麼會致使內存泄漏,若非手動實例化,實例對象生命週期都將由容器管理,但在構建容器時,咱們並未釋放(使用using),因此當咱們手動實現IDisposable接口,經過實現Dispose方法進行後續清理工做,但並不會進入該方法,因此會致使內存泄漏。看到這裏,我相信有一部分童鞋會有點大跌眼鏡,由於和沉浸在自我想象中的樣子不一致,實踐是檢驗真理的惟一標準,最後咱們對依賴注入作一個總結

 

在容器中註冊服務,容器爲了處理全部註冊實例,容器會跟蹤全部對象,即便是瞬時服務,也並非檢索完後,就一次性進行釋放,它依然在容器中保持「活躍」狀態,同時咱們也應防止GC釋放超出其範圍的瞬時服務

 

即便是瞬時服務也和做用域(scope)有關,經過引入做用域而進行釋放,不然根容器會一直保存其實例對象,形成巨大的內存損耗,甚至是內存泄漏

總結

💡 瞬時服務可做爲註冊服務的首選方法,範圍和單例用於共享狀態


💡 每個scope被釋放時,瞬時和範圍服務都會被釋放,且釋放順序爲倒置

 

💡 單例服務從不與做用域關聯,它們與根容器關聯,並在處置根容器時處理。

相關文章
相關標籤/搜索