如题。
为什么要依赖注入,简言之为了解耦。
对一些概念做一些拆解,网上的说法一锅粥,容易糊涂。
依赖:
一个人类,每个人出来就应该有100块钱。直觉上,会这么写(别去纠结钱类是啥):
internal class 人 { 钱 _钱; public 人() { _钱 = new 钱("一百块"); } }
这段逻辑里,人类对钱类产生了依赖,即:没有钱就不是人。
这么写没问题,但先进的编程理念告诉我们:类应当尽量封闭,不与外界相关。即:人类应该尽量关心自己的事,少去“挣钱”(即new 钱),这是不务正业。
那么没钱又不行,不挣怎么办呢?答案:直接拿。
注入:
看下面的代码
internal class 人 { 钱 _钱; public 人(钱 一笔钱) { _钱 = 一笔钱; } }
在这里,人类不new钱,即不挣钱,也就不会不务正业。需要的钱,在构造方法里,从外界获得。这个动作,就是注入(外界给人注入钱)。构造方法的参数,就是注入点。
获得钱的方式从类内到了类外,这就是控制反转(IoC)。
注入的好处:人类更单纯了,给钱办事。原先需要挣的钱,如果从人民币变成美刀,需要修改类代码。现在,看老板怎么给,给啥都行,我不操心。
容器:
通过注册的方式,可以列张表,说明什么类对象用什么实现。
例如:有个接口叫“报酬”,钱类、物品类、以身相许类都实现这个接口。
我想以后只要提到“报酬”就表示“以身相许”。那么表里就应该是:
类型 | 服务 |
报酬 | 以身相许 |
这个表就是容器,它管理一系列对象的关系,甚至可以创建这些对象。我们可以这样用:
报酬 a=容器.给个对象<报酬>();
这时候容器会创建一个“以身相许”的类对象赋值给a。
假设人类变成这样:
internal class 人 { 报酬 _报酬; public 人(报酬 一笔报酬) { _报酬 = 一笔报酬; } }
而表格里增加一行:
类型 | 服务 |
报酬 | 以身相许 |
人 | 人 |
这时候想要获得一个人类的对象,可以写成:
var a=容器.给个对象<人>();
容器在构造人的时候发现还需要报酬,会根据表,自动传递一个“以身相许”的对象过去。
以后你想修改程序,把“报酬”变成“物品”,只需要修改这张表(容器)就行了。所有注册了的,使用了“报酬”做构造参数的地方就全都改过来了。很省事。
小结:
依赖是客观存在的。注入可以让类更封闭。容器可以对注入进行管理。
也许会出现容器管不了/管不到的注入,不必强求,离了筷子一样吃东西。
控制台下做了些验证性的例子,想用最简单的代码说清楚它。
概述:
接口Person,派生类Chinese和American。一个应用类Test。自己写了个做服务的类“MyService”,利用“ServiceCollection”类的对象注册(也就是描述接口和类的对应关系),并通过它的“BuildServiceProvider()”方法来生成“IServiceProvider”对象"provider"(即容器)。后续创建实例都是通过“provider”来完成。
完成以上操作后,通过容器初始化Test对象时,参数里的几个类,容器会自动生成。如需更改实现,修改注册内容即可。
前提:通过nuget下载“Microsoft.Extensions.DependencyInjection”包。
Person.cs:
internal interface Person:IDisposable { void showMe(); }
Chinese.cs:
internal class Chinese:Person { int c = 0; public string Name { get; set; } public Chinese() { Name = "无名氏"; } public Chinese(string name) { Name = name; } public void Dispose() { Console.WriteLine("中国人对象销毁"); } public void showMe() { Console.WriteLine($"{Name}:你好,{c++}"); } }
两个构造函数是为了演示有参/无参注册和使用DI。
American.cs:
internal class American:Person { int c = 0; public void Dispose() { Console.WriteLine("American is disposed."); } public void showMe() { Console.WriteLine($"hello!{c++}"); } }
Test.cs:
internal class Test { Person person; Chinese chinese; American american; public Test(Person p,Chinese c,American a) { person = p; chinese = c; american = a; } public void showTest() { person.showMe(); chinese.showMe(); american.showMe(); } }
MyService.cs:
1 internal class MyService 2 { 3 static IServiceProvider provider=getMyService(); 4 public static IServiceProvider getMyService() 5 { 6 var services = new ServiceCollection(); 7 services.AddSingleton<Test>(); 8 //services.AddSingleton<Person,Chinese>(); 9 services.AddSingleton<Chinese>(); 10 services.AddSingleton(new American()); 11 services.AddTransient<Person>(sp=>new Chinese("张三")); 12 return services.BuildServiceProvider(); 13 } 14 public static T? getInstance<T>() 15 { 16 return provider.GetService<T>(); 17 } 18 19 public void Dispose() 20 { 21 Console.WriteLine("MyService is disposed."); 22 } 23 }
可以用第8行取代第11行查看效果。第16行,如果使用GetServices<T>()可以返回一个IEnumerable类型的集合。tolist的话,可以提取重复注册的几个类对象。
一个泛型表示注册这个类,两个表示后面的类实现前面的接口。
对于三个作用域,AddTransient每次都是新对象;AddSingleton从头到尾都是一个对象;AddScoped一般用于web请求,在当前会话有效。
微软官方说法如下:
暂时生存期服务是每次从服务容器进行请求时创建的。 这种生存期适合轻量级、 无状态的服务。 向 AddTransient 注册暂时性服务。
在处理请求的应用中,在请求结束时会释放暂时服务。
范围内
对于 Web 应用,指定了作用域的生存期指明了每个客户端请求(连接)创建一次服务。 向 AddScoped 注册范围内服务。
在处理请求的应用中,在请求结束时会释放有作用域的服务。
使用 Entity Framework Core 时,默认情况下 AddDbContext 扩展方法使用范围内生存期来注册 DbContext 类型。
创建单例生命周期服务的情况如下:
在首次请求它们时进行创建;或者
在向容器直接提供实现实例时由开发人员进行创建。 很少用到此方法。
来自依赖关系注入容器的服务实现的每一个后续请求都使用同一个实例。 如果应用需要单一实例行为,则允许服务容器管理服务的生存期。 不要实现单一实例设计模式,或提供代码来释放单一实例。 服务永远不应由解析容器服务的代码释放。 如果类型或工厂注册为单一实例,则容器自动释放单一实例。
向 AddSingleton 注册单一实例服务。 单一实例服务必须是线程安全的,并且通常在无状态服务中使用。
在处理请求的应用中,当应用关闭并释放 ServiceProvider 时,会释放单一实例服务。 由于应用关闭之前不释放内存,因此请考虑单一实例服务的内存使用。
主程序:
using ConsoleApp1; Test? t= MyService.getInstance<Test>(); t?.showTest();
也可用下面的主程序调试:
1 using ConsoleApp1; 2 3 //using (var p = MyService.getInstance<Person>()) 4 //{ 5 // p.showMe(); 6 //} 7 //以上写法给出作用域,可执行/检验销毁(dispose)方法。 8 9 for (int i = 0; i < 3; i++) 10 { 11 var p = MyService.getInstance<Person>(); 12 p?.showMe();//"?."先检查p是否为空,是则不进行后续操作,返回null 13 }
在.net6 core mvc中,可以在Program.cs中注册依赖注入。
C1.cs
public class C1 { public string msg { get; set; } = "hi"; }
Program.cs
1 using WebApplication1; 2 3 var builder = WebApplication.CreateBuilder(args); 4 5 // Add services to the container. 6 builder.Services.AddControllersWithViews(); 7 builder.Services.AddScoped<C1>(); 8 9 var app = builder.Build(); 10 11 // Configure the HTTP request pipeline. 12 if (!app.Environment.IsDevelopment()) 13 { 14 app.UseExceptionHandler("/Home/Error"); 15 } 16 app.UseStaticFiles(); 17 18 app.UseRouting(); 19 20 app.UseAuthorization(); 21 22 app.MapControllerRoute( 23 name: "default", 24 pattern: "{controller=Home}/{action=Index}/{id?}"); 25 26 app.Run();
其中除了第7行,都是自动生成的。
控制器HomeController.cs:
1 public class HomeController : Controller 2 { 3 private readonly ILogger<HomeController> _logger; 4 C1 _c1; 5 6 public HomeController(ILogger<HomeController> logger,C1 c1) 7 { 8 _logger = logger; 9 _c1 = c1; 10 } 11 12 public IActionResult Index() 13 { 14 ViewData["Message"] = _c1.msg; 15 return View(); 16 }
16行后面的方法略。
这里添加全局变量_c1,在构造方法里自动调用注册的内容,并把值传给页面显示。
index.cshtml包含如下代码:
<h1 class="display-4">@ViewData["Message"].ToString() Welcome</h1>
运行结果: