【导读】自从.NET Core给我们呈现了依赖注入,在我们项目中到处充满着依赖注入,虽然一切都已帮我们封装好,但站在巨人的肩膀上,除了凭眺远方,我们也应平铺好脚下的路

使用依赖注入不仅仅只是解耦,而且使代码更具维护性,同时我们也可轻而易举查看依赖关系,单元测试也可轻松完成,本文我们来聊聊依赖注入,文中示例版本皆为5.0。

浅谈依赖注入

在话题开始前,我们有必要再提一下三种服务注入生命周期, 由浅及深再进行讲解,基础内容,我这里不再多述废话

Transient(瞬时):每次对瞬时的检索都会创建一个新的实例。

Singleton(单例):仅被实例化一次。此类型请求,总是返回相同的实例。

Scope(范围):使用范围内的注册。将在请求类型的每个范围内创建一个实例。

如果已用过.NET Core一段时间,若对上述三种生命周期管理的概念没有更深刻的理解,我想有必要基础回炉重塑下。为什么?至少我们应该得出两个基本结论

其一:生命周期由短到长排序,瞬时最短、范围次之、单例最长

只要做过Web项目,关于第一点就很好理解,首先我们只对瞬时和范围作一个基本的概述,关于单例通过实际例子来阐述,我们理解会更深刻

若为瞬时:那么我们每次从容器中获取的服务将是不同的实例,所以名为瞬时或短暂

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

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

你有把依赖注入玩坏?相关推荐

  1. 【ASP.NET Core】依赖注入高级玩法——如何注入多个服务实现类

    依赖注入在 ASP.NET Core 中起中很重要的作用,也是一种高大上的编程思想,它的总体原则就是:俺要啥,你就给俺送啥过来.服务类型的实例转由容器自动管理,无需我们在代码中显式处理. 因此,有了依 ...

  2. Spring依赖注入的三种方式(好的 坏的和丑的)

    关于spring bean三种注入方式的优缺点对比,翻译自Spring DI Patterns: The Good, The Bad, and The Ugly,水平有限,如有错误请指正. Sprin ...

  3. 如来神掌 - 玩转 AngualrJS 的依赖注入

    先了解依赖 首先我们要了解什么是依赖:举个例子吧,比如你玩撸啊撸,你看到别人在玩小提莫,你觉得挺好玩,也想玩这个英雄,那么你就要去商店把这个英雄买下来,然后才可以使用这个英雄,拥有这个英雄就是你的依赖 ...

  4. java 什么时候依赖注入_玩框架java依赖注入 – 何时使用单例

    So I am wondering, should I be using singleton objects as the examples seem to imply? If this is the ...

  5. Jetpack新成员,一篇文章带你玩转Hilt和依赖注入

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新. 各位小伙伴们大家早上好. 终于要写这样一篇我自己都比较怕的文章了. 虽然今年的Google ...

  6. 依赖注入?依赖注入是如何实现解耦的?

    如何用最简单的方式解释依赖注入?依赖注入是如何实现解耦的? 第一章:小明和他的手机 从前有个人叫小明 小明有三大爱好,抽烟,喝酒-- 咳咳,不好意思,走错片场了.应该是逛知乎.玩王者农药和抢微信红包 ...

  7. 如何理解依赖注入(DI)

    什么是依赖注入(Dependency Injection) 依赖倒置原则:高层模块不应该依赖于低层模块,两个都应该依赖于抽象(接口). 依赖倒置是一种软件设计思想,在传统软件中,上层代码依赖于下层代码 ...

  8. 【C#|.NET】从控制反转(依赖注入)想到事件注入 (非AOP)

    前文 事件注入的想法是由依赖注入所联想到 依赖注入不算什么吸引人的话题 本篇就不详说了 不过有闲暇时间的机会不妨按照自己的兴趣去摸索.研究一些东西,也是一种乐子. 在抓虫系列里简单的描述一下依赖注入在 ...

  9. 漫谈可视化Prefuse(四)---被玩坏的Prefuse API

    这个双12,别人都在抢红包.逛淘宝.上京东,我选择再续我的"漫谈可视化"系列(好了,不装了,其实是郎中羞涩...) 上篇<漫谈可视化Prefuse(三)---Prefuse ...

最新文章

  1. python读写文件绝对路径_[Spark][Python]对HDFS 上的文件,采用绝对路径,来读取获得 RDD...
  2. python利器怎么用-bluepy 一款python封装的BLE利器简单介绍
  3. 鲁东大学计算机王跃,鲁东大学学子在“程序设计天梯赛”中取得优异成绩
  4. DAVINCI DM365-368中 linux-2.6.32的移植
  5. JavaScript校验网址
  6. BetterZip使用教程分享:Mac用户如何提取压缩文件?
  7. WxParse手机端报console.dir错误
  8. 2017.9.28 降雨量 思考记录
  9. 数据可视化之MATPLOTLIB实战:PLT.POLAR()函数 绘制极线图 (转载)
  10. 【Flink】did not rea ct to cancelling signal for 30 seconds, but is stuck No implementation was bound
  11. 包含对象的json格式_如何把JSON数据格式转换为Python的类对象?
  12. java高级能力_java高级技术工程师该具备哪些能力?
  13. excel表格分割线一分为二_高效秘技!用EXCEL制作导航页和日志表管理日常工作...
  14. linux中sendto函数路径,发送原始数据包时,sendto函数不使用struct sockaddr_ll中提供的MAC地址...
  15. 同比、环比的区别及计算公式
  16. Win7 自制关闭屏幕快捷键
  17. java十进制转换成二进制
  18. 如何通过二极管设计一个或门电路
  19. 开放平台设计方案与实践
  20. 29 | 无锁的原子操作:Redis如何应对并发访问?

热门文章

  1. Factory Method工厂方法
  2. PHP常用工具方法集...
  3. ELK之elasticsearch5.6的安装和head插件的安装
  4. Oracle中的USEREVN()
  5. C#委托,事件理解入门 (译稿)
  6. 转载 一篇UI规范文件
  7. php图片地址参数错误,图片上传时一直显示请求地址错误怎么办
  8. 什么是自然语言处理,它如何工作?
  9. mac按文件名查找文件_如何在Mac上查找和删除大文件
  10. quantum_如何从Firefox Quantum删除Pocket