你有把依赖注入玩坏?

前言

自从.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被释放时,瞬时和范围服务都会被释放,且释放顺序为倒置

 

💡 单例服务从不与做用域关联,它们与根容器关联,并在处置根容器时处理。

相关文章
相关标签/搜索